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

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

一. 子线程能更新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出主线程消息队列详情:

kotlin 复制代码
    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()

四. 验证

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

kotlin 复制代码
    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 导致主线程假死

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui