当前位置: 首页 > news >正文

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

背景

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

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 reporte.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 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;}}

以下是优化后的日志:
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

相关文章:

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

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

【C#】RemoveAt索引越界问题

系列文章 【C#】单号生成器&#xff08;编号规则、固定字符、流水号、产生业务单号&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129129787 【C#】日期范围生成器&#xff08;开始日期、结束日期&#xff09; 本文链接&#xff1a;h…...

【华为OD机试2023】工位序列统计友好度最大值 100% C++ Java Python

【华为OD机试2023】工位序列统计友好度最大值 100% C++ Java Python 前言 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能最优),不能保证通过率。 Tips1:机试为ACM 模式 你的代码需要处理输入输出…...

Rust Atomics and Locks 阅读笔记 第二章 Atomics

原子操作&#xff08;atomic operations&#xff09;是多线程实现的基石&#xff0c;互斥锁&#xff08;mutex&#xff09;和条件变量&#xff08;condition variable&#xff09;都是通过原子操作来实现&#xff1b;std::sync::atomic包括了rust的内置原子操作类型&#xff08…...

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 是两个字符串。我们要用最少的字符操作次数&#xff0c;将字符串 A A A 转换为字符串 B B B。这里所说的字符操作共有三种&#xff1…...

ResNet 论文理解含视频

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

Java8之Stream操作

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

二分查找基础篇-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

一、概述&#xff1a; 基于VC MSSQL实现的一套三甲医院医学影像PACS源码&#xff0c;集成3D后处理功能&#xff0c;包括三维多平面重建、三维容积重建、三维表面重建、三维虚拟内窥镜、最大/小密度投影、心脏动脉钙化分析等功能。 二、医学影像PACS实现功能&#xff1a; 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 配置文件调整&#xff08;一劳永逸&#xff09; 2、Jmeter线程组基本操作2.1 线程组是什么2.2 线程组2.2.1 创建线程组2.2…...

如何在云上部署java项目

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

IT行业项目管理软件,你知道多少?

IT行业项目管理软件&#xff0c;主要得看用来管理的是软件研发还是做IT运维。如果是做软件研发&#xff0c;那还得看项目经理是用什么思路&#xff0c;是传统的瀑布式方法还是敏捷的方法或者是混合的方法。 如果用来管理的是IT运维工作&#xff0c;那么很多通用型的项目管理软件…...

小爱同学接入chatGPT

大致流程 最近入手了一款小爱音响&#xff0c;想着把小爱音响接入 chatGPT, 在 github 上找了一个非常优秀的开源项目&#xff0c;整个过程还是比较简单的&#xff0c;一次就完成了。 其中最难的技术点是 如何获取与小爱的对话记录&#xff1f;如何让小爱播放文本&#xff1f…...

java运算符

1.运算符和表达式 运算符&#xff1a; ​ 就是对常量或者变量进行操作的符号。 ​ 比如&#xff1a; - * / 表达式&#xff1a; ​ 用运算符把常量或者变量连接起来的&#xff0c;符合Java语法的式子就是表达式。 ​ 比如&#xff1a;a b 这个整体就是表达式。 ​ 而其…...

StrongSORT_文献翻译

StrongSORT 【摘要】 现有的MOT方法可以被分为tracking-by-detection和joint-detection-association。后者引起了更多的关注&#xff0c;但对于跟踪精度而言&#xff0c;前者仍是最优的解决方案。StrongSORT在DeepSORT的基础之上&#xff0c;更新了它的检测、嵌入和关联等多个…...

Python每日一练(20230512) 跳跃游戏 V\VI\VII

目录 1. 跳跃游戏 V 2. 跳跃游戏 VI 3. 跳跃游戏 VII &#x1f31f; 每日一练刷题专栏 &#x1f31f; 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配置…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

MVC 数据库

MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

跨链模式:多链互操作架构与性能扩展方案

跨链模式&#xff1a;多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈&#xff1a;模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展&#xff08;H2Cross架构&#xff09;&#xff1a; 适配层&#xf…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...

c# 局部函数 定义、功能与示例

C# 局部函数&#xff1a;定义、功能与示例 1. 定义与功能 局部函数&#xff08;Local Function&#xff09;是嵌套在另一个方法内部的私有方法&#xff0c;仅在包含它的方法内可见。 • 作用&#xff1a;封装仅用于当前方法的逻辑&#xff0c;避免污染类作用域&#xff0c;提升…...

GraphQL 实战篇:Apollo Client 配置与缓存

GraphQL 实战篇&#xff1a;Apollo Client 配置与缓存 上一篇&#xff1a;GraphQL 入门篇&#xff1a;基础查询语法 依旧和上一篇的笔记一样&#xff0c;主实操&#xff0c;没啥过多的细节讲解&#xff0c;代码具体在&#xff1a; https://github.com/GoldenaArcher/graphql…...

ArcGIS Pro+ArcGIS给你的地图加上北回归线!

今天来看ArcGIS Pro和ArcGIS中如何给制作的中国地图或者其他大范围地图加上北回归线。 我们将在ArcGIS Pro和ArcGIS中一同介绍。 1 ArcGIS Pro中设置北回归线 1、在ArcGIS Pro中初步设置好经纬格网等&#xff0c;设置经线、纬线都以10间隔显示。 2、需要插入背会归线&#xf…...

Python第七周作业

Python第七周作业 文章目录 Python第七周作业 1.使用open以只读模式打开文件data.txt&#xff0c;并逐行打印内容 2.使用pathlib模块获取当前脚本的绝对路径&#xff0c;并创建logs目录&#xff08;若不存在&#xff09; 3.递归遍历目录data&#xff0c;输出所有.csv文件的路径…...

Java中栈的多种实现类详解

Java中栈的多种实现类详解&#xff1a;Stack、LinkedList与ArrayDeque全方位对比 前言一、Stack类——Java最早的栈实现1.1 Stack类简介1.2 常用方法1.3 优缺点分析 二、LinkedList类——灵活的双端链表2.1 LinkedList类简介2.2 常用方法2.3 优缺点分析 三、ArrayDeque类——高…...