theme: juejin
布景
在抖音的技术博客 /post/708006…中,其介绍了经过修正音讯行列次序完成冷发动优化的方案,不过并未对其详细完成展开详细阐明。 本文是对其技术方案的思考验证及完成。 详细代码见github: github.com/Knight-ZXW/…
模仿劣化场景
咱们首要模仿一个会影响冷发动的耗时音讯场景, 在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秒。
对应运转时的日志: 那么为了不让其他音讯,影响到 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) 而在Android 9.0以上版别,所有Activity生命周期事务变化被合并到一个音讯 EXECUTE_TRANSACTION 中, 那么高版别怎么判别一个音讯是为了 PauseActivity呢?经过源码剖析,能够发现这个Message的obj特点是一个ClientTransaction类型的方针,而该方针的mLifecycleStateRequest的getTargetState()函数返回值 标识了希望的生命周期状况 以pauseActivity为例,其实际的方针类型为 PauseActivityItem, 它的getTargetState 函数返回值为 ON_PAUSE =4。 因而,咱们能够先经过判别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音讯调度监控,每次音讯调度结束时,查看音讯行列中的音讯,判别是否存在方针音讯 其间pauseActivity的Message判别逻辑为, launchActivity音讯判别同理。 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到音讯行列中。
在提高mH相关音讯优先级后,最新的运转日志成果如下:
此刻的视频作用如下,看上去从画面上并没产生什么变化(不过生命周期函数提前了):
结合对应的日志可知,MainActivity现已履行到onResume状况,可是由于Choreographer音讯被堵塞,导致MainActivity的首帧一向无法得到渲染,从界面上看,还是展现的Splash的页面。
首帧优化
接下来持续剖析怎么处理上面的问题,进行首帧展现优化。首要需求知道首帧制作触发的逻辑,在Activity的launch音讯处理阶段,会调用addView函数向window增加View,终究会触发requestLayou、scheduleTraversal函数,在scheduleTraversal函数中,会先设置一个音讯屏障,并向Choreographer注册traversal Callback,终究鄙人一次vsync信号产生时,在traversalRunnable函数中进行真正的制作流程。 在resume Activity对应的音讯刚履行结束时,此刻的音讯行列如下所示,能够发现虽然设置了音讯屏障,可是音讯屏障并没有发送至行列首部,由于之前的慢音讯次序在音讯屏障之前,所以vsync对应的音讯依旧得不到优先履行。 因而,咱们能够经过遍历音讯行列,找到屏障音讯 并移动至队首,这样就能够保证后续对应的异步音讯优先得到履行。
详细完成代码如下: 首要咱们在MainActivity的onResume阶段设置新的监听状况,符号下来需求优化 帧制作的音讯 之后,在每次音讯调度结束时,尝试优化屏障音讯
经过判别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;
}
}
以下是优化后的日志: 能够发现,帧制作音讯被成功优化到其他音讯之前履行。而且该方案能够用于任何一个页面的首帧优化。 以下是优化后的视频作用:从视频中能够发现,现在MainActivity的画面会在onResume函数履行结束后当即展现。 这儿我设置了一个按钮,当点击按钮时,发现没有反应,这是由于首帧音讯优化后,进随这以后,其他音讯开端正常处理,等履行到慢音讯时,点击事件对应的音讯就得不到响应了。
终究,咱们经过两次音讯次序修正,完成了从页面发动到新页面首帧展现阶段的耗时优化,但这并不能处理在主线程的慢音讯问题,仅仅将其他非高优先级的音讯的处理拖延了 ,如果该音讯存在耗时问题,依旧会影响用户体会。 因而虽然音讯调度优化能够处理部分问题,可是想要彻底消除耗时音讯对使用体会的影响,音讯耗时的监控是必不可少的,经过记载慢音讯对应的Handler、音讯处理耗时、堆栈采样的方式 采集问题现场信息,再去优化对应的音讯函数耗时,然后从根本上处理详细问题。
总结
- 经过在关键流程,如发动页面、页面首帧制作阶段 优化相应音讯的次序 能够提高相应流程的速度,避免由于其他音讯堵塞了关键流程
- 音讯次序的修正只能优化部分问题,从全体上看,耗时问题并没有处理,仅仅将问题拖延了。
- 音讯耗时的监控及管理是处理根本问题的方式
以上demo 示例代码已上传到 github: github.com/Knight-ZXW/… 中, 未在出产环境验证,仅供参考。
另欢迎重视我的个人大众号:编程物语 ,后续将共享更多大厂功能监控&优化方案
功能优化专栏历史文章:
文章 | 地址 |
---|---|
抖音音讯调度优化发动速度方案实践 | /post/721766… |
扒一扒抖音是怎么做线程优化的 | /post/721244… |
监控Android Looper Message调度的另一种姿态 | /post/713974… |
Android 高版别采集体系CPU使用率的方式 | /post/713503… |
Android 平台下的 Method Trace 完成及使用 | /post/710713… |
Android 怎么处理使用SharedPreferences 形成的卡顿、ANR问题 | /post/705476… |
根据JVMTI 完成功能监控 | /post/694278… |
本文正在参加「金石方案」