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

news2024/10/7 14:24:18

背景

在抖音的技术博客 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/412640.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PMO在企业项目管理中起到的重要性

在现代企业中&#xff0c;项目管理助力企业在激烈的市场竞争中获得成功&#xff0c;而这需要一个专业化的项目管理办公室&#xff08;PMO&#xff09;来确保项目的顺利实施。在企业项目管理中&#xff0c;PMO扮演着至关重要的角色&#xff0c;下文我们将对其的重要作用进行探讨…

docker+jenkins+maven+git构建聚合项目,实现自动化部署,走了800个坑

流程 主要的逻辑就是Docker上安装jenkins&#xff0c;然后拉取git上的代码&#xff0c;把git上的代码用Maven打包成jar包&#xff0c;然后在docker运行 这个流程上的难点 一个是聚合项目有可能Maven install的时候失败。 解决办法&#xff1a;在基础模块的pom文件上添加 <…

【配电网故障重构SOP】基于二阶锥松弛的加光伏风机储能进行的配电网故障处理和重构【考虑最优潮流】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

uniapp即时通讯sdk哪个好?

UniApp 是什么? UniApp 是一种跨平台移动应用开发框架&#xff0c;可以使用 Vue.js 语法开发支持 Android、iOS 和桌面浏览器的应用。 UniApp 即时通讯功能如何实现? 对于实现即时通讯功能&#xff0c;您可以使用以下2种方法: 1、使用 websocket 实现即时通讯。Websocket 是一…

x210官方uboot配置编译

