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

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

前言


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


进入正题,大家应该都听过这样一句话——“UI更新要在主线程,子线程更新UI会崩溃”。久而久之就感觉这是个真理,甚至被认为是“官方结论”。


但是如果问你,官方什么时候在哪里说过这句话,你会不会有点懵。而且就算是官方说的,也就不一定对的是吧,众所周知,Google官方文档一直都有点说的不清不楚,需要我们进行大量实践得出实际的结论。


就好比之前的Android11更新文档,我也是看了好久,通过一个个实践才写出了适配指南,然后就发现其中一个比较明显的BUGGoogle官方有说过这样一句:


下面是首先需要关注的行为变更 (无论您应用的 targetSdkVersion 是多少):  外部存储访问权限  - 应用无法再访问外部存储空间中其他应用的文件。


其实经过实践会发现,外部存储访问权限还是会和targetSdkVersion有关,具体可以看这篇Android11适配指南。


废话有点多了,今天还是通过实践案例,看看这个关于线程和UI更新的 “官方结论” 正确吗?


案例一,子线程更新button文字


1)onCreate方法中更新了按钮显示文字,修改Button的宽度为固定或者wrap_content,都不崩溃。


<Button
        android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />
        //或者
    <Button
        android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />        
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)
        thread {
            btn_ui.text="年轻人要讲武德"
        }
    }


2)onCreate方法中更新了按钮显示文字,加了延时。


Button的宽度为固定不崩溃。Button的宽度为wrap_content,崩溃报错——Only the original thread that created a view hierarchy can touch its views


<Button
        android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />
        //或者
    <Button
        android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="我是一个按钮"
        />   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)
        thread {
         Thread.sleep(3000)
            btn_ui.text="年轻人要讲武德"
        }
    }


案例一分析


有点懵的感觉,不慌,来看看崩溃信息。


崩溃是在按钮宽度为wrap_content,也就是根据内容设定宽度,然后3秒之后去更新按钮文字,发生了崩溃。相比之下,有两个崩溃影响点需要注意下:


  • 宽度wrap_content。如果设置为固定值,是不会崩溃的,见案例2,所以是不是跟布局改变的逻辑有关呢?
  • 延时3秒。如果不延时的话,即使是wrap_content也不会崩溃,见案例1,所以是不是跟某些类的加载进度有关呢?


带着这些疑问去源码中找找答案。先看看崩溃日志:


android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)
        at android.view.View.requestLayout(View.java:24884)


可以看到是ViewRootImplrequestLayout中检查线程的时候报错了,那我们就看看这个方法:


@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }


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


ViewRootImpl


Activity从创建到我们看到界面,其实是经历了两个过程:加载布局和绘制


  • 加载布局


加载布局其实就是我们常用的setContentView(int layoutResID)方法,这个方法主要做的就是新建了一个DecorView,然后根据activity设置的主题(theme)或者特征(Feature)加载不同的根布局文件,最后再加载layoutResID资源文件。为了方便大家理解,画了一张图:


0.png


这里的最后一步是调用了LayoutInflaterinflate()方法,这个方法只做了一件事,就是解析xml文件,然后根据节点生成了view对象。最后形成了一个完整的DOM结构,返回最顶层的根布局View。(DOM是一种文档对象模型,他的层次结构是除了顶级元素,所有元素都被包括到另外的元素节点中,有点像家谱树结构,很典型的就是html代码解析)


到这里,一个有完整view结构的DecorView就创建出来了,但是它还没有被绘制,也没有被显示到手机界面上。


  • 绘制


绘制的流程发生在handleResumeActivity中,熟悉app启动流程的朋友应该知道,handleResumeActivity方法是用来触发onResume方法的,这里也完成了DecorView的绘制。再来一张图:


1.png


  • 总结


由此我们可以得出一些结论:


1)setContentView用来新建DecorView并加载布局的资源文件。

2)onResume方法之后,会新建一个ViewRootImpl,作为DecorViewparentDecorView进行测量,布局和绘制等操作。

3)PhoneWindow作为Window的唯一子类,存储了DecorView变量,并对其进行管理,属于ActivityView交互的中间层。


分析崩溃


好了。再回来看看崩溃的原因:


void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }


可以看到是因为当前线程currentThread不是mThread的时候,就会崩溃,报的错误是 “只有创建视图层次结构的原始线程才能触摸它的视图” ,看到这里是不是猜到一些了,这个mThread难道就是“创建视图的原始线程”?


通过查找,其实这个mThread是在ViewRootImpl被创建的时候赋值的:


public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}


而通过上方分析Activity加载布局过程得知,ViewRootImpl实例化发生在onResume之后,用来绘制DecorViewwindow上。


所以我们就可以得知崩溃的真正原因,就是当前线程不是ViewRootImpl创建时候的线程就会崩溃。翻译的还是比较准确的,只有创建视图的原始线程才能修改这个视图,听起来也蛮有道理的,我创造了你才有权利改变你,有那味了。


然后再看看前面的案例:


  • 案例一,在onCreate中修改Button,这时候只是在修改DecorView,都没创建ViewRootImpl,也就没走到所以checkThread方法,当然不会崩溃了。ViewRootImpl的创建是在onResume之后。
  • 案例二,延时3秒之后,界面也绘制完成了,创建ViewRootImpl显然是在主线程完成的,所以mThread为主线程,而改变Button的线程为子线程,所以setText方法会触发requestLayout方法重新绘制,最终导致崩溃。


但是,Button的宽度设置为固定值咋又不崩溃了?难道就不会执行checkThread方法了?奇怪。


找找setText的源码可以发现,有一个方法是负责检查是否需要新的布局——checkForRelayout()


private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.
        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }
               //...
            }
            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }


可以看到,如果布局大小没有改变的话,我们是不会去执行requestLayout方法重新进行布局绘制的,只会调用autoSizeText方法计算文字大小,invalidate绘制文字本身,所以当我们宽高设置为固定值,setText()方法就不会执行到requestLayout()方法了,自然也就执行不到checkThread()方法了。


反思


解决了问题,还需要反思下,为什么需要checkThread检查线程呢?


  • 检查线程,其实就是检查更新UI操作的当前线程是不是当初创建UI的那个线程,这样就保证了线程安全,因为UI控件本身不是线程安全的,但是加锁又显得太重,会降低View加载效率,毕竟是跟交互相关的。所以就直接通过判断线程这一逻辑来形成一个单线程模型,保证View操作的线程安全。



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

相关实验场景

更多