您现在的位置是:首页 >技术交流 >基于消息调度优化启动速度方案实践网站首页技术交流

基于消息调度优化启动速度方案实践

卓修武 2023-05-17 20:00:02
简介基于消息调度优化启动速度方案实践

背景

在抖音的技术博客 https://juejin.cn/post/7080065015197204511#heading-10中,其介绍了通过修改消息队列顺序实现冷启动优化的方案,不过并未对其具体实现展开详细说明。 本文是对其技术方案的思考验证及实现。
详细代码见github: https://github.com/Knight-ZXW/AppOptimizeFramework

模拟劣化场景

我们首先模拟一个会影响冷启动的耗时消息场景, 在demo中,插入一个耗时消息到 startActivity对应的消息之前。

package com.knightboost.appoptimizeframework

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.knightboost.optimize.looperopt.ColdLaunchBoost
import com.knightboost.optimize.looperopt.ColdLaunchBoost.WatchingState

class SplashActivity : AppCompatActivity() {
    val handler = Handler(Looper.getMainLooper())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)
        Log.d("MainLooperBoost", "SplashActivity onCreate")

    }

    override fun onStart() {
        super.onStart()
        Log.d("MainLooperBoost", "SplashActivity onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d("MainLooperBoost", "SplashActivity onResume")
        Handler().postDelayed({
            //发送3秒的耗时消息到队列中
            //这里为了方便模拟,直接在主线程发送耗时任务,模拟耗时消息在 启动Activity消息之前的场景
            handler.post({
                Thread.sleep(3000)
                Log.e("MainLooperBoost", "任务处理3000ms")
            })
            val intent = Intent(this, MainActivity::class.java)
            Log.e("MainLooperBoost", "begin start to MainActivity")
            startActivity(intent)
            //标记接下来需要优化 启动Activity的相关消息
            ColdLaunchBoost.getInstance().curWatchingState = WatchingState.STATE_WATCHING_START_MAIN_ACTIVITY
        },1000)

    }

    override fun onPause() {
        super.onPause()
        Log.d("MainLooperBoost", "SplashActivity onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d("MainLooperBoost", "SplashActivity onStop")
    }

}

这里的startActivity函数在实现底层会生成2个消息,其目的分别对应“Pause当前的Activity",以及 “resume MainActivity”。在函数刚执行结束时,此时的消息队列大概是这样的(为了方便理解,忽略延迟1秒对应的消息以及其它消息)。

以下视频为代码运行效果,可以发现在闪屏页展示一秒后,并未立即进行页面跳转操作,其被阻塞了3秒。

new_case2.gif
对应运行时的日志:
image.png
那么为了不让其他消息,影响到 startActivity的操作,就需要提升 startActivity操作相应消息的顺序。

优化方案

消息调度监控

提高目标消息的顺序,首先需要一个检查消息队列内消息的时机, 我们可以在每次消息调度结束时进行,如果发现当前队列中 有相应的需要提升优先级的消息,则将其移动至消息队首。

消息的调度监控有两种方式,在低版本系统可以基于设置Printer替换实现,不过这种方式只能获取到消息的开始和结束时间,无法获取到Message对象,并且基于Printer的方案会有额外的字符串拼接的性能开销。 第二种是通过调用Looper的 setObserver 函数设置消息调度观察者,相比Printer的方案,它可以拿到调度的Message对象,并且没有额外的性能开销,缺点是 有hiddenApi的限制,并且它具体实现方案可以参看之前写的文章 监控Android Looper Message调度的另一种姿势

消息类型判断

修改消息的顺序,需要先从队列中获取到目标消息,上个小节已经说过,startActivity 会有2个消息调度,分别是:“pause 当前Activity”,以及“resum新的Activity” 。 在Android 9.0以下版本,可以通过判断 message的target(Handler) 以及 what值区分,它们分别对应 ActivityThread中 mH Handler 的 LAUNCH_ACTIVITY (100), PAUSE_ACTIVITY(107)
image.png
而在Android 9.0以上版本,所有Activity生命周期事务变化被合并到一个消息 EXECUTE_TRANSACTION 中,
image.png
那么高版本如何判断一个消息是为了 PauseActivity呢?通过源码分析,可以发现这个Message的obj属性是一个ClientTransaction类型的对象,而该对象的mLifecycleStateRequest的getTargetState()函数返回值 标识了期望的生命周期状态
image.png
以pauseActivity为例,其实际的对象类型为 PauseActivityItem, 它的getTargetState 函数返回值为 ON_PAUSE =4。
image.png
image.png
因此,我们可以先通过判断Message what值为 EXECUTE_TRANSACTION(159), 再通过反射最终获取到 mLifecycleStateRequest 对象getTargetState函数的返回值,来判断消息是pauseActivity,还是 resumeActivity。

以下为整个流程具体的实现代码:
首先在startActivity 后,主动标记后续需要优化 启动页面的消息

class SplashActivity : AppCompatActivity() {
//...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)
        Log.d("MainLooperBoost", "SplashActivity onCreate")
        Handler().postDelayed({
            //发送3秒的耗时消息到队列中
            //这里为了方便模拟,直接在主线程发送耗时任务,模拟耗时消息在 启动Activity消息之前的场景
            handler.post({
                Thread.sleep(3000)
                Log.e("MainLooperBoost", "任务处理3000ms")
            })
            val intent = Intent(this, MainActivity::class.java)
            Log.e("MainLooperBoost", "begin start to MainActivity")
            startActivity(intent)
            //标记接下来需要优化 启动Activity的相关消息
            ColdLaunchBoost.getInstance().curWatchingState = WatchingState.STATE_WATCHING_START_MAIN_ACTIVITY

        },1000)
    }
//...
}

