基于消息调度优化启动速度方案实践
背景
在抖音的技术博客 https://juejin.cn/post/7080065015197204511#heading-10中,其介绍了通过修改消息队列顺序实现冷启动优化的方案,不过并未对其具体实现展开详细说明。 本文是对其技术方案的思考验证及实现。
详细代码见github: https://github.com/Knight-ZXW/AppOptimizeFramework
模拟劣化场景
我们首先模拟一个会影响冷启动的耗时消息场景, 在demo中,插入一个耗时消息到 startActivity对应的消息之前。
package com.knightboost.appoptimizeframeworkimport 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.WatchingStateclass 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 reporte.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 MessageMessage 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: 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 |
相关文章:

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

【C#】RemoveAt索引越界问题
系列文章 【C#】单号生成器(编号规则、固定字符、流水号、产生业务单号) 本文链接:https://blog.csdn.net/youcheng_ge/article/details/129129787 【C#】日期范围生成器(开始日期、结束日期) 本文链接:h…...
【华为OD机试2023】工位序列统计友好度最大值 100% C++ Java Python
【华为OD机试2023】工位序列统计友好度最大值 100% C++ Java Python 前言 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能最优),不能保证通过率。 Tips1:机试为ACM 模式 你的代码需要处理输入输出…...
Rust Atomics and Locks 阅读笔记 第二章 Atomics
原子操作(atomic operations)是多线程实现的基石,互斥锁(mutex)和条件变量(condition variable)都是通过原子操作来实现;std::sync::atomic包括了rust的内置原子操作类型(…...
Helm3入门
目录 Helm三大概念 Chart Repository Release Helm相关命令 helm 命令公共参数 helm search hub/repo - 查找可用的Charts helm repo - 仓库操作 helm install - 安装Chart helm status - 查看release状态 helm show values - 查看Chart的values.yaml内容 helm get…...
动态规划-线性动态规划-最长上升子序列模型
title: 线性动态规划 date: 2023-05-12 08:49:10 categories: Algorithm动态规划 tags:动态规划 编辑距离 题目描述 设 A A A 和 B B B 是两个字符串。我们要用最少的字符操作次数,将字符串 A A A 转换为字符串 B B B。这里所说的字符操作共有三种࿱…...

ResNet 论文理解含视频
ResNet 论文理解 论文理解 ResNet 网络的论文名字是《Deep Residual Learning for Image Recognition》,发表在2016年的 CVPR 上,获得了 最佳论文奖。ResNet 中的 Res 也是 Residual 的缩写,它的用意在于基于 残差 学习,让神经网…...

Java8之Stream操作
Java8之Stream操作 stream干啥用的?创建流中间操作终结操作好文推荐----接口优化思想 stream干啥用的? Stream 就是操作数据用的。使用起来很方便 创建流 → 中间操作 → 终结操作 Stream的操作可以分为两大类:中间操作、终结操作 中间操作可…...

二分查找基础篇-JAVA
文章目录 前言 大家好,我是最爱吃兽奶,这篇博客给大家介绍一下二分查找,我们先从最基本的开始讲解,再慢慢深入,把优化和变形也和大家说一下,那么,跟着我的步伐,我们一起去看看吧! 一、什么是二分查找? 二分查找(Binary Search)也称作折半查找 二分查找的效率很高,每查找一次…...

shell脚本5数组
文章目录 数组1 数组定义方法2 获取数组长度2.1 读取数组值2.2 数组切片2.3 数组替换2.4 数组删除2.5 追加数组元素 3 实验3.1 冒泡法3.2 直接选择法3.3 反排序法 数组 1 数组定义方法 数组名(value0 valuel value2 …) 数组名( [0]value [1]value [2]value …) 列表名“val…...

Kubernetes二进制部署 单节点
目录 1.环境准备 1.关闭防火墙和selinux 2.关闭swap 3.设置主机名 4.在master添加hosts 5.桥接的IPv4流量传递到iptables的链 6.时间同步 2.部署etcd集群 1.master节点部署 2.在node1与node2节点修改 3.在master1节点上进行启动 4.部署docker引擎 3.部署 Master 组…...

基于VC + MSSQL实现的县级医院医学影像PACS
一、概述: 基于VC MSSQL实现的一套三甲医院医学影像PACS源码,集成3D后处理功能,包括三维多平面重建、三维容积重建、三维表面重建、三维虚拟内窥镜、最大/小密度投影、心脏动脉钙化分析等功能。 二、医学影像PACS实现功能: 1、…...

Jmeter 压测 QPS
文章目录 1、准备工作1.1 Jmeter的基本概念1.2 Jmeter的作用1.3.Windows下Jmeter下载安装1.4 Jmeter的目录结构1.5 启动1.6 设置中文1.6.1 设置调整1.6.2 配置文件调整(一劳永逸) 2、Jmeter线程组基本操作2.1 线程组是什么2.2 线程组2.2.1 创建线程组2.2…...

如何在云上部署java项目
最近博主接了一波私活,由于上云的概念已经深入人心,客户要求博主也上云,本文将介绍上云的教程。 1.如何选择服务器 这里博主推荐阿里云服务器,阿里云云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务,助您降低 IT…...

IT行业项目管理软件,你知道多少?
IT行业项目管理软件,主要得看用来管理的是软件研发还是做IT运维。如果是做软件研发,那还得看项目经理是用什么思路,是传统的瀑布式方法还是敏捷的方法或者是混合的方法。 如果用来管理的是IT运维工作,那么很多通用型的项目管理软件…...
小爱同学接入chatGPT
大致流程 最近入手了一款小爱音响,想着把小爱音响接入 chatGPT, 在 github 上找了一个非常优秀的开源项目,整个过程还是比较简单的,一次就完成了。 其中最难的技术点是 如何获取与小爱的对话记录?如何让小爱播放文本?…...
java运算符
1.运算符和表达式 运算符: 就是对常量或者变量进行操作的符号。 比如: - * / 表达式: 用运算符把常量或者变量连接起来的,符合Java语法的式子就是表达式。 比如:a b 这个整体就是表达式。 而其…...
StrongSORT_文献翻译
StrongSORT 【摘要】 现有的MOT方法可以被分为tracking-by-detection和joint-detection-association。后者引起了更多的关注,但对于跟踪精度而言,前者仍是最优的解决方案。StrongSORT在DeepSORT的基础之上,更新了它的检测、嵌入和关联等多个…...

Python每日一练(20230512) 跳跃游戏 V\VI\VII
目录 1. 跳跃游戏 V 2. 跳跃游戏 VI 3. 跳跃游戏 VII 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 跳跃游戏 V 给你一个整数数组 arr 和一个整数 d 。每一步你可以从下标 i 跳到&a…...

k8s部署mysql并使用nfs持久化数据
k8s部署mysql并使用nfs持久化数据 一、配置nfs服务器1.1 修改配置文件1.2. 载入配置1.3. 检查服务配置 二、创建K8S资源文件2.1 mysql-deployment.yml2.2 mysql-svc.yml 一、配置nfs服务器 参考文章: pod使用示例https://cloud.tencent.com/developer/article/1914388nfs配置…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...

以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...

基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...

【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)
旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据!该数据集源自2025年4月发表于《地理学报》的论文成果…...