优化思路
优化标准
优化的过程标准要从用户体验出发,从点击图标到用户真正可以操作的整个过程。
业务梳理
首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。保证启动期间加载的每个功能和业务都是必须的,这对中低端机上的表现会有很大的改进。
业务优化
业务梳理完成后,剩下的都是启动过程一定要用的模块。这个时候就只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟耗时在哪里。
比如考虑这些耗时任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更复杂,建议衡量修改后的维护成本再决定是否使用这种方法。另外,懒加载要防止集中化,否则容易出现首页显示出来但用户无法操作的情形。
应用启动过程分析
启动的三种状态
应用启动的三种状态:冷启动、温启动或热启动。
在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。
最好在冷启动的基础上进行优化,这样同时也可以提升温启动和热启动的性能。
冷启动
冷启动开始,系统首先要做三个任务:
系统创建应用进程后,应用进程就负责后续阶段:
整个冷启动流程图如下:
热启动
应用的热启动比冷启动简单得多,开销也更低。
在热启动中,系统的所有工作就是将您的Activity
带到前台。只要应用的所有 Activity
仍驻留在内存中,应用就不必重复执行对象初始化、布局填充和呈现。
温启动
温启动包含了在冷启动期间发生的部分操作,它的开销要比热启动高。以下情况可视为温启动:
- 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用
onCreate()
从头开始重新创建Activity
。应用只会重走Activity
的生命周期,而不会重走进程的创建,Application
的创建与生命周期等。 - 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和
Activity
需要重启,但传递到onCreate()
的state bundle
实例已保存。
启动过程的常见问题
1.点击应用图标很久没有响应
系统在拉起应用进程之前,会先根据应用的 Theme
属性创建预览窗口,这会耗费一定的时间,尤其在低中端机比较明显。
解决方法:可以禁用预览窗口或者将预览窗口指定为透明,但用户在这段时间看到的还会是桌面,给用户的感觉会是怎么没反应,是没点中图标吗?体验不是很好。
2.应用首页显示太慢了
随着应用的业务越来越复杂,用到各种框架和闪屏广告,这些都要在应用启动阶段的时候去做,这就会出现首页需要很长的时间才会显示出来。
3.首页显示出来了也没法操作
对于问题2,容易想到的一个方法是,尽量把启动阶段的初始化任务异步化执行。
要注意的是,那些需要准备好才能正常使用的资源如果异步处理的话,很可能会造成首页出现白屏,或首页出现后却没法操作的问题。
工具善其事,必先利其器
日志和ADB
1.查看日志
在 Android 4.4(API 级别 19)及更高版本中,logcat
包含一个输出行,其中包含名为 Displayed
的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。
因为提供此日志的是系统服务器,不是应用本身,所以记得查看是关闭过滤器
可以看到类似这样的日志:
I/ActivityManager: Displayed com.xxx.xxx/.activity.xxxActivity: +1s539ms
复制代码
2.adb命令行
adb shell am start -S -W com.xxx.xxx/com.xxx.xxx.activity.xxxActivity
复制代码
-S
:在启动 Activity 前,强行停止目标应用
-W
:等待启动完成
启动成功后,可以看到启动时间数据:一般只要关心TotalTime
即可,这个时间才是自己应用真正启动的耗时。
Status: ok
Activity: com.xxx.xxx/com.xxx.xxx.activity.xxxActivity
ThisTime: 1539
TotalTime: 1539
WaitTime: 1621
Complete
复制代码
ThisTime:表示一连串启动Activity
的最后一个Activity`的启动耗时
TotalTime:表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时
WaitTime:总的耗时,包括前一个应用Activity pause
的时间和新应用启动的时间
如果对这条命令是如何得出的三个时间感兴趣的,可以看知乎Groffa的回答:
一款合适的启动优化分析工具
常用的启动分析工具有Traceview
和systrace
:
Traceview
性能损耗太大,得出的结果并不真实。
systrace
可以很方便地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析。
TraceView试图收集某个阶段所有函数的运行信息,它希望在你并不知道哪个函数有问题的时候直接定位到关键函数;但可惜的是,收集所有信息这个是不现实的,它的运行时开销严重干扰了运行环境。
Systrace的思路是反过来的,它会统计出一些基本的信息,让开发者通过假设-分析-验证 的过程一步一步找出问题的原因。
选择systrace
,再配合上函数插桩,是一个优秀的方式。
systrace + 函数插桩 。通过对需要统计耗时的函数进行插桩后,就可以借助systrace
生成HTML
报告进行分析。
函数插桩很简单,借助系统自带的Trace
类即可,具体使用下面就详细介绍~
优化过程分析
Systrace
systrace
是Android4.1
中新增的性能数据采样和分析工具。它可帮助开发者收集 Android
关键子系统(如 SurfaceFlinger/SystemServer/Kernel/Input/Display 等 Framework 部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
systrace脚本文件位置:xxx/Android/sdk/platform-tools/systrace
执行以下命令就可以生成HTML报告:
python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am
复制代码
通过命令参数,可以查看常用的数据,分析启动过程,dalvik
、sched
、ss
、am
类型是我们比较关心的:
- sched:CPU Scheduling, CPU调度的信息,可看出CPU在每个时间段在运行什么线程,线程调度情况,比如锁信息。
- ss:System Server
- dalvik: Dalvik VM,虚拟机相关信息,比如GC停顿
- am: Activity Manager,分析Activity的启动过程很有用
以上的类型可能在某些机型不支持,adb
连接到测试机,通过以下命令可查看systrace
支持的类型,
python systrace.py --list-categories
复制代码
Perfetto分析报告
直接在浏览器打开上述的HTML文件,可以查看报告
在
mac
电脑上,Chrome
直接打开上述生成的HTML
是空白的。解决办法:在
chrome
地址栏中输入chrome:tracing
,然后点击load
按钮选择你的trace.html
文件。
不过更推荐使用Perfetto工具:
Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。与 Systrace 不同,它提供数据源超集,可让您以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。
我们的设备可能是Android10以下,不能使用Perfetto
提供的新追踪方式。但它也支持打开通过systrace
生成的trace
文件,它的UI交互使用起来更舒服。
Trace跟踪
通过Trace
类,可跟踪方法到调用流程,配合systrace
视图分析,非常好用,比如跟踪下onResume
方法
protected void onResume() {
super.onResume();
Trace.beginSection("START_TEST");
// 你的操作
...
Trace.endSection();
Log.i(TAG, "[onResume]");
}
复制代码
注意: 如果要想再systrace
查看到跟踪阶段,需要在之前的命令行加上 -a 【包名】
python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am -a 【包名】
复制代码
在Perfetto
打开生成mynewtrace.html
文件,找到【包名】的进程,可以看到我们添加的Trace跟踪出现了(蓝颜色条目):
小栗子
运行一个demo程序,先退出应用。执行命令:
python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am -a 【包名】
复制代码
启动demo
程序,页面成功打开后,结束命令,会生成一份HTML报告,使用Perfetto打开该文件,找到该应用进程,里面有应用启动的多个阶段:
选择其中一个阶段,可以看到很多时间统计,有两个比较重要的时间指标:
- Wall Duration : 代码持续耗时时间,即这段代码的耗时时间
- CPU Duration : 这段代码在CPU上真正的耗时
如果发现Wall Duration
和CPU Duration
的时间差很大,就说明这部分代码有明显的耗时,可以考虑优化了。
比如上图的ActivityStart
过程的耗时情况:
Wall Duration | 78.282 ms |
---|---|
CPU Duration | 71.004 ms |
可以看到代码耗时和CPU实际耗时差距不大
接下来再分析bindApplication
过程:
Wall Duration | 3,053.505 ms |
---|---|
CPU Duration | 49.767 ms |
发现Wall Duration
用了3s+,CPU实际耗时才49ms,这就不对头了,CPU这个阶段并没有一直在做事。
此时就要从代码上分析bindApplication
过程,应用层就可以分析Application
的代码,我的代码是这样的:
public class SampleApplication extends Application {
private static Context sContext;
@Override
public void onCreate() {
super.onCreate();
sContext = this;
try {
// 模拟耗时操作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static Context getContext(){
return sContext;
}
}
复制代码
显而易见,原因是我在Application
的onCreate
做了3s睡眠。当然这里只是简单模拟了耗时操作,实际业务肯定比这复杂的,但整体的分析思路是一样的。
统计方法耗时方案
如何查看每一个方法执行所耗时呢?
通过AOP统计方法耗时并打印
如果只是想更方便的统计方法耗时,可以通过AOP方式插入统计耗时的代码,在每个方法上添加一行注解就可以获取到该方法的耗时日志打印。
具体实现方式可以参考笔者的这篇博客写给Android工程师的AOP知识
自定义插件
如果想通过systrace
工具查看每个方法的耗时,就需要在每个方法的前后加Trace
检测代码
如果通过手动添加,这肯定是个体力活,不是一个对摸鱼有追求的程序猿做的事。这个时候就要想办法自动化,我们可以通过自定义插件的方法,去插桩我们的Trace
代码。
好在这个插件已经有大佬写了,我们可以参考学习下:
插件使用
该插件的使用方式在插件的readme
写的很详细了,这里就不copy
了。
插件解析
插件的源码比较多,笔者抽出最核心的结构代码剖析下:
1.自定义Extension:该扩展类支持在build.gradle
下使用的属性
class SystraceExtension {
boolean enable
String baseMethodMapFile
String blackListFile
String output
SystraceExtension() {
enable = true
baseMethodMapFile = ""
blackListFile = ""
output = ""
}
}
复制代码
2.应用自定义的Plugin:
@Override
void apply(Project project) {
project.extensions.create("systrace", SystraceExtension)
if (!project.plugins.hasPlugin('com.android.application')) {
throw new GradleException('Systrace Plugin, Android Application plugin required')
}
project.afterEvaluate {
def android = project.extensions.android
def configuration = project.systrace
android.applicationVariants.all { variant ->
String output = configuration.output
if (Util.isNullOrNil(output)) {
configuration.output = project.getBuildDir().getAbsolutePath() + File.separator + "systrace_output"
Log.i(TAG, "set Systrace output file to " + configuration.output)
}
Log.i(TAG, "Trace enable is %s", configuration.enable)
// 判断是否配置了开启
if (configuration.enable) {
SystemTraceTransform.inject(project, variant)
}
}
}
}
复制代码
3.自定义Transform:对需要插桩的方法,插桩Trace方法
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
}
input.jarInputs.each { JarInput jarInput ->
if (jarInput.getStatus() != Status.REMOVED) {
collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
}
}
}
MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
// 收集源代码和jar文件中的所有方法
HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(
scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
// 对所有的方法插桩代码
methodTracer.trace(scrInputMap, jarInputMap)
origTransform.transform(transformInvocation)
}
复制代码
4.遍历所有的方法插桩trace代码
# MethodTracer
private void innerTraceMethodFromSrc(File input, File output) {
for (File classFile : classFileList) {
if (mTraceConfig.isNeedTraceClass(classFile.getName())) {
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
is.close();
if (output.isDirectory()) {
os = new FileOutputStream(changedFileOutput);
} else {
os = new FileOutputStream(output);
}
os.write(classWriter.toByteArray());
os.close();
}
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
isMethodBeatClass);
}
}
复制代码
5.利用ASM工具修改字节码
public final static String MATRIX_TRACE_METHOD_BEAT_CLASS = "com/sample/systrace/TraceTag";
# MethodTracer
private class TraceMethodAdapter extends AdviceAdapter {
// 进入方法
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
String sectionName = methodName;
int length = sectionName.length();
if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
// 先去掉参数
int parmIndex = sectionName.indexOf('(');
sectionName = sectionName.substring(0, parmIndex);
// 如果依然更大,直接裁剪
length = sectionName.length();
if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
sectionName = sectionName.substring(length - TraceBuildConstants.MAX_SECTION_NAME_LEN);
}
}
// visitLdcInsn:加载到堆栈上的常量
mv.visitLdcInsn(sectionName);
// visitMethodInsn:调用方法的指令,这里调用TraceTag的i方法
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_METHOD_BEAT_CLASS, "i", "(Ljava/lang/String;)V", false);
}
}
// 退出方法
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
// visitMethodInsn:调用方法的指令,这里调用TraceTag的o方法
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_METHOD_BEAT_CLASS, "o", "()V", false);
}
}
}
复制代码