Android应用开发那些事Android技术讨论Android

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

2020-01-11  本文已影响0人  Stan_Z

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 开销,从而达到提升启动性能的目的。

注:

如何获取启动过程牵涉到的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优化
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的方案,点到为止。

上一篇 下一篇

猜你喜欢

热点阅读