利用启动类型
- 冷启动
- 场景:开机后第一次启动利用 或者 利用被杀死后再次启动
- 生命周期:Process.start->Application 创立 ->attachBaseContext->onCreate->onStart->onResume->Activity 生命周期
- 启动速度:在几种启动类型中最慢,也是咱们优化启动速度最大的拦路虎
- 温启动
- 场景:利用曾经启动,返回键退出
- 生命周期:onCreate->onStart->onResume->Activity 生命周期
- 启动速度:较快
- 热启动
- 场景:Home 键最小化利用
- 生命周期:onResume->Activity 生命周期
- 启动速度:快
从下面的总结能够看出,在利用的启动过程中,冷启动是最慢最耗时的,零碎以及利用自身都有大量的工作须要解决,所以,冷启动对于利用的启动速度是最具挑战以及最有必要进行优化的。
冷启动流程
冷启动指的是应用程序从过程在零碎不存在,到零碎创立利用运行过程空间的过程。冷启动通常会产生在一下两种状况:
- 设施启动以来首次启动应用程序
- 零碎杀死应用程序之后再次启动应用程序
在冷启动的最开始,零碎须要负责做三件事:
- 加载以及启动 app
- app 启动之后立即显示一个空白的预览窗口
- 创立 app 过程
一旦零碎实现创立 app 过程后,app 过程将要接着负责实现上面的工作:
- 创立 Application 对象
- 创立并且启动主线程 ActivityThread
- 创立启动第一个 Activity
- Inflating views
- 布局屏幕
- 执行第一次绘制
一旦 app 过程完实现了第一次绘制工作,零碎过程就会用 main activity 替换后面显示的预览窗口,这个时候,用户就能够正式开始与 app 进行交互了。
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/26/167e9f92508dd4ec~tplv-t2oaga2asx-watermark.image)
从冷启动的流程看,咱们无奈干涉 app 过程创立等零碎操作,咱们可能干涉的有:
- 预览窗口
- Application 生命周期回调
- Activity 生命周期回调
优化剖析测量工具
对研发人员来说,启动速度是咱们的“门面”,它清清楚楚能够被所有人看到,咱们都心愿本人利用的启动速度能够秒杀所有竞争对手。
“工欲善其事必先利其器”,咱们须要先找到一款适宜做启动优化剖析的工具或者形式。
- adb shell am start -W [packageName]/[packageName. AppstartActivity]
在统计 app 启动工夫时,零碎为咱们提供了 adb 命令, 能够输入启动工夫。零碎在绘制实现后,ActivityManagerService 会回调该办法,然而可能不便咱们通过脚本屡次启动测量 TotalTime,比照版本间启动工夫差别。然而统计工夫不如 Systrace 精确。
- 代码埋点
通过代码埋点来精确获取记录每个办法的执行工夫,晓得哪些地方耗时,而后再有针对性地优化。例如通过在 app 启动生命周期中,要害地位退出工夫点记录,达到测量目标;又例如能够在 Application 的 attachBaseContext
办法中记录开始工夫,而后在启动的第一个 Activity 的 onWindowFocusChanged
办法记录完结工夫。
然而从用户点击 app Icon 到 Application 被创立,再到 Activity 的渲染,两头还是有很多步骤的,比方冷启动的过程创立过程,而这个工夫用此版本是没方法统计了,必须得接受这点数据的不准确性。
- Nanoscope
Nanoscope 十分实在,不过临时只反对 Nexus 6 和 x86 模拟器。
- Simpleperf
Simpleperf 的火焰图并不适宜做启动流程剖析。
- TraceView
通过 TraceView 次要能够失去两种数据:单次执行耗时的办法 以及 执行次数多的办法。然而 TraceView 性能耗损太大,不能比拟正确反映真实情况。
- Systrace
Systrace 可能追踪要害零碎调用的耗时状况,如零碎的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。然而不反对利用程序代码的耗时剖析。
综上所述,这几种形式都各有各的长处以及毛病,咱们都要把握。
然而有没有一种比拟折中比拟现实的计划呢?有的。
- “Systrace + 函数插桩”
除了可能看到例如 GC、System Server、CPU 调度等零碎调用的耗时,还可能通过 Android 工程编译的过程中,在指定的办法前后,自动化插入插桩函数,统计办法执行工夫。通过插桩,咱们能够看到利用主线程和其余线程的函数调用流程。它的实现原理非常简单,就是将上面的两个函数 通过用 ASM 框架批改字节码的形式 别离插入到每个办法的入口和进口。
class TraceMethod {public static void i() {Trace.beginSection();
}
public static void o() {Trace.endSection();
}
}
当然这外面有十分多的细节须要思考,比方怎么样升高插桩对性能的影响、哪些函数须要被排除掉。函数插桩后的成果如下:
class Test {public void test() {TraceMethod.i();
// 原来的工作
TraceMethod.o();}
}
只有精确的数据评估能力指引优化的方向,这一步是十分重要的。没有充沛评估或者评估应用了谬误的办法,最终失去了谬误的方向,会导致最初发现基本达不到预期的优化成果。
启动优化办法
在拿到整个启动流程的全景图之后,咱们能够分明地看到这段时间内零碎、利用各个过程和线程的运行状况,当初咱们要开始真正开始“干活”了。
具体的优化形式,我把它们分为 预览窗口优化、业务梳理、业务优化、多过程优化、线程优化、GC 优化和零碎调用优化。业务梳理、业务优化、线程优化、GC 优化、零碎调用优化和布局优化。
预览窗口优化
当用户点击利用桌面图标启动利用的时候,利用提前展现进去的 Window,疾速展现出一个界面,用户只须要很短的工夫就能够看到“预览页”,这种齐全“跟手”的感觉在高端机上体验十分好,但对于中低端机,会把总的的闪屏工夫变得更长。
如果点击图标没有响应,用户主观上会认为是手机零碎响应比较慢。所以比拟举荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览窗口”计划,让手机性能好的用户能够有更好的体验。
要实现预览窗口的显示,只须要在利用 activity 的 windowBackground
主题属性提供一个简略的自定义 drawable 给启动的 activity,如下:
Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list>
Manifest file:
<activity ...
android:theme="@style/AppTheme.Launcher" />
这样一个 activity 启动的时候,就会先显示一个预览窗口,给用户疾速响应的体验。当 activity 想要复原原来 theme,能够通过在调用 super.onCreate()
和setContentView()
之前调用 setTheme(R.style.AppTheme)
,如下:
public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
// ...
}
}
业务梳理
不要一股脑把全副初始化工作放在 Application 中做,须要梳理分明以后启动过程正在运行的每一个模块,哪些是肯定须要的、哪些能够砍掉、哪些能够懒加载。然而须要留神的是,懒加载要避免集中化,否则容易呈现首页显示后用户无奈操作的情景。总的来说,用以下四个维度分整顿启动的各个点:
- 必要且耗时:启动初始化,思考用线程来初始化。
- 必要不耗时:首页绘制。
- 非必要但耗时:数据上报、插件初始化。
- 非必要不耗时:不必想,这块间接去掉,在须要用的时再加载。
把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如下图所示:
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/27/167eb514b1242797~tplv-t2oaga2asx-watermark.image)
一句话概述,要进步利用的启动速度,核心思想是在启动过程中少做事件,越少越好。
业务优化
通过梳理之后,剩下的都是启动过程肯定要用的模块。这个时候,咱们只能硬着头皮去做进一步的优化。优化后期须要“抓大放小”,先看看主线程到底慢在哪里。最现实是通过算法进行优化,例如一个数据解密操作须要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,咱们要思考这些工作是不是能够通过异步线程预加载实现,但须要留神的是过多的线程预加载会让咱们的逻辑变得更加简单。
业务优化做到前面,会发现一些架构和历史包袱会连累咱们后退的步调。比拟常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,局部框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至多几百毫秒。还有一些历史包袱十分惨重,而且“牵一动员全身”,改变危险比拟大。然而我想说,如果有适合的机会,咱们仍然须要怯懦去偿还这些“历史债权”。
多过程优化
Android app 是反对多过程的,在 Manifest 中只有在组件申明中退出 android:process
属性就能够让组件在启动时运行在不同的过程中。举个例子:对于多过程 app,可能领有主过程,插件过程以及下载过程,但开发者只能在 Manifest 中申明一个 Application 组件,如果对应不同过程的组件启动时,零碎会创立三个过程,创立三个 Application 对象,同时 attachBaseContext
、onCreate
等生命周期回调办法也会被调用三次。
然而每个过程须要初始化的内容必定是不一样的,所以,为了避免资源的节约,咱们须要在 Application 中辨别过程,对应过程只初始化对应的内容。
线程优化
线程优化分两方面:
第一,耗时工作异步化。子线程解决耗时工作,主线程做的事件越少,越早进入 Acitivity 绘制阶段,界面越早展示。例如不在主线程做如 IO、网络等耗时操作。然而要留神,子线程不能阻塞主线程。
第二,线程池治理线程,控制线程的数量。线程数量太多会相互竞争 CPU 资源,导致分给主线程的工夫片缩小,从而导致启动速度变慢。线程切换的数据咱们能够通过卡顿优化中学到的 sched 文件查看,这里特地须要留神 nr\_involuntary\_switches 被动切换的次数。
proc/[pid]/sched:
nr_voluntary_switches:被动上下文切换次数,因为线程无奈获取所需资源导致上下文切换,最广泛的是 IO。nr_involuntary_switches:被动上下文切换次数,线程被零碎强制调度导致上下文切换,例如大量线程在抢占 CPU。
第三,防止主线程与子线程之间的锁阻塞期待。有一次咱们把主线程内的一个耗时工作放到线程中并发执行,然而发现这样做基本没起作用。仔细检查后发现线程外部会持有一个锁,主线程很快就有其余工作因为这个锁而期待。通过 Systrace 能够看到锁期待的事件,咱们须要排查这些期待是否能够优化,特地是避免主线程呈现长时间的空转。
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/27/167ee9de9c1293b8~tplv-t2oaga2asx-watermark.image)特地是当初有很多启动框架,会应用 Pipeline 机制,依据业务优先级规定业务初始化机会。比方微信外部应用的 [mmkernel](https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2F6Q818XA5FaHd7jJMFBG60w%3F "https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w?")、阿里最近开源的 [Alpha](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Falibaba%2Falpha "https://github.com/alibaba/alpha") 启动框架,它们为各个工作建设依赖关系,最终形成一个有向无环图。对于能够并发的工作,会通过线程池最大水平晋升启动速度。如果工作的依赖关系没有配置好,很容易呈现下图这种状况,即主线程会始终期待 taskC 完结,空转 2950 毫秒。
第四,设置子线程优先级。不重要工作,设置子线程优先级为 THREAD\_PRIORITY\_BACKGROUND,这样子线程最多能获取到 10% 的工夫片,优先保障主线程执行。
GC 优化
在启动过程,要尽量减少 GC 的次数,防止造成主线程长时间的卡顿,特地是对 Dalvik 来说,咱们能够通过 Systrace 独自查看整个启动过程 GC 的工夫。
启动过程防止进行大量的字符串操作,特地是序列化跟反序列化过程。一些频繁创立的对象,例如网络库和图片库中的 Byte 数组、Buffer 能够复用。如果一些模块切实须要频繁创建对象,能够思考移到 Native 实现。
Java 对象的逃逸也很容易引起 GC 问题,咱们在写代码的时候比拟容易疏忽这个点。咱们应该保障对象生命周期尽量的短,在栈上就进行销毁。
零碎调用优化
局部零碎的 API 应用是阻塞性的,文件很小可能无奈感知,当文件过大,或者应用频繁时,可能造成阻塞。例如:SharedPreference.Editor 的提交操作倡议应用异步的 apply,而不是阻塞的 commit。
通过 systrace 的 System Service 类型,咱们能够看到启动过程 System Server 的 CPU 工作状况。在启动过程,咱们尽量不要做零碎调用,例如 PackageManagerService 操作、Binder 调用期待。
在启动过程也不要过早地拉起利用的其余过程,System Server 和新的过程都会竞争 CPU 资源。特地是零碎内存不足的时候,当咱们拉起一个新的过程,可能会成为“压死骆驼的最初一根稻草”。它可能会触发零碎的 low memorykiller 机制,导致系统杀死和拉起(保活)大量的过程,从而影响前台过程的 CPU。举个例子,之前一个程序在启动过程会拉起下载和视频播放过程,改为按需拉起后,线上启动工夫进步了 3%,对于 1GB 以下的低端机优化,整个启动工夫能够优化 5%~8%,成果还是非常明显的。
布局优化
布局越简单,测量布局绘制的工夫就越长。次要做到以下几点:
- 布局的层级越少,加载速度越快。
- 一个控件的属性越少,解析越快,删除控件中的无用属性。
- 应用 <ViewStub/> 标签加载一些不罕用的布局,做到应用时在加载。
- 应用 <merge/> 标签缩小布局的嵌套档次。
- 尽可能少用 wrap\_content,wrap\_content 会减少布局 measure 时的计算成本,已知宽高为固定值时,不必 wrap\_content。
启动优化进阶办法
还有什么办法能够做进一步优化吗?
数据重排
如果咱们在启动的过程中须要读一个文件 test.io 的 1KB 数据,而咱们的 buffer 不小心写成 1byte,那么总共要读取 1000 次。零碎是否会真的发动 1000 次磁盘 IO 呢?
事实上 1000 次读操作只是咱们发动的次数,并不是真正的磁盘 I/O 次数。你能够参考上面 Linux 文件 I/ O 流程。
Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,个别 block 大小是 4KB。也就是说一次磁盘读写大小至多是 4KB,而后会把 4KB 数据放到页缓存 Page Cache 中。如果下次读取文件数据曾经在页缓存中,那就不会产生实在的磁盘 I/O,而是间接从页缓存中读取,大大晋升了读的速度。所以下面的例子,咱们尽管读了 1000 次,但事实上只会产生一次磁盘 I/O,其余的数据都会在页缓存中失去。
Dex 文件用的到的类和安装包 APK 外面各种资源文件个别都比拟小,然而读取十分频繁。咱们能够利用零碎这个机制将它们依照读取程序重新排列,缩小实在的磁盘 I/O 次数。
在启动优化中,数据的重排次要有两方面:类重排 以及 资源文件重排。
类重排
类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列程序。
不明确能够看这篇文章:Redex 初探与 Interdex:Andorid 冷启动优化
依据 interdex 官网介绍的原理,咱们能够晓得要实现这个优化须要解决三个问题:
- 如何获取启动时加载类的序列?
redex 中的计划是 dump 出程序启动时的 hprof 文件,再从中剖析出加载的类,比拟麻烦。这里咱们采纳的计划是 hook 住 ClassLoader.findClass 办法,在零碎加载类时日志打印出类名,这样剖析日志就能够失去启动时加载的类序列了。
class GetClassLoader extends PathClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将类名 name 记录到文件
writeToFile(name, "coldstart_classes.txt");
return super.findClass(name);
}
}
- 如何把须要的类放到主 dex 中?
redex 的做法应该是解析出所有 dex 中的类,再按配置的加载类序列,从主 dex 开始从新生成各个 dex,所以会打乱原有的 dex 散布。而在手 q 中,分 dex 规定是编译脚本中保护的,因而咱们能够批改分包逻辑,将须要的类放到主 dex。
- 如何调整主 dex 中类的程序?
开源就是好。Android 编译时把.class 转换成.dex 是依附 dx.bat,这个工具理论执行的是 sdk 中的 dx.jar。咱们能够批改 dx 的源码,替换这个 jar 包,就能够执行自定义的 dx 逻辑了。简略说下具体批改办法:
这里须要对 dex 的文件格式做肯定理解,不再细说,网上有一篇很好的文章,有趣味能够理解下 http://blog.csdn.net/jiangwei…
资源文件重排
Facebook 在比拟早的时候就应用“资源热图”来实现资源文件的重排,最近支付宝在 《通过安装包重排布优化 Android 端启动性能》 中也具体讲述了资源重排的原理和落地办法。
类的加载
加载类的过程有一个 verify class 的步骤,它须要须要校验办法的每一个指令,是一个比拟耗时的操作。
verify 步骤能够看这篇文章:微信 Android 热补丁实际演进之路
咱们能够通过 Hook 来去掉 verify 这个步骤,这对启动速度有几十毫秒的优化。不过我想说,其实最大的优化场景在于首次和笼罩装置时。以 Dalvik 平台为例,一个 2MB 的 Dex 失常须要 350 毫秒,将 classVerifyMode 设为 VERIFY\_MODE\_NONE 后,只须要 150 毫秒,节俭超过 50% 的工夫。
// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;
然而 ART 平台要简单很多,Hook 须要兼容几个版本。而且在装置时大部分 Dex 曾经优化好了,去掉 ART 平台的 verify 只会对动静加载的 Dex 带来一些益处。Atlas 中的 dalvik\_hack-3.0.0.5.jar 能够通过上面的办法去掉 verify,然而以后没有反对 ART 平台。
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);
这个黑科技能够大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要思考兼容性问题,临时不倡议在 ART 平台应用。
黑科技
保活
讲到黑科技,你可能第一个想到的就是保活。保活能够缩小 Application 创立跟初始化的工夫,让冷启动变成温启动。不过在 Target 26 之后,保活确实变得越来越难。对于大厂来说,可能须要寻求厂商单干的机会。
插件化和热修复
它们真的那么好吗?事实上大部分的框架在设计上都存在大量的 Hook 和公有 API 调用,带来的毛病次要有两个:
- 稳定性。尽管大家都号称兼容 100% 的机型,因为厂商的兼容性、装置失败、dex2oat 失败等起因,还是会有那么一些代码和资源的异样。Android P 推出的 non-sdk-interface 调用限度,当前适配只会越来越难,老本越来高。
- 性能。Android Runtime 每个版本都有很多的优化,因为插件化和热修复用到的一些黑科技,导致底层 Runtime 的优化咱们是享受不到的。Tinker 框架在加载补丁后,利用启动速度会升高 5%~10%。
总的来说,对于黑科技咱们须要谨慎,当你足够理解它们外部的机制当前,能够选择性的应用。
总结
以上就是自己学习过程中对启动优化相干内容的总结,明天的文章就到这里,感谢您的浏览,喜爱的话不要忘了 三连。大家的反对和认可,是我分享的最大能源。