线程与更新UI,细谈原理(下)

简介: 相信不少读者都阅读过相类似的文章了,但是我还是想完整的把这之间的关系梳理清楚,细节聊好,希望你也能从中学到一些。

案例二,子线程和主线程分别showToast


1)onCreate方法中弹出toast,崩溃——Can't toast on a thread that has not called Looper.prepare()


override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)
        thread {
            showToast("我去年买了个表")
        }
    }


2)onCreate方法中弹出toast,增加Looper.prepare(),Looper.loop()方法。不崩溃。


加上延时3秒,不崩溃。


override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)
        thread {
            //Thread.sleep(3000)
            Looper.prepare()
            showToast("我去年买了个表")
            Looper.loop()
        }
    }


3)使用同一个Toast实例,在子线程中的Toast没消失之前点击按钮,在主线程中修改Toast文字并显示,则程序崩溃——Only the original thread that created a view hierarchy can touch its views.。


重新运行,在子线程中显示并消失后,点击按钮,不崩溃。


换个手机——三星s9,重新运行,在子线程中的Toast没消失之前点击按钮,不崩溃。


lateinit var mToast: Toast
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)
        thread {
            Looper.prepare()
            mToast=Toast.makeText(this@UIMainActivity,"我去年买了个表",Toast.LENGTH_LONG)
            mToast.show()
            Looper.loop()
        }
        btn_ui.setOnClickListener {
            mToast.setText("我今年买了个表")
            mToast.show()
        }
    }


案例二分析


在解开谜底之前,我们先了解下Toast


Toast原理


Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()


简单又常用的一句代码,还是通过流程图的方式看看它是怎么创建并展示的。


2.png


DecorView加载绘制流程如出一辙,首先加载了布局文件,创建了View。然后通过addView方法,再次新建一个ViewRootImpl实例,作为parent,进行测量布局和绘制。


崩溃分析


1)首先,说下第一次崩溃——Can't toast on a thread that has not called Looper.prepare(),也就是在创建Toast的线程必须要有Looper在运行。


根据源码我们也得知Toast的显示和隐藏都是通过Handler传递消息的,所以必须要有Handler使用环境,也就是绑定Looper对象,并且通过loop方法开始循环处理消息。


2)第二次崩溃——Only the original thread that created a view hierarchy can touch its views


这里的崩溃和之前更新Button一样的报错,所以我们有理由怀疑也是一样的原因,在不同的线程调用了ViewRootImplrequestLayout方法。


我们看到点击按钮的时候,调用了mToast.setText()方法,咦,这不就跟案例一一模一样了吗。


setText方法中调用了TextViewsetText()方法,然后由于Toast中的TextView宽高都是wrap_content的,所以会触发requestLayout方法,最后会调用到最上层View也就是ViewRootImplrequestLayout方法。


所以崩溃的原因就是因为Toast在第一次在子线程中show的时候,新建了一个ViewRootImpl实例,绑定了当前线程也就是子线程到mThread变量。然后同一个Toast,在主线程调用setText方法,最终会调用到ViewRootImpl的requestLayout方法,引起线程检查,当前线程也就是主线程并不是当初那个创建ViewRootImpl实例的线程,所以导致崩溃。


3)那为什么等Toast消失之后,点击按钮又不崩溃了呢?


原因就在Toast的hide方法中,最终会调用到View的assignParent方法,将Toast的mParent设置为null,也就是ViewRootImpl设置为null了。所以调用setText方法的时候也就执行不到requestLayout方法了,也就不会到checkThread方法检查线程了。贴下代码:


public void handleHide() {
    if (mView != null) {
        if (mView.getParent() != null) {
            mWM.removeViewImmediate(mView);
        }
        mView = null;
    }
}
removeViewImmediate--->removeViewLocked
private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
 //...
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}
void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"+ " it already has a parent");
        }
}


4)但是但是,为啥换个手机又不崩溃了呢?


这是我偶然发现的,在我的三星S9手机上,运行时不会崩溃的,而且界面给我的反馈并不是修改当前页面上Toast上的文字,而是像新建了一个Toast展示,即时代码中写的是setText方法。


所以我猜测在部分手机上,应该是改变了Toast的设置,当调用setText方法的时候,就会马上结束当前的Toast展示,调用hide方法。然后再进行Toast文字修改并展示,也就是刚才第三点的做法。


当然这只是我的猜测,有研究过手机源码的大神也可以补充下。


总结


任何线程都可以更新UI,也都有更新UI导致崩溃的可能。


其中的关键就是view被绘制到界面时候的线程(也就是最顶层ViewRootImpl被创建时候的线程)和进行UI更新时候的线程是不是同一个线程,如果不是就会报错。


参考


https://www.jianshu.com/p/1cdd5d1b9f3d

https://www.cnblogs.com/fangg/p/12917235.html


拜拜~

目录
相关文章
|
28天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
56 0
|
28天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
2天前
|
存储 开发框架 JavaScript
深入探讨Flutter中动态UI构建的原理、方法以及数据驱动视图的实现技巧
【6月更文挑战第11天】Flutter是高效的跨平台移动开发框架,以其热重载、高性能渲染和丰富组件库著称。本文探讨了Flutter中动态UI构建原理与数据驱动视图的实现。动态UI基于Widget树模型,状态变化触发UI更新。状态管理是关键,Flutter提供StatefulWidget、Provider、Redux等方式。使用ListView等可滚动组件和StreamBuilder等流式组件实现数据驱动视图的自动更新。响应式布局确保UI在不同设备上的适应性。Flutter为开发者构建动态、用户友好的界面提供了强大支持。
18 2
|
13天前
|
监控 Java 调度
Java并发编程:线程池的原理与实践
【5月更文挑战第30天】 在现代软件开发中,尤其是Java应用中,并发编程是一个不可忽视的领域。线程池作为提升应用性能和资源利用率的关键技术之一,其正确使用和优化对系统稳定性和效率至关重要。本文将深入探讨线程池的核心原理、常见类型以及在实际开发中的使用案例,旨在帮助开发者更好地理解和运用线程池技术,构建高性能的Java应用程序。
|
14天前
|
监控 Java 开发者
深入理解Java并发编程:线程池的工作原理与实践
【5月更文挑战第29天】 在现代Java应用开发中,高效地管理并发任务是至关重要的。本文将深入探讨Java线程池的核心机制,揭示其背后的设计哲学和运作模式。通过分析线程池的优势、工作过程及关键参数,结合实例演示如何合理配置和优化线程池以提高应用程序的性能和响应能力。
|
14天前
|
Java 开发者
Java并发编程:理解线程池的工作原理与实践应用
【5月更文挑战第29天】在Java并发编程中,线程池是一种管理线程资源的重要工具。通过深入探讨线程池的工作原理和实践应用,本文将帮助开发者更好地理解和使用线程池,提高系统性能和稳定性。
|
15天前
|
安全 Java 编译器
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
|
19天前
|
安全 Java 开发者
谈谈Java线程同步原理
【5月更文挑战第24天】Java 线程同步的原理主要基于两个核心概念:互斥(Mutual Exclusion)和可见性(Visibility)。
13 3
|
28天前
|
Web App开发 开发框架 前端开发
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
39 0
|
28天前
|
消息中间件 安全 Ubuntu
【操作系统原理】—— 线程同步
【操作系统原理】—— 线程同步
21 1