Android 性能监控之——消息调度启动优化方案实践_性能优化demo

    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秒对应的消息以及其它消息)。


![](https://img-blog.csdnimg.cn/img_convert/f4e32b63c213daadd9d4afbdc52adf0c.png)


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


![](https://img-blog.csdnimg.cn/img_convert/a56f952031ea1e2e93dab96734158a87.png)


对应运行时的日志:


![](https://img-blog.csdnimg.cn/img_convert/900b72dc7b34e36a533c15ea461cbc38.png)


那么为了不让其他消息,影响到 startActivity的操作,就需要提升 startActivity操作相应消息的顺序。


### 优化方案


#### 消息调度监控


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


![](https://img-blog.csdnimg.cn/img_convert/959f23b189bb36ebfbeb0fc71bace4c4.png)


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


#### 消息类型判断


修改消息的顺序,需要先从队列中获取到目标消息,上个小节已经说过,startActivity 会有2个消息调度,分别是:“pause 当前Activity”,以及“resum新的Activity” 。 在Android 9.0以下版本,可以通过判断 message的target(Handler) 以及 what值区分,它们分别对应 ActivityThread中 mH Handler 的 LAUNCH\_ACTIVITY (100), PAUSE\_ACTIVITY(107)


![](https://img-blog.csdnimg.cn/img_convert/dad0fc144d9e9678abd4856f08165bc6.png)


而在Android 9.0以上版本,所有Activity生命周期事务变化被合并到一个消息 EXECUTE\_TRANSACTION 中


![](https://img-blog.csdnimg.cn/img_convert/6de2e601b185969ac30176a42ca3b620.png)


那么高版本如何判断一个消息是为了 PauseActivity呢?通过源码分析,可以发现这个Message的obj属性是一个ClientTransaction类型的对象,而该对象的mLifecycleStateRequest的getTargetState()函数返回值 标识了期望的生命周期状态


![](https://img-blog.csdnimg.cn/img_convert/5476aecb73b8e6e76edd1743ae6fac51.png)


以pauseActivity为例,其实际的对象类型为 PauseActivityItem, 它的getTargetState 函数返回值为 ON\_PAUSE =4。


![](https://img-blog.csdnimg.cn/img_convert/b5f9522f8a78bbbe8492a1d13373f5a4.png)


![](https://img-blog.csdnimg.cn/img_convert/41c60bbfa036d730ca34c6d227c668de.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消息调度监控,每次消息调度结束时,检查消息队列中的消息,判断是否存在目标消息


![](https://img-blog.csdnimg.cn/img_convert/ee2a8e1f512bf9d3ad6065a85c1a2673.png)


其中pauseActivity的Message判断逻辑为, launchActivity消息判断同理。


![](https://img-blog.csdnimg.cn/img_convert/f08e46200bfcbed8c10bf3afbea74377.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到消息队列中。


![](https://img-blog.csdnimg.cn/img_convert/f280b9af0247cf1615f81ddc8d17eb07.png)


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


![](https://img-blog.csdnimg.cn/img_convert/d2eae7038a0377787457b7ee555f72f7.png)


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


![](https://img-blog.csdnimg.cn/img_convert/9a47168cd45d482625ddfafb791086c4.png)


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


#### 首帧优化


接下来继续分析如何解决上面的问题,进行首帧展示优化。首先需要知道首帧绘制触发的逻辑,在Activity的launch消息处理阶段,会调用addView函数向window添加View,最终会触发requestLayou、scheduleTraversal函数,在scheduleTraversal函数中,会先设置一个消息屏障,并向Choreographer注册traversal Callback,最终在下一次vsync信号发生时,在traversalRunnable函数中进行真正的绘制流程。


![](https://img-blog.csdnimg.cn/img_convert/ca1d82d71fe2d8f25db4643c5f2c6097.png)


在resume Activity对应的消息刚执行结束时,此时的消息队列如下所示,可以发现虽然设置了消息屏障,但是消息屏障并没有发送至队列首部,因为之前的慢消息顺序在消息屏障之前,所以vsync对应的消息依旧得不到优先执行。


![](https://img-blog.csdnimg.cn/img_convert/4f0d2d6ef0a67cb3239b9df5ea7f2adc.png)


因此,我们可以通过遍历消息队列,找到屏障消息 并移动至队首,这样就可以保证后续对应的异步消息优先得到执行。


具体实现代码如下: 首先我们在MainActivity的onResume阶段设置新的监听状态,标记下来需要优化 帧绘制的消息


![](https://img-blog.csdnimg.cn/img_convert/570a74043f9d77c1d04c5518446a3201.png)


之后,在每次消息调度结束时,尝试优化屏障消息


# 文末

**不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊**

小编将自己6年以来的**面试经验和学习笔记**都整理成了一个**937页的PDF,**以及我学习进阶过程中看过的一些优质视频教程。

![](https://img-blog.csdnimg.cn/img_convert/7fed879b8d66caf074dfe2e8f98920d1.webp?x-oss-process=image/format,png)


其实看到身边很多朋友抱怨自己的工资很低,包括笔者也是一样的,其原因是在面试过程中没有给面试官一个很好的答案。所以笔者会持续更新面试过程中遇到的问题,也希望大家和笔者一起进步,一起学习。


加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
验和学习笔记**都整理成了一个**937页的PDF,**以及我学习进阶过程中看过的一些优质视频教程。

[外链图片转存中...(img-8xT2hYQg-1725658419125)]


其实看到身边很多朋友抱怨自己的工资很低,包括笔者也是一样的,其原因是在面试过程中没有给面试官一个很好的答案。所以笔者会持续更新面试过程中遇到的问题,也希望大家和笔者一起进步,一起学习。


加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值