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、音讯处理耗时、堆栈采样的方式 采集问题现场信息,再去优化对应的音讯函数耗时,然后从根本上处理详细问题。

总结

  1. 经过在关键流程,如发动页面、页面首帧制作阶段 优化相应音讯的次序 能够提高相应流程的速度,避免由于其他音讯堵塞了关键流程
  2. 音讯次序的修正只能优化部分问题,从全体上看,耗时问题并没有处理,仅仅将问题拖延了。
  3. 音讯耗时的监控及管理是处理根本问题的方式

以上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…

本文正在参加「金石方案」