基于Looper消息调度监控,每次消息调度结束时,检查消息队列中的消息,判断是否存在目标消息
image.png
其中pauseActivity的Message判断逻辑为, launchActivity消息判断同理。
image.png
launchActivity消息判断同理,只是判断targetState的值不同。

修改消息顺序、优化页面跳转

修改普通消息的顺序比较简单。当遍历消息队列找到目标message后,可以修改前一个消息的next值,使其指向下一个消息,这样就从消息队列中移除了消息,之后再复制一份目标消息,重新发送到队列首部。

public boolean upgradeMessagePriority(Handler handler, MessageQueue messageQueue,
                                      TargetMessageChecker targetMessageChecker) {
    synchronized (messageQueue) {
        try {
            Message message = (Message) filed_mMessages.get(messageQueue);
            Message preMessage = null;
            while (message != null) {
                if (targetMessageChecker.isTargetMessage(message)) {
                    // 拷贝消息
                    Message copy = Message.obtain(message);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
                        if (message.isAsynchronous()) {
                            copy.setAsynchronous(true);
                        }
                    }
                    if (preMessage != null) { //如果已经在队列首部了,则不需要优化
                        //当前消息的下一个消息
                        Message next = nextMessage(message);
                        setMessageNext(preMessage, next);
                        handler.sendMessageAtFrontOfQueue(copy);
                        return true;
                    }
                    return false;
                }
                preMessage = message;
                message = nextMessage(message);
            }
        } catch (Exception e) {
            //todo report
            e.printStackTrace();
        }
    }
    return false;
}

这里需要复制原消息是因为:在消息首次入队时会被标记为已使用,一个 isInUse 的消息无法被重新enqueue到消息队列中。

image.png

在提升mH相关消息优先级后,最新的运行日志结果如下:
image.png

此时的视频效果如下,看上去从画面上并没发生什么变化(不过生命周期函数提前了):

new_case2.gif

结合对应的日志可知,MainActivity已经执行到onResume状态,但是由于Choreographer消息被阻塞,导致MainActivity的首帧一直无法得到渲染,从界面上看,还是展示的Splash的页面。

首帧优化

接下来继续分析如何解决上面的问题,进行首帧展示优化。首先需要知道首帧绘制触发的逻辑,在Activity的launch消息处理阶段,会调用addView函数向window添加View,最终会触发requestLayou、scheduleTraversal函数,在scheduleTraversal函数中,会先设置一个消息屏障,并向Choreographer注册traversal Callback,最终在下一次vsync信号发生时,在traversalRunnable函数中进行真正的绘制流程。
image.png
在resume Activity对应的消息刚执行结束时,此时的消息队列如下所示,可以发现虽然设置了消息屏障,但是消息屏障并没有发送至队列首部,因为之前的慢消息顺序在消息屏障之前,所以vsync对应的消息依旧得不到优先执行。
image.png
因此,我们可以通过遍历消息队列,找到屏障消息 并移动至队首,这样就可以保证后续对应的异步消息优先得到执行。