1、在linux源生目录下配置编译 (1)X210移植过的uboot在开发板光盘的BSP中。 (2)BSP就是board support package(板级支持包&#xff0c;一般由开发板供应商提供)&#xff0c;里面的内容就是这个开发板的所有相关的源代码、文档、教程等。 (3)将整个BSP打包文件弄到linux的源生目…

Linux的vi编辑器常见命令总结

注&#xff1a;本文中的给定模式或者模式等词中模式所指是指的一个词语并无其他意思。例如给定模式查找定位指的是查找到给定词的定位。 三种方式的关系图 命令方式和插入方式之间可以相互转换&#xff0c;命令方式和ex转义方式也可以相互转换。但是插入方式和ex转义方式之间不…

Python 小型项目大全 61~65

六十一、ROT13 密码 原文&#xff1a;http://inventwithpython.com/bigbookpython/project61.html ROT13 密码是最简单的加密算法之一&#xff0c;代表“旋转 13 个空格”密码将字母A到Z表示为数字 0 到 25&#xff0c;加密后的字母距离明文字母 13 个空格&#xff1a; A变成N&…

MyBatis 03 -MyBatis动态SQL与分页插件

动态SQL与分页插件 动态SQL与分页插件动态SQL与分页插件1 动态SQL1.1 < sql >1.2 < if >1.3 < where >1.4 < set >1.5 < choose >1.6 < trim >1.7 < foreach >2 mybatis缓存2.1 一级缓存2.2 二级缓存3 分页插件3.1 概念3.2 访问与下…

没有什么壁纸比这里的更绝美精致了吧,我一天换一张

嗨&#xff0c;这里是清安&#xff0c;本章来学习学习获取精美壁纸。视频教程&#xff1a;https://b23.tv/iR7bOFF源码&#xff1a;https://gitee.com/qinganan_admin/reptile-case/tree/master/%E5%A3%81%E7%BA%B8本视频还会有第二期&#xff0c;代码也会有第二份&#xff0c;…

类ChatGPT项目的部署与微调(上):从LLaMA到Alpaca、Vicuna、BELLE

前言 近期&#xff0c;除了研究ChatGPT背后的各种技术细节 不断看论文(至少100篇&#xff0c;100篇目录见此&#xff1a;ChatGPT相关技术必读论文100篇)&#xff0c;还开始研究一系列开源模型(包括各自对应的模型架构、训练方法、训练数据、本地私有化部署、硬件配置要求、微…

Python | 蓝桥杯进阶第三卷——动态规划

欢迎交流学习~~ 专栏&#xff1a; 蓝桥杯Python组刷题日寄 蓝桥杯进阶系列&#xff1a; &#x1f3c6; Python | 蓝桥杯进阶第一卷——字符串 &#x1f50e; Python | 蓝桥杯进阶第二卷——贪心 &#x1f49d; Python | 蓝桥杯进阶第三卷——动态规划 ✈️ Python | 蓝桥杯进阶…

精通Python(基础篇)——流程控制语句

流程控制语句 文章目录流程控制语句1️⃣简介2️⃣条件判断⚜️关系运算符⚜️逻辑运算符⚜️if语句⚜️if...else 语句⚜️if...elif...else语句⚜️match...case3️⃣循环结构⚜️while⚜️while...else语句⚜️for⚜️for...else语句4️⃣退出循环⚜️continue⚜️break1️⃣…

(排序6)快速排序(小区间优化,非递归实现)

TIPS 快速排序本质上是一个分治递归的一个排序。快速排序的时间复杂度是NlogN&#xff0c;这是在理想的情况之下&#xff0c;但是它最坏可以到达N^2。决定快速排序的效率是在单趟排序之后这个key最终落在的位置&#xff0c;越落在中间就越接近二分&#xff0c;越接近2分就越接…

Android创建项目

目录 创建Android项目 配置项目结构 创建安卓模拟器 模拟器运行 HelloWorld 应用 真机运行 HelloWorld 应用 创建Android项目 打开 Android studio 工具&#xff0c;选择Project&#xff0c;选择 New Project 由于现在是教程博客&#xff0c;所以我们随便选择 一个 空 Ac…

JS内置对象2

数组对象&#xff1a; &#xff08;1&#xff09;检测是否为数组&#xff1a; …

【数据结构】第十站:堆与堆排序

目录 一、二叉树的顺序结构 二、堆的概念及结构 三、堆的实现 1.堆的创建 2.堆的各接口实现 四、堆排序 1.堆排序的基本思想 2.堆排序的实现 3.堆排序时间复杂度 四、TOP-K问题 五、堆的完整代码 一、二叉树的顺序结构 二叉树有顺序结构和链式结构&#xff0c;分别使…

Android之adb安装busybox使用wget、telnet等服务

一、adb里面安装busybox BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件。BusyBox 包含了一些简单的工具&#xff0c;例如ls、cat和echo等等&#xff0c;还包含了一些更大、更复杂的工具&#xff0c;例grep、find、mount以及telnet。 1、下载busybox busybox—bi…

有哪些通过PMP认证考试的心得值得分享?

回顾这100多天来艰辛的备考经历&#xff0c;感慨颇多 一&#xff0c;对于pmp的认知 百度百科&#xff1a;PMP&#xff08;Project Management Professional&#xff09;指项目管理专业人士&#xff08;人事&#xff09;资格认证。美国项目管理协会&#xff08;PMI&#xff09;举…

使用 Floyd Warshall 检测负循环

我们得到了一个有向图。我们需要计算图形是否有负循环。负循环是循环的总和为负的循环。 在图形的各种应用中都可以找到负权重。例如,如果我们沿着这条路走,我们可能会得到一些好处,而不是为一条路付出代价。 例子:

PVE相关的各种一键脚本(一键安装PVE)(一键开设KVM虚拟化的NAT服务器-自带内外网端口转发)

PVE 原始仓库&#xff1a;https://github.com/spiritLHLS/pve 前言 建议debian在使用前尽量使用最新的系统 非debian11可使用 debian一键升级 来升级系统 当然不使用最新的debian系统也没问题&#xff0c;只不过得不到官方支持 请确保使用前机器可以重装系统&#xff0c;…