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


拜拜~

目录
相关文章
|
4天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
15 0
|
20天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
1月前
|
XML 前端开发 JavaScript
深入介绍 UI5 框架里 Smart Field 控件的工作原理
深入介绍 UI5 框架里 Smart Field 控件的工作原理
18 0
|
1月前
|
算法 安全 调度
多线程如何工作,工作原理是什么
多线程如何工作,工作原理是什么
|
27天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解04-阻塞队列之PriorityBlockingQueue原理及扩容机制详解
1. **继承实现图关系**: - `PriorityBlockingQueue`实现了`BlockingQueue`接口,提供了线程安全的队列操作。 - 内部基于优先级堆(小顶堆或大顶堆)的数据结构实现,可以保证元素按照优先级顺序出队。 2. **底层数据存储结构**: - 默认容量是11,存储数据的数组会在需要时动态扩容。 - 数组长度总是2的幂,以满足堆的性质。 3. **构造器**: - 无参构造器创建一个默认容量的队列,元素需要实现`Comparable`接口。 - 指定容量构造器允许设置初始容量,但不指定排序规则。 - 可指定容量和比较
42 2
|
30天前
|
设计模式 安全 C++
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
71 2
|
1月前
|
Java 程序员 API
【深入探究 Qt 线程】一文详细解析Qt线程的内部原理与实现策略
【深入探究 Qt 线程】一文详细解析Qt线程的内部原理与实现策略
77 0
|
1月前
|
Java API 开发者
springboot 多线程的使用原理与实战
在Spring Boot中实现多线程,主要依赖于Spring框架的@Async注解以及底层Java的并发框架。这里将深入剖析Spring Boot多线程的原理,包括@Async注解的工作方式、任务执行器的角色以及如何通过配置来调整线程行为。
41 5
|
1月前
|
负载均衡 Java 数据处理
【C++ 并发 线程池】轻松掌握C++线程池:从底层原理到高级应用(三)
【C++ 并发 线程池】轻松掌握C++线程池:从底层原理到高级应用
55 2
|
1月前
|
存储 监控 Java
【C++ 并发 线程池】轻松掌握C++线程池:从底层原理到高级应用(二)
【C++ 并发 线程池】轻松掌握C++线程池:从底层原理到高级应用
43 1

热门文章

最新文章

相关实验场景

更多