一.启动类型
冷启动
指进程死亡的情况下,从点击应用图标到UI界面完全显示且用户可操作的全部过程。
大致流程:
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
用户点击桌面图标,这个点击事件它会触发一个IPC的操作,之后便会执行到Process的start方法中,这个方法是用于进程创建的,接着,便会执行到ActivityThread的main方法,这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。
热启动
即进程存活情况下,点击桌面图标,应用从后台切换到前台
二.如何检测启动耗时
1.查看Logcat
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
2.adb shell
使用adb shell获取应用的启动时间
// 其中的AppstartActivity全路径可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]
执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:
ThisTime
表示最后一个Activity启动耗时。
TotalTime
表示所有Activity启动耗时。
WaitTime
表示AMS启动Activity的总耗时。
一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
特点:
1、线下使用方便,不能带到线上。
2、非严谨、精确时间。
3.AOP(Aspect Oriented Programming) 打点
具体AOP可以自行上网查找文章
下面以统计统计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类型其对应的方法入参是不同的,具体的差异如下所示:
当Action为Before、After时,方法入参为JoinPoint。
当Action为Around时,方法入参为ProceedingPoint。
Around和Before、After的最大区别:
ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法。
4.使用TraceView
这个的使用参考我以前写的文章 《Android性能优化系列之App启动优化》
三.启动优化进阶方法
启动优化一些常用的方法参考《Android性能优化系列之App启动优化》,这里不再赘述,这里讲一些进阶的方法
1.定制一套APP启动框架
常见的启动优化,我们会将一些sdk或者模块的初始化进行并发的进行,但这些工作之间可能存在前后依赖的关系,所以我们又需要想办法保证他们执行顺序的正确性,所以需要通过启动框架,为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。
目前开源的启动框架有:
阿里的alpha:https://github.com/alibaba/alpha
美团的AppInit: https://github.com/laohong/AppInit
具体原理,感兴趣的可以check源码来看
2.I/O 优化
SharedPreference 在初始化的时候还是要全部数据一起解析。如果它的数据量超过
1000 条,启动过程解析时间可能就超过 100 毫秒。如果只解析启动过程用到的数据项则会很大程度减少解析时间,启动过程适合使用随机读写的数据结构。
解决方式:可以将 ArrayMap 改造成支持随机读写、延时解析的数据存储方式。具体实现后续将出文章讲解。
3.数据重排
Linux 文件 I/O 流程
Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,一般 block 大小是4KB。也就是说一次磁盘读写大小至少是 4KB,然后会把 4KB 数据放到页缓存 Page Cache 中。如果下次读取文件数据已经在页缓存中,那就不会发生真实的磁盘 I/O,而是直接从页缓存中读取,大大提升了读的速度。所以上面的例子,我们虽然读了 1000 次,但事实上只会发生一次磁盘 I/O,其他的数据都会在页缓存中得到。
Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数。
类重排
启动过程类加载顺序可以通过复写 ClassLoader 得到。
class GetClassLoader extends PathClassLoader
{
public Class<?> findClass(String name) { // 将 name 记录到文件 writeToFile(name,"coldstart_classes.txt");
return super.findClass(name);
}
}
具体实现可以参考 ReDex 的Interdex,调整类在 Dex 中的排列顺序,可以利用 010 Editor 查看修改后的效果。
资源文件重排
修改 Kernel 源码,单独编译一个特殊的 ROM。这样做的目的
有三个:
1)统计。统计应用启动过程加载了安装包中哪些资源文件,比如 assets、drawable、layout 等。跟类重排一样,我们可以得到一个资源加载的顺序列表。
2)度量。在完成资源顺序重排后,我们需要确定是否真正生效。比如有哪些资源文件加载了,它是发生真实的磁盘 I/O,还是命中了 Page Cache。
3)自动化。任何代码提交都有可能改变启动过程中类和资源的加载顺序,如果完全依靠人工手动处理,这个事情很难持续下去。通过定制 ROM 的一些埋点和配合的工具,我们可以将它们放到自动化流程当中。
事实上如果仅仅为了统计,我们也可以使用 Hook 的方式。下面是利用 Frida 实现获得Android 资源加载顺序的方法
resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){
send('file:'+a)
return this.loadXmlResourceParser(a,b,c,d)
}
resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){
send("file:"+a)
return this.loadDrawableForCookie(a,b,c,d,e)
}
调整安装包文件排列需要修改 7zip 源码实现支持传入文件列表顺序,同样最后可以利用010 Editor 查看修改后的效果。
类的加载
在加载类的过程有一个 verify class 的步骤,它需要校验方法的每一个指令,是一个比较耗时的操作。
我们可以通过 Hook 来去掉 verify 这个步骤,这对启动速度有几十毫秒的优化。其实最大的优化场景在于首次和覆盖安装时。以 Dalvik 平台为例,一个 2MB 的 Dex
正常需要 350 毫秒,将 classVerifyMode 设为 VERIFY_MODE_NONE 后,只需要 150毫秒,节省超过 50% 的时间。
但是 ART 平台要复杂很多,Hook 需要兼容几个版本。而且在安装时大部分 Dex 已经优化好了,去掉 ART 平台的 verify 只会对动态加载的 Dex 带来一些好处。Atlas 中的dalvik_hack可以通过下面的方法去掉 verify,但是当前没有支持 ART 平台。
这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要考虑兼容性问题,暂时不建议在 ART 平台使用。
最后附上redex地址:https://github.com/facebook/redex
启动阶段抑制GC
启动时CG抑制,允许堆一直增长,直到手动或OOM停止GC抑制。(空间换时间)
前提条件
1、设备厂商没有加密内存中的Dalvik库文件。
2、设备厂商没有改动Google的Dalvik源码。
实现原理
1、首先,在源码级别找到抑制GC的修改方法,例如改变跳转分支。
2、然后,在二进制代码里找到 A 分支条件跳转的"指令指纹",以及用于改变分支的二进制代码,假设为 override_A。
3、最后,应用启动后扫描内存中的 libdvm.so,根据"指令指纹"定位到修改位置,并使用 override_A 覆盖。
缺点
需要白名单覆盖所有设备,但维护成本高。
5.0 以下Multidex预加载优化
安装或者升级后首次 MultiDex 花费的时间过于漫长,我们需要进行Multidex的预加载优化。
优化步骤
1、启动时单独开一个进程去异步进行Multidex的第一次加载,即Dex提取和Dexopt操作。
2、此时,主进程Application进入while循环,不断检测Multidex操作是否完成。
3、执行到Multidex时,则已经发现提取并优化好了Dex,直接执行。MultiDex执行完之后主进程Application继续执行ContentProvider初始化和Application的onCreate方法。
注意
5.0以上默认使用ART,在安装时已将Class.dex转换为oat文件了,无需优化,所以应判断只有在主进程及SDK 5.0以下才进行Multidex的预加载。