联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

子线程刷UI->Barrier屏障->主线程装死->应用GG?太难了

作者:访客发布时间:2023-12-22分类:程序开发学习浏览:77


导读:大家好,本篇文章给大家分享一个困扰我多周的问题,为了这个问题真的是天天殚精竭虑、夜不能寐,幸好最终定位到了问题原因,接下来的内容干货满满,相信能对你有所帮助。一.子线程能更新U...

大家好,本篇文章给大家分享一个困扰我多周的问题,为了这个问题真的是天天殚精竭虑、夜不能寐,幸好最终定位到了问题原因,接下来的内容干货满满,相信能对你有所帮助。

一. 子线程能更新UI?

这是一个老生常谈的问题,对于这个问题,在这里可以下一个结论:是的,子线程能更新UI。对于有些文章不加修饰的断言“只有主线程才能更新UI”这种错误说法,读者还需谨慎,擦亮眼睛。

这里就带着大家介绍三种能在子线程更新UI的场景:

1. onResume()生命周期前子线程更新UI

比如我们可以在onCreate()生命周期中调用子线程更新主线程UI,因为UI刷新的校验机制是发生在ViewRootImpl的诸如requestLayout() 等方法中,而ViewRootImpl是在onResume() 生命周期之后才被创建的

  • ActivityThread#handleResumeActivity()

  • WindowManagerImpl#addView()->WindowManagerGlobal#addView()

所以在onResume生命周期之前在子线程中更新主线程UI是没有问题的。

2. 创建ViewRootImpl的子线程能更新UI

我们先了解下ViewRootImpl中的线程校验机制:

mThread的赋值地方看一下:

也就是说UI刷新时的线程校验,只要能确保刷新UI的线程和ViewRootImpl创建的线程是同一个即可。换句话说,只要你在子线程中创建了ViewRootImpl,那么你就能在子线程刷新UI。

常见的例子就是比如在子线程中创建Dialog,就能在子线程刷新Dialog相关UI。

3. 开了硬件加速后Android O版本以上子线程能更新UI

这个就是本篇文章介绍的重点,这里以View#invalidate()作为入口进行分析:

  • View#invalidate()
  • View#invalidateInternal()
  • ViewGroup#invalidateChild()

    关键就是这个AttachInfo#mHardwareAccelerated这个属性表示是否开启了硬件加速,如果开启了硬件加速,就会执行方法ViewGroup#onDescendantInvalidated()方法,这个方法最终会执行到ViewRootImpl#onDescendantInvalidated()方法中:

    可以看到,线程检验函数checkThread已经被注释掉了,所以开启了硬件加速,子线程更新UI不会被检测到异常,即就实现了更新主线程UI的操作。

二. 子线程更新UI有啥弊端

讨论这个话题的前提我们以上面介绍的第三种情况进行分析。

在讨论子线程更新UI的操作前,我们先简单了解下Android消息屏障的相关知识点。

消息屏障

Android消息屏障也是一种特殊的Message,其中的标识就是target属性未null,一旦消息队列中存在了这样一类消息,那么该消息之后的同步消息都会得不到执行,只会执行异步消息:

MessageQueue#next():

这个消息有啥用呢,比如你当前有一个优先级比较高的Message需要执行,此时你就可以插入消息屏障,然后将要执行的Message设置为异步消息,保证该消息能较早被执行

在Android源码的场景中,也有如此应用场景,比如界面UI渲染的实现机制就是如此,保证渲染相关的Message已较高优先级被执行。

目前MessageQueue中也给我们提供了插入和移除消息屏障的方法:

  • MessageQueue#postSyncBarrier()
  • MessageQueue#removeSyncBarrier()

主线程、子线程并发更新UI

讲完了上面的消息屏障,现在我们来开始今天的重点内容:主线程和子线程并发刷新UI的弊端

UI的刷新入口为ViewRootImpl#scheduleTraversals()

先通过变量mTraversalScheduled判断是否已经执行了UI刷新,为false执行才走到下面的逻辑中:

  1. 插入消息屏障;
  2. 注册Vsync信号执行callback;
  3. 注册Vsync信号监听;

这就出现了一个非常严重的主线程、子线程并发更新UI的弊端,原因在于scheduleTraversals方法并不是线程安全的。

mTraversalScheduled这个变量是一个线程间不可见的变量,假设当前主线程执行了一次scheduleTraversals()方法更新UI并将mTraversalScheduled置为true,然后发送了消息屏障,此时子线程也开始执行scheduleTraversals()方法更新UI,但是对于子线程而言,根据JMM内存模型,它并不知道mTraversalScheduled这个已经被置为true了,在其内存缓存的副本中,mTraversalScheduled的值仍然为false

接下来子线程也会走到if条件分支中,发送消息屏障,此时就会发生一个问题:子线程调用mHandler.getLooper().getQueue().postSyncBarrier() 发送消息屏障的返回值mTraversalBarrier会覆盖掉主线程发送的消息屏障的返回值

请注意,此时消息队列中就存在了两个消息屏障,然后当UI更新完毕时,会根据mTraversalBarrier从消息队列中移除对应的消息屏障,由于子线程覆盖了主线程的mTraversalBarrier,导致最终移除消息屏障时,只会移除掉子线程发送的消息屏障,而主线程发送的消息屏障就会一直停留在队列中

而主线程消息队列平白无故多了一个消息屏障,那这可影响非常大了,要知道,我们平时执行的Message大多为同步消息,你这搞了个消息屏障,那就只能执行异步消息了,同步消息就只能一直停留在消息队列无法被执行,应用正常的消息调度就出现了大问题,各种事件无法被影响,各种功能逻辑无法被执行,应用表现就是黑屏或者触摸无响应GG。

三. 监听主线程消息队列屏障消息

为了能及时发现上面的问题根因,这里提供两个分析手段:

1. adb命令

借助adb shell dumpsys activity top > detail_activity.txt指令:

这个指令能够dump出当前应用消息队列的detail。查看Detail最下面就能发现主线程消息队列的详情,是否存在遗留的消息屏障消息。

2. dump

利用消息队列提供的dump方法定时dump出主线程消息队列详情:

    private Handler mMainHandler = new Handler(Looper.getMainLooper());

    private void startTrace() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (mMainHandler != null) {
                        mMainHandler.dump(new Printer() {
                            @Override
                            public void println(String s) {
                                Log.d("", "startTrace dump: " + s);
                            }
                        }, "<-->");
                    }
                    try {
                        Thread.sleep(5000);
                    } catch (Exception e) {
                        Log.d("", "startTrace exception: " + e.getMessage());
                    }
                }
            }
        }, "MessageDetails");
        thread.start();
    }

其中利用的源码机制为MessageQueue#dump()

四. 验证

基于以上分析,大家也可以简单写个代码验证下,通过反射主动发送一次消息屏障,而不主动去移除它,这时候最容易发现的就是触摸事件失灵了:

    fun sendBarrier() {
        val postSyncBarrier = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier", Long.javaClass)
        //关闭安全校验
        postSyncBarrier.isAccessible = true
        //发送消息屏障并获取对应token
        val token = postSyncBarrier.invoke(Looper.getMainLooper().queue, SystemClock.uptimeMillis())
    }

五. 参考文章

今日头条 ANR 优化实践系列 - Barrier 导致主线程假死


标签:屏障线程主线太难装死用户界面


程序开发学习排行
最近发表
网站分类
标签列表