Android 性能第一篇,随着项目功能的迭(zeng)代(jia),启动速度也会受到影响,性能优化之路的第一步,也就是启动优化,个人认为非常重要,它可以直接影响 APP 的留存率,没有人希望自己应用半天打不开。这篇文章就带你解决🌞用户初体验-启动优化🌞。
注意:如果出现图片不显示问题请移步 图片不显示问题
本文主要记载
- 1 启动
- 2 检测启动消耗
(敲黑板)- 2.1 启动时间测量方式
- 2.1.1 adb 命令启动时间测量方式
- 2.1.2 手动打点
- 2.2 启动耗时检测方式
- 2.2.1 TraceView
- 2.2.2 systrace
- 2.2.3 systrace + 函数插桩 AOP
- 2.1 启动时间测量方式
- 3 启动优化
(干货)- 3.1 闪屏页展示
- 3.2 异步启动实践
- 3.3 数据重排
- 3.4 类的加载
- 3.5 延迟加载方案 IdleHandler
- 3.6 其他优化思路
1 启动
Google官方文档 《Launch-Time Performance》 对应用启动优化的概述
应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!我们只要知道我们处理的是冷启动的情况。
2 检测启动消耗(敲黑板)
“工欲善其事必先利其器”,我们需要先找到一款适合做启动优化分析的工具。
2.1 启动时间测量方式
2.1.1 adb 命令启动时间测量方式
1 | adb shell am start -W packagename/packagename.Activity |

