Android性能优化盘点 - 启动优化

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

simpleperf官方使用介绍

工具如何使用不铺开说了,网上相关文章也非常多。

四、启动流程

既然是做启动优化,那么相关的代码流程也务必需要全局了解,之前针对启动流程进行过详细梳理,可以参考之前文章,这里就不赘述了:

应用启动流程梳理(一)-应用安装流程
应用启动流程梳理(二)-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, 一个没做:

无VerifyClass
有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的方案,点到为止。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345