Stan_Z原创文章,转载注明出处,不过我也设置了禁止转载,嘻嘻。
一、优化大纲介绍
二、启动时间测量
2.1 am start
$ adb shell am start -W com.stan.androidproj/.app.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.stan.androidproj/.app.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.stan.androidproj/.app.MainActivity
TotalTime: 645
WaitTime: 646
Complete
thisTime: 最后一个activity启动耗时
TotalTime: 所有activity启动耗时(包含splash页)
WaitTime: AMS启动activity总耗时
2.2 Displayed
01-09 10:35:06.800 1698 1815 I ActivityTaskManager: Displayed com.stan.androidproj/.app.MainActivity: +645ms
Displayed展示的和am start的TotalTime一致
2.3 系统角度启动时间定位
起始于system_server的iq 最后一个up事件,到SurfaceFlinger完成第一帧处理。事实上当前帧只是被post到了FrameBuffer,内容会在下一个vsync信号才会被Display消费。但是基本就关注iq到当前SurfaceFlinger合成就差不多了。
三、分析工具
3.1Systrace
添加标签
App:
void func() {
TraceCompat.beginSection("”);
...
TraceCompat.endSection();
}
Jave Framework:
import android.os.Trace;
void func() {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Activity:setContentView"); //choose one tag from Trace.java
...
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
Native:
#define ATRACE_TAG ATRACE_TAG_ALWAYS
#include <utils/Trace.h> // for c++
#include <cutils/trace.h> // for c
ATRACE_CALL();
ATRACE_BEGIN();
ATRACE_END();
.bashrc配置了个小函数,方便无脑使用。
function systrace {
systrace_path=“/XXX/Android/sdk/platform-tools/systrace/systrace.py"
python $systrace_path -a $1 gfx input audio view webview wm am hal app res dalvik rs bionic power sched freq idle load sync workq memreclaim irq disk binder_driver binder_lock -b 10240 -t 5 -o "/XXX/systrace-$2.html"
}
使用:systrace packagename filename
官方使用介绍:了解 Systrace
3.2TraceView
添加标签:
Debug.startMethodTracing("filename");
...
Debug.stopMethodTracing(“”)
生成文件默认在 sdcard/Android/data/packagename/files 目录下
.bashrc配置了个小函数,针对应用冷启动直接抓取整段trace文件
function lunch {
adb shell am start -S -W -n $1 --start-profiler /data/local/tmp/$2.trace
printf "start trace...";
adb shell am profile $1 stop
adb pull /data/local/tmp/$2.trace /XXX/traceview/$2.trace
printf "pull success.";
}
使用:lunch packagename(包含top activity) filename
使用参考:Android 性能优化:使用 TraceView 找到卡顿的元凶
3.3SimplePerf
使用 Simpleperf 可以看到所有的 Native 代码的耗时,有时候一些 Android 系统库的调用对分析问题有比较大的帮助,例如加载 dex、verify class 的耗时等。同时他的性能开销比traceView小很多。
同样写了个.bashrc配置了个小函数,方便使用
function simpleperf {
path=“系统源码的如下路径:/system/extras/simpleperf/scripts/"
python ${path}app_profiler.py -p $1 -a $2
adb pull /data/local/tmp/perf.data $path
python ${path}report_html.py
}
使用:simpleperf packagename .activityName
工具如何使用不铺开说了,网上相关文章也非常多。
四、启动流程
既然是做启动优化,那么相关的代码流程也务必需要全局了解,之前针对启动流程进行过详细梳理,可以参考之前文章,这里就不赘述了:
应用启动流程梳理(一)-应用安装流程
应用启动流程梳理(二)-Input事件传递流程
应用启动流程梳理(三)-Activity启动流程
应用启动流程梳理(四)-视图处理流程
五、启动优化方案
5.1 app做的优化:
常规部分:
这部分是常规优化方案,属于所有app可以去普及落地的方案,并且优化效果明显。
1) 添加startingwindow
它是应用启动过程一个过渡窗口,目的是提高启动响应体验,本身不会影响到冷启动速度。
<style name="WelcomeTheme" parent="@style/AppTheme">
<item name="android:windowBackground">@mipmap/ic_splash</item>
</style>
2)application初始化内存,异步改造。
一般app 会在application oncreate中初始化很多三方库,这个初始化过程如果在主线程中势必造成耗时,那么肯定需要选择用异步来处理,方案如何选择呢?直接起个线程?不好管理,那么线程池呢?会好些,但是有些复杂条件不好满足,或者说写起来不够优雅,比如:B初始化需要依赖A先初始化,或者C的初始化必须要要求在application onCreate中完成,转化为需求就是要实现有序队列以及条件阻塞。
目前好点的方案就是使用启动器:成熟项目比如阿里开源的 Alpha
如果自己写:
主要解决两点核心问题:
1实现有序队列 :参考有向无环图的拓扑排序算法
2条件阻塞:可以使用CountDownLatch
3)IdelHandler延迟加载
对应及时性要求不高的任务可以使用IdleHandler来处理,它是在MessageQueue空闲的时候才会回调执行的消息。
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {//处理消息
if (!tasks.isEmpty()) {
Runnable task = tasks.get(0);//取出任务
task.run();
tasks.remove(task);//移除任务
}
return !tasks.isEmpty();
}
};
//添加消息
Looper.myQueue().addIdleHandler(idleHandler);
深入部分
这部分内容不算优化的重点,投入产出比偏低,属于大厂成熟期项目抠细节的优化方案。
1)安装包重排布
安装包重排布是站在IO优化的角度来做的优化,核心原理简单说就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
注:
访问文件内容首选去page cache中去找,cache没命中会产生缺页中断,然后去磁盘读取文件,并缓存到page cache中,这种有文件背景的page cache叫文件页,这种类型页面在内存回收时,有脏数据会做回写,没有则丢弃。另外无文件背景的page cache叫匿名页,如进程的堆、栈、数据段使用的页,这种类型页面在内存回收时,有脏数据会交换到swap(android 如果打开的话,会swap 到zram区间),没有则丢弃。
结合 Android 系统实际来看,上层 App 每次读取磁盘时,文件系统默认会按 16 * 4k block 去磁盘读取数据,一次IO能缓存多个page页(1个page 4K)。
磁盘的访问的速度比内存慢好几个数量级。
如何获取启动过程牵涉到的dex加载的类信息、 xml和resource?
BaseDexClassLoader findClass hook类信息
ResourcesImpl loadXmlResourceParser hook xml信息
ResourcesImpl loadDrawableForCookie hook drawable信息
如何分别对资源和类做重排列?这部分具体参考对应文章说明:
redex做了类重排列:
Redex 初探与 Interdex:Andorid 冷启动优化
支付宝做了资源重排列:
支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能)
2)启动阶段抑制GC
支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
这个方案主要针对的Dalvik虚拟机,现在虚拟机都是ART了,ART在对GC做了优化:gc被触发时,dalvik在查找无用对象时挂起所有线程,art则是在查找无用对象时并发执行,回收对象才挂起所有线程,缩短了挂起时间,所以首先不知道ART是否也能做GC抑制,如果能做的话,抑制收益应该相比Dalvik也会大打折扣。
个人感觉GC抑制在app的角度来说,还是尽量考虑如何做正向抑制,比如说:避免进行大量的字符串操作,特别是序列化和反序列化;频繁创建的对象需要考虑复用等角度去减少GC。
3)绕过VerifyClass验证
应用冷启动过程,有时候会在bindApplication阶段看到比较多的VerifyClass验证,这个过程相对比较耗时,例如如下对比,两款相同app,一个做了VerifyClass, 一个没做:
光binderApplication阶段就有270ms左右的差别,后者红框部分夹杂了大量的VerifyClass。
简单解释什么场景下会走VerifyClass:
类加载时,需要对类信息做验证,流程是先从.vdex中去获取,如果获取不到证明没有验证信息缓存,才会走VerifyClass动态验证,那么.vdex是什么时候生成的呢?编译阶段会生成。另外Android Q上动态加载插件已经不走编译了。因此这种情况很有可能是动态加载插件还没来得及编译情况下走的VerifyClass。
首先,绕过VerifyClass这种事儿,系统是不会去干的,只能APP偷偷自己干。依据是只要正规流程编译出来的字节码文件,一般说来类验证基本上不太可能过不了,因此为了避免这种场景的耗时,app会想针对启动这个场景绕过去,后面该验证还验证。但按张绍文的描述:这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时考虑兼容性问题,暂时不建议在ART平台使用。
做法是:
// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;
参考自张绍文的:08 | 启动优化(下):优化启动速度的进阶方法
注意部分
这部分内容是app启动优化需要注意的点,属于细节性的优化点,需要重视,尽量普及落地。
1)SharedPreference优化
不支持跨进程,MODE_MULTI_PROCESS 也没用。跨进程频繁读写可能导致数据损坏或丢失。
初始化的时候会读取 sp 文件,可能导致后续 getXXX() 方法阻塞。建议在Application attachBaseContext初始化,如果项目中使用了MultiDex,可在MultiDex.install()之前或者在multidex执行的这段时间初始化。
提交: commit 过程:同步内存 ,然后当前线程写文件 ,apply 过程:同步内存,然后QueuedWork实现异步写文件,使用apply替换commit。另外一点是尽量减少频繁提交,好是收集信息批处理一次提交。
不要使用SharedPreference存储大文件及存储大量的key和value,它只适合少量数据保存,比如状态保存。
2)启动阶段尽量避免启动子进程
子进程会与主进程产生CPU竞争,此时CPU负载越高,竞争的可能性就越大。
3)其他
包括布局优化、内存优化、安装包瘦身都会给应用启动优化带来正向反馈。
5.2 系统做的优化:
1)调度优化:
针对冷启动的进程,让他的任务尽量被调度到当前能满足负载需求的最好的核来处理。其次可以考虑在应用冷启动过程提升CPU核频率。
2)进程冻结:
应用启动过程,通过freezer的cgroup操作将后台某些进程冻结,减少CPU竞争。
/sys/fs/cgroup/freezer/XXX
冷冻:
将pid echo到子节点,并且设置freezer.state状态为FROZEN。
解冻:
将pid echo到根节点,或设置freezer.state状态为THAWED。
3)预编译:
在合适场景下提前做好应用的主apk包和插件的编译,让下次启动阶段尽量跑机器码而非解释执行。
系统层面针对启动的优化这里就简单介绍三个比较common的方案,点到为止。