划掉的红色部分都是包名
| 字段 | 功能 |
|---|---|
| ThisTime | 最后一个 Activity 启动耗时 |
| TotalTIme | 所有 Activity 启动耗时,比如添加启动页 |
| WaitTime | AMS 启动 Activity 的总耗时 |
| ThisTime ≤ TotalToime < WaitTime |
线下方便使用、可以测量竞品、不能带到线上
2.1.2 手动打点
1 | public class LaunchTimer { |
在应用启动中,我们所能接收到最早的回调是 Application 中 attachBaseContext(Context context) ,所以只能将 startRecord() 写到这个回调中
应用启动的结束时间,是用户可操作的时间,先了解两个误区
onAttachedToWindow() 这个方法是在 onResume 之后,只调用一次
onWindowFocusChanged(boolean hasFocus) 这个方法是在获取焦点和失去焦点是调用,在 onAttachedToWindow() 之后调用
这两个方法,回调时,用户并不可以操作,计算启动耗时的最好的时机应该是某个数据被加载出来后,当然会掺杂一些网络因素,但这里不是为了数据,只是为了用户体验,可以使用一些监听方法
1 | mView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { |
在 View 将要绘制的时候去进行耗时的统计,addOnDrawListener() 需要 API 16
2.2 启动耗时检测方式
2.2.1 TraceView
图形化界面的形式展示出执行时间、调用栈,包含所有线程,信息全面
但是加入 TraceView 的代码后,运行时开销严重,整体都会变慢,可能会带骗优化方向
- 通过代码跟踪💦
只能用于某一个方法的检测
1 | Debug.startMethodTracing("xxx"); |
默认生成文件在 mnt/sdcard/Android/data/packagename/fils
1 | adb pull /sdcard/Android/data/packagename/fils/my.trace |
这样可以将 trace 文件拷贝到项目根目录
也可以通过 Device File Explorer(AS 右下角) 找到文件,右键点击 Sava as…
- 通过命令跟踪💦
可用于整个开始结束的过程(内容会比较多~)
开始跟踪
1 | adb shell am start -n packagename/packagename.Activity --start-profiler /data/local/tmp/my.trace --sampling 1000 |
终止跟踪
1 | adb shell am profile stop |
拉取文件
1 | adb pull /data/local/tmp/my.trace |
这种方法可以检测启动到调用终止期间的全部内容,但是有些手机出来的 trace 文件时 0B,这就需要在需要停止的地方写上
1 | Debug.stopMethodTracing(); |
然后调用开始追踪,会自动停止。文件会放在 data/local/tmp 下。
- 查看 Trace 文件💦

这就是用 AndroidStudio 查看生成的 TraceView 文件
范围选择
标有
Cpu usage details unavailable的地方可以选择时间范围;标有 ·THREADS· 的地方可以选择某条线程,括号中的67表示检测全称有67条线程。选择时间或线程底部的Call Chart都会有相应的变化。执行选择
在线程选择下有一个
Wall Clock Time这个表示真正执行的时间,可以切换为Thread Time表示CPU执行的时间,Thread Time始终小于Wall Clock Time。CallChart
从上到下,调用者在上方,被调用者在下方;
系统 API是橙色、应用自身调用是绿色、第三方 API是蓝色,看绿色的部分很直观的可以看出我们自己代码在哪里耗时了~TopDown

| 标题 | 作用 |
|---|---|
| Total | 整个函数执行时间 |
| Self | 函数代码内执行时间 |
| Children | 子函数耗时 |
🌞举个栗子🌞:调用 A 函数整体时间是 Total ,在函数中执行了一行代码耗时 Self ,然后调用 B 函数耗时 Children
这两个方式是比较常用的方式,如果是应用自身方法都可以点击右键进入源码所在位置。
- 总结💦
- 运行时开销严重,整体都会变慢(因为
TraceView要抓取所有线程的所有执行函数以及顺序) - 可能会带偏优化方向(本来不好时间的函数可能加入
TraceView后,变的非常耗时)
2.2.2 systrace
结合 Android 内核的数据,生成 HTML 报告,API 18 以上,推荐 TraceCompat
- 首先清空后台💦
杀掉所有应用,防止出现莫名莫名方法。
- 在需要检测的地方写入代码(可以不写,但是会少一些 tag 提示)💦
1 | TraceCompat.beginSection("AppOnCreate"); |
- 执行检测💦
首先进入 SDK 目录,Sdk\platform-tools\systrace\ 目录下有一个 systrace.py 文件,打开 cmd 输入
1 | python systrace.py -b 32768 -t 5 -a <packagename> -o test.log.html sched gfx view wm am |
这种方式是 5 秒后自动输出
1 | python systrace.py gfx view wm am pm ss dalvik sched -b 32768 -a <packagename> -o test.log.html |
这种方式可以在自己收集完后,点击 Enter 键停止收集
两种内容是不太一样的, -t 表示时间,-a 表示包名,-o 输出文件名,最终在当前目录打开文件即可看到,文件只能使用 Chrome 来打开,如果打开 HTML 出现
Unable to select a master clock domain because no path can be found from "SYSTRACE" to "LINUX_FTRACE_GLOBAL".
那就是命令出错了,命令我也是收集了好久,一定要注意必须是 python 2.x,而不是能 3.x,否则可能会出现问题。另外,buffer 大小不可过大,否则会出现 oom 异常。最终找到可用的,想了解更多的请查看 Gityuan 或 官方文档
- 分析文件

这里可以看到 CPU 核心数以及运行状态,还有各个线程。
在 UI Thread 中的 AppOnCreate 正是我们在之前的代码埋 tag 点,点击条目并按 M 键可以查看具体信息。右上角是可以搜索 tag 的。
具体信息中有 Wall Duration (代码执行时间) CPU Duration (代码消耗 CPU 的时间)两者出现差值的原因是同步锁冲突。
这种方式不仅可以帮助监控启动过程中性能问题,卡顿优化的时候也可以用这种方式。因为它会把 UI 的渲染也检测到。
- 总结💦
- 轻量级,开销小
- 直观的反应 CPU 利用率
想要了解更多,全力推荐 Android Systrace
2.2.3 systrace + 函数插桩 AOP(Aspect Oriented Programming)
面向切面编程,针对同一类问题的统一处理,无侵入添加代码。
在根目录的 build.gradle 下,最新版本请查看 AspectJX 开源地址
1 | classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8' |
在 app 模块下添加如下,通过我的测试这些引用只能添加在 app 模块下,添加在其他子模块下不生效。最新版本请查看 AspectJ官网
顶部
1 | apply plugin: 'android-aspectjx' |
dependencies 内
1 | api 'org.aspectj:aspectjrt:1.9.5' |
下面是通过注解的方式结合 Systrace 进行埋点。
1 | /** |
具体的切入代码,需要将 packagename 换成自己的路径名字
1 |
|
在需要埋点的加入注解
1 |
|
这样就完成了 AOP 埋点辅助systrace 监控的方式。AOP 还有很多更强大更方便的用法,这里只是一种注解切入的方式。更多用法请查看其它文章。
由于我现在使用 AOP 还存在很多问题,还在研究中。。。(上面的例子是可以使用的)
2.2.4 Nanoscope
还未使用过 ~
在极客时间 《Android开发高手课》 中提到了 Nanoscope ,它是在 instrument 类型的性能分析工具中性能损耗比较小的。
它的实现原理是直接修改 Android 虚拟机源码,在 ArtMethod 执行入口和执行结束位置增加埋点代码,将所有的信息先写到内存,等到 trace 结束后才统一生成结果文件。在使用过程可以明显感觉到应用不会因为开启 Nanoscope 而感到卡顿,但是 trace 结束生成结果文件这一步需要的时间比较长。另一方面它可以支持分析任意一个应用,可用于做竞品分析。
但是它也有不少限制:
- 需要自己刷
ROM,并且当前只支持Nexus 6P,或者采用其提供的x86架构的模拟器。 - 默认只支持主线程采集,其他线程需要 代码手动设置 。考虑到内存大小的限制,每个线程的内存数组只能支持大约
20秒左右的时间段。
Uber 写了一系列自动化脚本协助整个流程,使用起来还算简单。Nanoscope 作为基本没有性能损耗的 instrument 工具,它非常适合做启动耗时的自动化分析。
Nanoscope 生成的是符合 Chrome tracing 规范的 HTML 文件。我们可以通过脚本来实现两个功能:
第一个是反混淆。通过 mapping 自动反混淆结果文件。
第二个是自动化分析。传入相同的起点和终点,实现两个结果文件的diff,自动分析差异点。
这样我们可以每天定期去跑自动化启动测试,查看是否存在新增的耗时点。
3 启动优化(干货)
3.1 闪屏页展示
闪屏页是优化启动速度的一个小技巧,虽然对实际的启动速度没有任何帮助,但是能让用户感觉应用到应用在第一时间已经被打开。
合并闪屏和主页面的 Activity,减少一个 Activity 会给线上带来 100 毫秒左右的优化。但是如果这样做的话,管理时会非常复杂。
闪屏如果存在网络请求,一般都是提前准备好闪屏页的,在下一次生效。
具体操作文末参考中会有 ~ 这里不再说明。
3.2 异步启动实践
3.2.1 启动器
微信内部使用的 mmkernel
阿里最近开源的 Alpha 启动框架
慕课网中学习到的 启动器 Task
它们为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。
3.2.1 异步Layout子线程预加载
参考文章 Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载)
遇到个点,如果 xml 中是 com.google.android.material.appbar.AppBarLayout 等 google 包里的,需要加上一条属性
1 | android:theme="@style/Theme.AppCompat.Light.DarkActionBar" |
这个属性与自己 Activity 的 theme 相对应。
3.3 数据重排
支付宝 《通过安装包重排布优化 Android 端启动性能》
3.4 类重排
本人费劲心血自己自己实现了一次~ Redex Ubuntu 实现
更多资料:
Facebook 开源的 Dex 优化工具 ReDex
Redex 初探与 Interdex:Andorid 冷启动优化
都9102年了,Android 冷启动优化除了老三样还有哪些新招?
3.5 延迟加载方案 IdleHandler
利用 IdleHandler 特性,空闲时执行初始化。
IdleHandler 可以用来提升性能,主要用在我们希望能够在当前线程消息队列空闲时做些事情 (譬如 UI 线程在显示完成后,如果线程空闲我们就可以提前准备其他内容)的情况下,不过最好不要做耗时操作。
1 | Looper.myQueue().addIdleHandler(new IdleHandler() { |
如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。我们可以将从服务器获取推送 Token 的任务放在延迟 IdleHandler中执行,或者把一些不重要的 View 的加载放到 IdleHandler 中执行
3.6 其他优化思路
AndroidX App Startup
App 启动运行时会初始化一些逻辑,它们为了方便开发者使用,避免开发者手动调用,使用 ContentProvider 进行初始化
- 多个
ContentProvider会增加了App启动运行的时间。 ContentProvider的onCreate方法会先于Application的OnCreate方法执行,这是在冷启动阶段自动运行初始化的,
这样只会增加 App 的加载时间,用户希望 App 加载得快,启动慢会带来糟糕的用户体验,AndroidX App Startup 正是为了解决这个问题而出现的。
App Startup Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
5.0 以下机型 MultiDex 优化
GC 优化
支付宝提出一种 GC 抑制 的方案
在启动过程,要尽量减少 GC 的次数,避免造成主线程长时间的卡顿。
特别是对 Dalvik 来说,我们可以通过 systrace 单独查看整个启动过程 GC 的时间。
1 | python systrace.py dalvik -b 90960 -a com.sample.gc |
1 | // GC使用的总耗时,单位是毫秒 |
- 避免进行大量的字符串操作,特别是序列化和反序列化
- 频繁创建的对象需要考虑复用
- 转移到 Native 实现
参考 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
系统调用优化
通过 systrace 的 System Service 类型,我们可以看到启动过程 System Server 的 CPU 工作情况。在启动过程,我们尽量不要做系统调用,例如
- 启动过程中减少系统调用,避免与
AMS、WMS竞争锁。启动过程中本身AMS和WMS的工作就很多,且AMS和WMS很多操作都是带锁的,如果此时App再有过多的Binder调用与AMS、WMS通信,SystemServer就会出现大量的锁等待,阻塞关键操作 - 启动过程中不要启动子进程,如果好几个进程同时启动,系统负担则会加倍,
SystemServer也会更繁忙 - 启动过程中除了 Activity 之外的组件启动要谨慎,因为四大组件的启动都是在主线程的,如果组件启动慢,占用了
Message通道,也会影响应用的启动速度 Application和主Activity的onCreate中异步初始化某些代码
在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。
线程优化
线程优化就像做填空题和解锁题,我们希望能把所有的时间片都利用上,因此主线程和各个线程都是一直满载的。当然我们也希望每个线程都开足马力向前跑,而不是作为接力棒。所以线程的优化主要在于减少 CPU 调度带来的波动,让应用的启动时间更加稳定。
从具体的做法来看,线程的优化一方面是控制线程数量,线程数量太多会相互竞争 CPU 资源,因此要有统一的线程池,并且根据机器性能来控制数量。
3.2.1 启动器 提到,合理分配线程数量。
另一方面是检查线程间的锁。
业务优化
我们首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。
通过梳理之后,剩下的都是启动过程一定要用的模块。
这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。
退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现(上面有讲到),但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。
I/O 优化
启动过程不建议出现网络 I/O
磁盘 I/O 是启动优化一定要抠的点
还有一个是数据结构的选择问题,我们在启动过程只需要读取 Setting.sp 的几项数据,不过 SharedPreference 在初始化的时候还是要全部数据一起解析。如果它的数据量超过 1000 条,启动过程解析时间可能就超过 100 毫秒。如果只解析启动过程用到的数据项则会很大程度减少解析时间,启动过程适合使用随机读写的数据结构。
保活
保活可以减少 Application 创建跟初始化的时间,让冷启动变成温启动。不过在 Target 26 之后,保活的确变得越来越难。
对于大厂来说,可能需要寻求厂商合作的机会,例如微信的 Hardcoder 方案和 OPPO 推出的 Hyper Boost 方案。根据OPPO 的数据,对于手机 QQ、淘宝、微信启动场景会直接有 20% 以上的优化。
有的时候你问为什么微信可以保活?为什么它可以运行的那么流畅?这里可能不仅仅是技术上的问题,当应用体量足够大,就可以倒逼厂商去专门为它们做优化。
插件化和热修复
大部分的框架在设计上都存在大量的 Hook 和私有 API 调用,带来的缺点主要有两个:
- 稳定性差。虽然大家都号称兼容 100% 的机型,由于厂商的兼容性、安装失败、
dex2oat失败等原因,还是会有那么一些代码和资源的异常。Android P 推出的non-sdk-interface调用限制,以后适配只会越来越难,成本越来越高。 - 性能差。Android Runtime 每个版本都有很多的优化,因为插件化和热修复用到的一些黑科技,导致底层 Runtime 的优化我们是享受不到的。Tinker 框架在加载补丁后,应用启动速度会降低 5%~10%。
应用加固对启动速度来说简直是灾难,有时候我们需要做一些权衡和选择。
感谢
支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
以及上文中的链接







