线程与更新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


拜拜~

目录
相关文章
|
1月前
|
存储 开发框架 JavaScript
深入探讨Flutter中动态UI构建的原理、方法以及数据驱动视图的实现技巧
【6月更文挑战第11天】Flutter是高效的跨平台移动开发框架,以其热重载、高性能渲染和丰富组件库著称。本文探讨了Flutter中动态UI构建原理与数据驱动视图的实现。动态UI基于Widget树模型,状态变化触发UI更新。状态管理是关键,Flutter提供StatefulWidget、Provider、Redux等方式。使用ListView等可滚动组件和StreamBuilder等流式组件实现数据驱动视图的自动更新。响应式布局确保UI在不同设备上的适应性。Flutter为开发者构建动态、用户友好的界面提供了强大支持。
39 2
|
2天前
|
监控 Java 开发者
深入理解Java并发编程:线程池的原理与实践
【5月更文挑战第85天】 在现代Java应用开发中,高效地处理并发任务是提升性能和响应能力的关键。线程池作为一种管理线程的机制,其合理使用能够显著减少资源消耗并优化系统吞吐量。本文将详细探讨线程池的核心原理,包括其内部工作机制、优势以及如何在Java中正确实现和使用线程池。通过理论分析和实例演示,我们将揭示线程池对提升Java应用性能的重要性,并给出实践中的最佳策略。
|
16天前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
26 1
|
16天前
|
缓存 Java
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
18 0
|
16天前
|
Java 开发者
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
19 0
|
16天前
|
存储 算法 Java
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
15 0
|
16天前
|
Java 开发者
Java面试题:解释Java内存模型中的内存可见性,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的CountDownLatch和CyclicBarrier的区别
Java面试题:解释Java内存模型中的内存可见性,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的CountDownLatch和CyclicBarrier的区别
14 0
|
16天前
|
监控 Java 开发者
Java面试题:解释Java内存模型中的内存顺序规则,Java中的线程组(ThreadGroup)的工作原理,Java中的FutureTask的工作原理
Java面试题:解释Java内存模型中的内存顺序规则,Java中的线程组(ThreadGroup)的工作原理,Java中的FutureTask的工作原理
14 0
|
16天前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
14 0
|
16天前
|
存储 算法 Java
Java面试题:详细描述Java堆内存的垃圾回收过程,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的FutureTask的工作原理
Java面试题:详细描述Java堆内存的垃圾回收过程,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的FutureTask的工作原理
19 0

相关实验场景

更多