几乎包含了市面上所有启动优化方案,已拿意向书

注意

在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
onWindowFocusChanged只是首帧时间,App启动完成的结束点应该是真实数据展示出来的时候,如列表第一条数据展示,记得使用getViewTreeObserver().addOnPreDrawListener(),它会把任务延迟到列表显示后再执行。

AOP(Aspect Oriented Programming)打点

面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。

作用

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。

AOP核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理。

2、切面(Aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象。

3、连接点(JoinPoint)

被拦截到的点(方法、字段、构造器)。

4、切入点(PointCut)

对JoinPoint进行拦截的定义。

5、通知(Advice)

拦截到JoinPoint后要执行的代码,分为前置、后置、环绕三种类型。

准备

首先,为了在Android使用AOP埋点需要引入AspectJ,在项目根目录的build.gradle下加入:

classpath ‘com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0’

然后,在app目录下的build.gradle下加入:

apply plugin: ‘android-aspectjx’
implement ‘org.aspectj:aspectjrt:1.8.+’

AOP埋点实战

JoinPoint一般定位在如下位置

1、函数调用
2、获取、设置变量
3、类初始化

使用PointCut对我们指定的连接点进行拦截,通过Advice,就可以拦截到JoinPoint后要执行的代码。Advice通常有以下几种类型:

1、Before:PointCut之前执行
2、After:PointCut之后执行
3、Around:PointCut之前、之后分别执行

首先,我们举一个小栗子:

@Before(“execution(* android.app.Activity.on**(…))”)
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {

}

在execution中的是一个匹配规则,第一个*代表匹配任意的方法返回值,后面的语法代码匹配所有Activity中on开头的方法。

处理Join Point的类型:

1、call:插入在函数体里面
2、execution:插入在函数体外面

如何统计Application中的所有方法耗时?

@Aspect
public class ApplicationAop {

@Around(“call (* com.json.chao.application.BaseApplication.**(…))”)
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}

注意

当Action为Before、After时,方法入参为JoinPoint。
当Action为Around时,方法入参为ProceedingPoint。

Around和Before、After的最大区别:

ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法。

总结AOP特性:

1、无侵入性
2、修改方便

四、启动速度分析工具 — TraceView

使用方式

1、代码中添加:Debug.startMethodTracing()、检测方法、Debug.stopMethodTracing()。(需要使用adb pull将生成的**.trace文件导出到电脑,然后使用Android Studio的Profiler加载)

2、打开Profiler -> CPU -> 点击 Record -> 点击 Stop -> 查看Profiler下方Top Down/Bottom Up 区域找出耗时的热点方法。

Profile CPU

1、Trace types

Trace Java Methods

会记录每个方法的时间、CPU信息。对运行时性能影响较大。

Sample Java Methods

相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁捕获应用的调用堆栈,对运行时性能的影响比较小,能够记录更大的数据区域。

Sample C/C++ Functions

需部署到Android 8.0及以上设备,内部使用simpleperf跟踪应用的native代码,也可以命令行使用simpleperf。

Trace System Calls

检查应用与系统资源的交互情况。
查看所有核心的CPU瓶。
内部采用systrace,也可以使用systrace命令。

2、Event timeline

显示应用程序中在其生命周期中转换不同状态的活动,如用户交互、屏幕旋转事件等。

3、CPU timeline

显示应用程序实时CPU使用率、其它进程实时CPU使用率、应用程序使用的线程总数。

4、Thread activity timeline

列出应用程序进程中的每个线程,并使用了不同的颜色在其时间轴上指示其活动。

绿色:线程处于活动状态或准备好使用CPU。
黄色:线程正等待IO操作。(重要)
灰色:线程正在睡眠,不消耗CPU时间。

Profile提供的检查跟踪数据窗口有四种

1、Call Chart

提供函数跟踪数据的图形表示形式。

水平轴:表示调用的时间段和时间。
垂直轴:显示被调用方。
橙色:系统API。
绿色:应用自有方法
蓝色:第三方API(包括Java API)

提示:右键点击Jump to source跳转至指定函数。

2、Flame Chart

将具有相同调用方顺序的完全相同的方法收集起来。

水平轴:执行每个方法的相对时间量。
垂直轴:显示被调用方。

注意:看顶层的哪个函数占据的宽度最大(平顶),可能存在性能问题。

3、Top Down

递归调用列表,提供self、children、total时间和比率来表示被调用的函数信息。
Flame Chart是Top Down几乎包含了市面上所有启动优化方案,已拿意向书
列表数据的图形化。

4、Bottom Up

展开函数会显示其调用方。
按照消耗CPU时间由多到少的顺序对函数排序。

注意点:

Wall Clock Time:程序执行时间。
Thread Time:CPU执行的时间。

TraceView小结

特点

1、图形的形式展示执行时间、调用栈等。
2、信息全面,包含所有线程。
3、运行时开销严重,整体都会变慢,得出的结果并不真实。

作用

主要做热点分析,得到两种数据:

单次执行最耗时的方法。
执行次数最多的方法。

五、启动速度分析工具 — Systrace

使用方式:

代码插桩

定义Trace静态工厂类,将Trace.begainSection(),Trace.endSection()封装成i、o方法,然后再在想要分析的方法前后进行插桩即可。

在命令行下执行systrace.py脚本:

python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a “com.wanandroid.json.chao” -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html

具体参数含义如下:

-t:指定统计时间为20s。
shced:cpu调度信息。
gfx:图形信息。
view:视图。
wm:窗口管理。
am:活动管理。
app:应用信息。
webview:webview信息。
-a:指定目标应用程序的包名。
-o:生成的systrace.html文件。

如何查看数据?

在UIThread一栏可以看到核心的系统方法时间区域和我们自己使用代码插桩捕获的方法时间区域。

Systrace原理

在系统的一些关键链路(如SystemServcie、虚拟机、Binder驱动)插入一些信息(Label);
通过Label的开始和结束来确定某个核心过程的执行时间;
把这些Label信息收集起来得到系统关键路径的运行时间信息,最后得到整个系统的运行性能信息;
Android Framework里面一些重要的模块都插入了label信息,用户App中可以添加自定义的Lable。

Systrace小结

特性

结合Android内核的数据,生成Html报告。
系统版本越高,Android Framework中添加的系统可用Label就越多,能够支持和分析的系统模块也就越多。
必须手动缩小范围,会帮助你加速收敛问题的分析过程,进而快速地定位和解决问题。

作用

主要用于分析绘制性能方面的问题。
分析系统关键方法和应用方法耗时。

六、启动监控

1、实验室监控:视频录制

80%绘制
图像识别

注意

覆盖高中低端机型不同的场景。

2、线上监控

需要准确地统计启动耗时。

1、启动结束的统计时机

是否是使用界面显示且用户真正可以操作的时间作为启动结束时间。

2、启动时间扣除逻辑

闪屏、广告和新手引导这些时间都应该从启动时间里扣除。

3、启动排除逻辑

Broadcast、Server拉起,启动过程进入后台都需要排除统计。

4、使用什么指标来衡量启动速度的快慢?

平均启动时间的问题
一些体验很差的用户很可能被平均了。

建议的指标

1、快开慢开比

如2s快开比,5s慢开比,可以看到有多少比例的用户体验好,多少比例的用户比较糟糕。

2、90%用户的启动时间

如果90%用户的启动时间都小于5s,那么90%区间的启动耗时就是5s。

5、启动的类型有哪几种?

首次安装启动
覆盖安装启动
冷启动(指标)
热启动(反映程序的活跃或保活能力)

借鉴Facebook的profilo工具原理,对启动整个流程的耗时监控,在后台对不同的版本做自动化对比,监控新版本是否有新增耗时的函数。

七、启动优化常规方案

启动过程中的常见问题

点击图标很久都不响应:预览窗口被禁用或设置为透明。
首页显示太慢:初始化任务太多。
首页显示后无法进行操作:太多延迟初始化任务占用主线程CPU时间片。

优化区域

Application、Activity创建以及回调等过程。

常规方案,省略了一些常规方案细节,感兴趣可以查看原文。

1、主题切换

2、第三方库懒加载

3、异步初始化

4、延迟初始化

5、Multidex预加载优化

6、预加载SharedPreferences

7、类预加载优化

在Application中提前异步加载初始化耗时较长的类。

如何找到耗时较长的类?

替换系统的ClassLoader,打印类加载的时间,按需选取需要异步加载的类。

注意:

Class.forName()只加载类本身及其静态变量的引用类。
new 类实例 可以额外加载类成员变量的引用类。

8、WebView启动优化

1、WebView首次创建比较耗时,需要预先创建WebView提前将其内核初始化。
2、使用WebView缓存池,用到WebView的时候都从缓存池中拿,注意内存泄漏问题。
3、本地离线包,即预置静态页面资源。

9、页面数据预加载

在主页空闲时,将其它页面的数据加载好保存到内存或数据库,等到打开该页面时,判断已经预加载过,就直接从内存或数据库取数据并显示。

10、启动阶段不启动子进程

子进程会共享CPU资源,导致主进程CPU紧张。此外,在多进程情况下一定要可以在onCreate中去区分进程做一些初始化工作。

注意启动顺序:

App onCreate之前是ContentProvider初始化。

11、闪屏页与主页的绘制优化

1、布局优化。
2、过渡绘制优化。

关于绘制优化可以参考Android性能优化之绘制优化。

八、启动优化黑科技

1、启动阶段抑制GC

启动时CG抑制,允许堆一直增长,直到手动或OOM停止GC抑制。(空间换时间)

前提条件

1、设备厂商没有加密内存中的Dalvik库文件。
2、设备厂商没有改动Google的Dalvik源码。

实现原理

在源码级别找到抑制GC的修改方法,例如改变跳转分支。
在二进制代码里找到 A 分支条件跳转的”指令指纹”,以及用于改变分支的二进制代码,假设为 override_A。
应用启动后扫描内存中的 libdvm.so,根据”指令指纹”定位到修改位置,然后用 override_A 覆盖。

缺点

白名单覆盖所有设备,但维护成本高。

2、CPU锁频

在Android系统中,CPU相关的信息存储在/sys/devices/system/cpu目录的文件中,通过对该目录下的特定文件进行写值,实现对CPU频率等状态信息的更改。

缺点

暴力拉伸CPU频率,导致耗电量增加。

3、数据重排

Dex文件用到的类和APK里面各种资源文件都比较小,读取频繁,且磁盘地址分布范围比较广。我们可以利用Linux文件IO流程中的page cache机制将它们按照读取顺序重新排列在一起,以减少真实的磁盘IO次数。

1、类重排

使用Facebook的ReDex的Interdex调整类在Dex中的排列顺序。

2、资源文件重排

最佳方案是修改内核源码,实现统计、度量、自动化。
其次可以使用Hook框架进行统计得出资源加载顺序列表。
最后,调整apk文件列表需要修改7zip源码以支持传入文件列表顺序。

技术视野:

所谓的创新,不一定是要创造前所未有的东西,也可以将已有的方案移植到新的平台,并结合该平台的特性落地,就是一个很大的创新。
当我们足够熟悉底层的知识时,可以利用系统的特性去做更加深层次的优化。

4、类加载优化(Dalvik)

1、类预加载原理

对象第一次创建的时候,JVM首先检查对应的Class对象是否已经加载。如果没有加载,JVM会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。

2、类加载优化过程

在Dalvik VM加载类的时候会有一个类校验过程,它需要校验方法的每一个指令。
通过Hook去掉verify步骤 -> 几十ms的优化
最大优化场景在于首次安装和覆盖安装时,在Dalvik平台上,一个2MB的Dex正常需要350ms,将classVerifyMode设为VERIFY_MODE_NONE后,只需150ms,节省超过50%时间。

ART比较复杂,Hook需要兼容几个版本。而且在安装时,大部分Dex已经优化好了,去掉ART平台的verify只会对动态加载的Dex带来一些好处。所以暂时不建议在ART平台使用。

九、总结

1、优化总方针

异步、延迟、懒加载
技术、业务相结合
创建的时候,JVM首先检查对应的Class对象是否已经加载。如果没有加载,JVM会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。

2、类加载优化过程

在Dalvik VM加载类的时候会有一个类校验过程,它需要校验方法的每一个指令。
通过Hook去掉verify步骤 -> 几十ms的优化
最大优化场景在于首次安装和覆盖安装时,在Dalvik平台上,一个2MB的Dex正常需要350ms,将classVerifyMode设为VERIFY_MODE_NONE后,只需150ms,节省超过50%时间。

ART比较复杂,Hook需要兼容几个版本。而且在安装时,大部分Dex已经优化好了,去掉ART平台的verify只会对动态加载的Dex带来一些好处。所以暂时不建议在ART平台使用。

九、总结

1、优化总方针

异步、延迟、懒加载
技术、业务相结合

上一篇:性能调优利器:火焰图


下一篇:golang源码阅读:VictoriaMetrics中的协程优先级的处理方式