具体实现代码如下:
首先我们在MainActivity的onResume阶段设置新的监听状态,标记下来需要优化 帧绘制的消息
image.png
之后,在每次消息调度结束时,尝试优化屏障消息
image.png

通过判断message的target是否为null 来找到第一个 barrier message, 之后直接反射调用 removeSyncBarrier 移除屏障消息(当然也可以通过手动操作前序消息的next指向来实现), 最后复制这个消息屏障,将其发送至队首。

实现代码如下:

/**
 * 移动消息屏障至队首
 *
 * @param messageQueue
 * @param handler
 * @return
 */
public boolean upgradeBarrierMessagePriority(MessageQueue messageQueue, Handler handler) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
        return false;
    }
    synchronized (messageQueue) {
        try {
            //反射获取 head Message
            Message message = (Message) filed_mMessages.get(messageQueue);
            if (message != null && message.getTarget() == null) {
                return false;
            }
            while (message != null) {
                if (message.getTarget() == null) { // target 为null 说明该消息为 屏障消息
                    Message cloneBarrier = Message.obtain(message);
                    removeSyncBarrier(messageQueue, message.arg1); //message.arg1 是屏障消息的 token, 后续的async消息会根据这个值进行屏障消息的移除
                    handler.sendMessageAtFrontOfQueue(cloneBarrier);
                    cloneBarrier.setTarget(null);//屏障消息的target为null,因此这里还原下
                    return true;
                }
                message = nextMessage(message);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return false;
}

removeSyncBarrier 直接反射调用了相关函数

private boolean removeSyncBarrier(MessageQueue messageQueue, int token) {
    try {
        Method removeSyncBarrier = class_MessageQueue.getDeclaredMethod("removeSyncBarrier", int.class);
        removeSyncBarrier.setAccessible(true);
        removeSyncBarrier.invoke(messageQueue, token);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }

}

以下是优化后的日志:
image.png
可以发现,帧绘制消息被成功优化到其他消息之前执行。并且该方案可以用于任何一个页面的首帧优化。
以下是优化后的视频效果:

在这里插入图片描述

从视频中可以发现,现在MainActivity的画面会在onResume函数执行结束后立即展示。 这里我设置了一个按钮,当点击按钮时,发现没有反应,这是因为首帧消息优化后,进随其后,其他消息开始正常处理,等执行到慢消息时,点击事件对应的消息就得不到响应了。

最终,我们通过两次消息顺序修改,完成了从页面启动到新页面首帧展示阶段的耗时优化,但这并不能解决在主线程的慢消息问题,只是将其他非高优先级的消息的处理延后了 ,如果该消息存在耗时问题,依旧会影响用户体验。
因此虽然消息调度优化可以解决局部问题,但是想要完全消除耗时消息对应用体验的影响,消息耗时的监控是必不可少的,通过记录慢消息对应的Handler、消息处理耗时、堆栈采样的方式 采集问题现场信息,再去优化对应的消息函数耗时,从而从根本上解决具体问题。

总结

  1. 通过在关键流程,如启动页面、页面首帧绘制阶段 优化相应消息的顺序 可以提高相应流程的速度,避免因为其他消息阻塞了关键流程
  2. 消息顺序的修改只能优化局部问题,从整体上看,耗时问题并没有解决,只是将问题延后了。
  3. 消息耗时的监控及治理是解决根本问题的方式

以上demo 示例代码已上传到 github: https://github.com/Knight-ZXW/AppOptimizeFramework 中, 未在生产环境验证,仅供参考。

另欢迎关注我的个人公众号:编程物语 ,后续将分享更多大厂性能监控&优化方案

性能优化专栏历史文章:

文章地址
抖音消息调度优化启动速度方案实践https://juejin.cn/post/7217664665090080826
扒一扒抖音是如何做线程优化的https://juejin.cn/post/7212446354920407096
监控Android Looper Message调度的另一种姿势https://juejin.cn/post/7139741012456374279
Android 高版本采集系统CPU使用率的方式https://juejin.cn/post/7135034198158475300
Android 平台下的 Method Trace 实现及应用https://juejin.cn/post/7107137302043820039
Android 如何解决使用SharedPreferences 造成的卡顿、ANR问题https://juejin.cn/post/7054766647026352158
基于JVMTI 实现性能监控https://juejin.cn/post/6942782366993612813
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。