Android项目时间越长,一不留神,就发现启动越来越慢,越慢越慢......
这极大地影响了用户体验,所以提升启动速度非常重要了。
所谓工欲善其事,必先利其器,本章咱们先来讲讲启动优化需要的一些工具!
一、 一些简单的启动耗时统计
1. ADB 命令查看启动耗时。
在命令行中输入以下命令,它会自己启动APP,并且打印启动耗时。
adb shell am start -S -W com.xiaoxiao.demo/.MainActivity
打印结果:
Stopping: com.xiaoxiao.demo
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xiaoxiao.demo/.MainActivity }
Status: ok
Activity: com.xiaoxiao.demo/.MainActivity
ThisTime: 6309
TotalTime: 6309
WaitTime: 6371
Complete
ThisTime :该activity启动耗时,单位ms。
TotalTime :应用自身启动耗时=ThisTime+应用application等资源启动时间。WaitTime :系统启动应用耗时=TotalTime+系统资源启动时间。
2. logcat查看页面启动时间
在logcat中过滤包含 Displayed 的打印结果。这个值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。
ActivityManager: Displayed com.babytree.apps.pregnancy/.MainActivity: +6s349ms
ActivityManager: Displayed com.babytree.apps.pregnancy/.MainActivity: +5s543ms
3. 增加启动耗时日志
想知道比较细节一些的,比如Application中哪些任务比较耗时,总耗时等,就可以自己打印一些日志了。在方法调用前后分别加上日志,记录时间即可。
/**
* 方便查看方法调用时长。
* 使用 方法前调用 begin("FACTION_TAG_NAME") 方法后调用 end("FACTION_TAG_NAME")
*/
object TimeLog {
private var TAG = "TimeLog"
private var isOpenLog = false
private var times: ConcurrentHashMap<String, Long>? = null
private var current = System.currentTimeMillis()
private var current1 = current
@JvmStatic
fun openLog(open: Boolean) {
isOpenLog = open
if (isOpenLog) {
times = ConcurrentHashMap(20)
}
}
@JvmStatic
fun setLogTag(tag: String) {
TAG = tag
}
/**
* 与 end 或者 endI 成对出现
*/
@JvmStatic
fun begin(name: String?) {
if (times != null && !name.isNullOrEmpty()) {
times!![name] = System.currentTimeMillis()
}
}
/**
* 与begin成对出现
*/
@JvmStatic
fun end(name: String?) {
if (times != null && !name.isNullOrEmpty() && times?.get(name) != null) {
val l = System.currentTimeMillis() - times?.get(name)!!
Log.d(TAG, "$name:$l")
}
}
/**
* 与begin成对出现
*/
@JvmStatic
fun endI(name: String?) {
if (!times.isNullOrEmpty() && !name.isNullOrEmpty() && times?.get(name) != null) {
val l = System.currentTimeMillis() - times?.get(name)!!
Log.i(TAG, "$name:$l")
}
}
/**
* 打印第一次初始化此类时间和现在时间的时间差。
* 打印与上一个调用times()方法的时间差。
* 打印当前时间的时间戳。
*/
@JvmStatic
fun times(tag: String) {
val millis = System.currentTimeMillis()
val l = millis - current
val l1 = millis - current1
Log.d(TAG, "$tag:totalTime:$l,thisTime:$l1,current:$millis")
current1 = millis
}
}
比如,想要知道直播初始化需要的时间:
TimeLog.begin("LiveCenter")
LiveCenter.init(this, 1, sdkDebug)
TimeLog.end("LiveCenter")
这种方式比较简单,方便我们自己查看各任务的启动时间,快速定位哪些任务耗时,后续再针对任务进行分析优化。还有,这种直接打印也只是自己看看,知道个大概,数据不准确。如果想知道准确的数据,则需要大批量的数据信息了,这个现在有些公司提供的有服务,比如火山,也是通过这种类似的方式添加日志,不过他们已经做好了数据统计,所以比较方便查看大数据量的统计结果,这样就能看到各个任务在各种机型和所有手机上的平均启动时长等信息了。
来,筒子们,看看美图,养养眼,咱们休息1秒钟~
二、启动时长分析监测工具
1. Systrace工具
最简单的生成 HTML 报告的命令:
python2 systrace.py
在systrace.py文件目录下(在platform-tools文件夹里面),输入左边命令。比如我的systrace.py在以下目录中
file:///Users/xiaoxiao/Library/Android/sdk/platform-tools/systrace
手机连上电脑,按下enter命令后就开始跟踪,启动想要追踪的APP,启动结束后,在命令行中再次按 Enter 键结束跟踪。systrace 会将报告保存到 systrace.py 所在的目录中,并将其命名为 trace.html。
至于systrace的其他命令,大家可以上网查啊,一大堆啊,我就不详细描述了。
python2 systrace.py -a com.xiaoxiao.demo -t 9 -o trace_app_9s.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory
trace.html
可以直接用浏览器打开 ( 基本操作:w键放大,s键缩小,a左移,d右移 ),这时候就可以比较直观的查看启动耗时了。
对于比较耗时的任务,可以查看它里面具体的耗时的方法,从而分析耗时的原因,再看能不能减少耗时。来,咱们举个栗子啊!
仔细看FileProvider启动耗时,我们发现它的启动耗时主要就是getPathStrategy() 方法耗时。如果要解决FileProvider耗时问题,就得从这个方法出发,往下找它的耗时原因,再找解决方案。
这里我直接说结果啊,分析完源码的结果就是:getPathStrategy 是在启动阶段完全没有必要的,可以在 FileProvider的query、getType、openFile 等接口被调用到的时候再去执行 getPathStrategy 逻辑。
解决方案:FileProvider 是 androidx 中的代码,无法直接修改,但是它会参与代码编译,所以可以在编译阶段通过修改字节码的方式去修改它的实现。
通过字节码修改FileProvider.attachInfo 内 getPathStrategy耗时方法调用时机,延迟给mStrategy变量赋值的减少Application.attachBaseContext结束到onCreate开始之间的耗时,然后在 FileProvider的query、getType、openFile 等接口被调用到的时候再去执行 getPathStrategy 并赋值的逻辑。
上图是优化后的图,可以看到在启动的时候getPathStrategy没有调用,不过图中没有体现出来启动时长差异,据不负责统计,实际上启动时长短了约30ms(这个我们没有走大数据量的统计,只是根据个别手机得出来的一个数值)。
篇幅原因,这里涉及到的修改字节码和具体修改这里不细说了,回头会单独写一篇文章说说FileProvider的启动耗时优化。感兴趣的朋友也可以在网上搜一下这一块的文章。
2. 字节开源性能分析工具-Btrace
btrace(又名 RheaTrace) 是一个基于 Systrace 实现的高性能 Android trace 工具,它支持在 App 编译期间自动注入自定义事件,并使用 bhook额外提供 IO 等 native 事件。
特征:
支持自动注入自定义事件,在编译 Apk 期间为 App 方法自动注入Trace#beginSection(String) 和 Trace#endSection()。
提供额外 IO 等 native 事件,方便定位耗时原因。
支持仅采集主线程 trace 事件。
使用便捷,稳定性高,性能优于 Systrace。
接入和使用:
- 根目录下 build.gradle 文件中增加 rhea-gradle-plugin 作为依赖。
dependencies {
classpath 'com.bytedance.btrace:rhea-gradle-plugin:1.0.1'
}
- app/build.gradle 文件中应用如下所示插件和依赖。
dependencies {
//rheatrace core lib
implementation "com.bytedance.btrace:rhea-core:1.0.1"
}
...
rheaTrace {
compilation {
//为减少 APK 体积, 你可以为 App 中需要跟踪的方法设置 id 以此来跟踪此自定义事件, 默认值 false。
traceWithMethodID = false
//该文件配置决定哪些方法您不希望跟踪, 默认值 null。
traceFilterFilePath = "${project.rootDir}/rhea-trace/traceFilter.txt"
//用特指定方法 id 来设置自定义事件名称, 默认值 null。
applyMethodMappingFilePath = "${project.rootDir}/rhea-trace/keep-method-id.txt"
}
runtime {
//仅在主线程抓取跟踪事件, 默认值 false。
mainThreadOnly true
//在 App 启动之初开始抓取跟踪事件, 默认值 true。
startWhenAppLaunch true
//指定内存存储 atrace 数据 ring buffer 的大小。
atraceBufferSize "500000"
}
}
...
apply plugin: 'com.bytedance.rhea-trace'
- 检测电脑 python 版本,由于 Systrace 的关系 RheaTrace 仅支持 python2.7 版本,请将 systrace配置在环境变量中。
例如,我是Mac,环境变量配置在 ~/.bash_profile 文件中。
export PATH=${PATH}:/Users/${user_name}/Library/Android/sdk/platform-tools/systrace
以上配置完成后,打开终端,先进入到rheatrace.py 所在位置,你下载的btrace代码存放位置。
如:/Users/xiaoxiao/code/btrace/scripts/python/rheatrace/
btrace的命令和systrace是一样的昂~~
python2 rheatrace.py -a com.xiaoxiao.demo -t 9 -o rheatrace_9s.html
已知问题(官方说的啊~):
仅支持 python2.7,请注意检查 python 环境。
暂不支持 Windows。
仅支持采集主进程的 trace 事件。
需要外置存储的读写权限,因此您需要手动赋予该权限。
如果您无法直接打开输出产物 systrace.html ,请用 perfetto 加载。ps:我就是这样的,直接用浏览器打不开,只能用perfetto打开。perfetto也很好用,直接将文件拖进去就行了,而且它支持手势放大缩小,不用像在浏览器那样用按键控制。
perfetto打开的大概长这样。
筒子们,咱们再休息一秒钟来~
三、 profiler使用和分析
Traceview是android平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到每个方法的执行时间。但是目前Traceview 已弃用。
如果使用 Android Studio3.2 或更高版本,则应改为使用 CPU Profiler.谷歌官网profiler工具使用文档:
采样方式有:
- 直接采样,运行APP的时候,打开profile,点击开始采样。
- 代码采样,通过代码进行精准采样。
- 捕获设备上的系统跟踪记录
1. 直接采样
APP --》 Edit configurations --》 Profiling
--》 勾选 Start this recording on startup
--》 选择Java/Kotlin Mothed Trace(全采样) 或者 Java/Kotlin Mothed Sample(部分采样,建议选择这个,另一个太卡了)
采样示例图如下:
分析数据主要看主线程的,就是上面的main模块。
2.代码采样
在需要采样的类里面,通过以下代码进行采样
val fileName = BAFFileUtil.getSDCardPath() + "test_trace_app_all"
Debug.startMethodTracing(fileName,800000000)
//startMethodTracing(String tracePath, int bufferSize) {
//tracePath:trace文件存放位置;
//bufferSize: trace文件最大支持的大小如果文件不设置大小,最大默认为8M,一般都不够。
//间隔采样
//intervalUs 间隔时间,微秒
//startMethodTracingSampling(String tracePath, int bufferSize,int intervalUs)
Debug.startMethodTracingSampling(fileName,800000000,10)
//结束采样
Debug.stopMethodTracing()
注意:
startMethodTracing 和 stopMethodTracing 需要成对出现
startMethodTracingSampling 和 stopMethodTracing 需要成对出现
3. 设备直接采样
优点:方便 缺点:不够详细,很多东西没采样到
首次使用设置:
如果您是首次在测试设备上使用 System Tracing,或在设备的快捷设置面板中看不到 System Tracing 图块(手机采样3.png),请完成以下设置步骤:
启用开发者选项(如果尚未启用此选项)。
打开开发者选项设置屏幕。
在调试部分中,选择 System Tracing。此时会打开 System Tracing 应用,其中显示了应用菜单。
在应用菜单中,启用显示“快捷设置”图块,如图 2 所示。系统会将 System Tracing 图块添加到快捷设置面板中。这样就可以采样了~
四、启动优化器
这个不算是工具啊,是一种启动框架,一般是通过有向无环图构建的异步启动框架。感兴趣的同学可以自己了解一下~
Alpha启动框架
android-startup
AppStartFaster
以上,就说到这里!
如果能帮到你是我的荣幸,如果有错误欢迎指正,欢迎留言~