百度App启动性能优化实践篇
The following article is from 百度App技术 Author 龙少
一、前言
GEEK TALK
二、优化理论
GEEK TALK
1、创建应用对象;
2、启动主线程;
3、创建主 activity;
4、扩充视图;
5、布局屏幕;
6、执行初始绘制。
一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 activity。此时,用户可以开始使用应用。
上面主要介绍了应用在启动过程中的各个阶段,但其实只是大致概括,其实启动方式会比较多,极有可能在不同的启动路径执行的逻辑有差异,因此全路径的认知在优化过程中起到了非常重要的作用,如下图所示:
在启动过程中,点击桌面图标是主流冷启动方式,而Push调起,浏览器调起等端外转化也是比较常见的调起方式,各种启动方式的启动过程基本可拆解为:进程创建、框架加载、首页渲染、预加载四个环节。而启动性能优化主要面对的不只是点击桌面图标这一种路径,更多的需要启动全路径的优化,达到体验的极致优化。
启动过程也需要结合系统层面来理解,进而挖掘优化点,探索优化的极限。启动过程是非常复杂的过程,需要较多系统级进程配合才能完成页面的展现,供用户正常使用,下图展示的点击icon的启动过程:
1、Launcher通知AMS启动APP的主Activity;
2、ActivityManagerService(以下简称AMS)记录要启动的Activity信息,并且通知Launcher进入pause状态;
3、Launcher进入pause状态后,通知AMS已经paused了,开始启动App;
4、App未开启过,AMS启动新的进程,并且在新进程中创建ActivityThread对象,执行其中的main函数方法;
5、App主线程启动完毕后通知AMS,并传入applicationThread以便通讯;
6、AMS通知App绑定Application并启动MainActivity;
7、启动MainActivitiy,并且创建和关联Context,最后调用onCreate方法,最终完成页面绘制和上屏。
1、Launcher进程:为手机桌面进程,负责接收用户的点击事件,并将事件通知到AMS
2、SystemServer进程:负责应用的启动流程调度、进程的创建和管理、窗口的创建和管理(StartingWindow 和 AppWindow) 等,比较核心的服务有AMS和WMS(WindowManagerService);
3、Zygote进程:通过fork创建应用程序进程,Zygote进程在初始化时就会会创建虚拟机,同时把需要的系统类库和资源文件加载到内存中。而Zygote在fork出子进程后,这个子进程也会得到一个已经加载好基础资源的虚拟机,从而加速应用进程的启动;
4、SurfaceFlinger进程:主要和应用的渲染相关,如Vsync信号处理、窗口的合成处理、帧缓冲区管理等。
有了全局的认知和视野后,我们就可以站在更高的角度,更加深入的思考与分析性能瓶颈,如手机负载合理性、系统资源使用等等,更加全面的考虑启动性能的优化方式,达到对启动性能的极致优化。
三、优化落地
GEEK TALK
百度App的启动性能的优化,大致分为三部分,常规优化、基础机制优化和底层技术优化三部分。
3.1 常规优化
如果是业务发展初期,业务的快速迭代较快,此时的优化会相对简单,极有可能会出现短时间内,启动速度提升秒级别的优化效果。启动性能的优化,也是基于对冷启动的理解以及启动任务的梳理,达到快速优化的目标。可通过性能工具,如前文提过的Trace工具、Thor Hook工具,发现耗时较为突出问题,评估是否可通过延迟、异步、删除等方式优化,依据投入产出情况评估工作优先级,达到快速优化启动性能的目的。
随着启动场景承载业务逐步庞大,手百逐渐成长为承载业务最多,体量巨大的航母级移动端应用,庞大业务的预加载不可能完全去除或者通过异步来解决,此部分是启动性能优化中面临的较大难题,需要有机制批量解决业务预加载问题,因此基础机制中的调度机制逐渐衍生出来,处理启动过程不同业务的预加载需求。
3.2 基础机制优化
基础机制优化主要分为调度机制优化、基础组件性能优化。
3.2.1 任务调度优化
业务多,预加载任务的执行诉求各有不同,平衡启动性能和业务预加载,百度App需建设任务调度框架,业务方通过接入可快速优化性能问题。
任务调度整体建设情况如下,目前还处在快速迭代中:
智能调度可以根据任务输入和信息输入,做出不同的调度反应,如:
1、个性化调度策略:识别出业务预加载任务ID和用户行为习惯相匹配,则会将任务提前做初始化,任务优先级会做提升,与此同时,在用户进入业务对应页面时,非业务相关任务需做避让;
2、分级体验策略:识别出在指定的机型配置中有对应的调度策略,则会执行对应的调度能力,如立即调度、延迟调度、不调度等,主要用于体验降级;
3、精细化调度策略:在不同的场景精细化调度业务预加载任务,如在闪屏场景,会识别闪屏相关业务信息并做预加载,在端外调起场景,会识别落地页所属业务信息并做对应预加载;
4、分优先级延迟调度:有较大量的任务初始化会依赖于延迟调度,需保障有序控制业务初始化,因此在延迟调度基础上添加优先级概念,可以在延迟调度中也分优先级调度,让更高优先级任务可以更快的执行;
5、首页UI并行渲染调度:主要服务于冷启动阶段商业闪屏业务,商业闪屏是否需要展现、展现哪个物料均是冷启动阶段的实时网络请求决定的,需在冷启动阶段尽量提高商业网络请求的可用时间,进而提高网络请求成功率,百度App目前可以实现,首页可以先初始化,但不做上屏,待首页渲染业务提交的时候再检查商业闪屏是否展现,做到了提供给商业网络请求更多可用时间的同时不阻塞首页初始化,通过此项技术大幅提升商业网络请求的成功率,带来了商业收入的提升。
由于调度器框架中涉及细节非常多,在这里只简单介绍其中一种调度器的设计:分级体验调度器。
机型评分:
通过设备信息计算评分信息,称为静态评分;
通过性能指标计算评分信息,称为动态评分;
依据模型训练评分信息,得出最终机型评分;
分级配置:
云端配置表:提供各业务级别按设备评分条件下的分级配置表,该表支持动态更新,增量更新,更新后端上及时生效。
本地预置表:本地会预置一份配置表,供首次安装时使用;
依据机型评分信息和分级配置信息得出控制策略;
分级调度:
业务方根据机型评分控制不同的业务逻辑,达到高端机全部功能最优体验,中端机部分功能良好体验,低端机核心功能流畅体验,如首页点赞动画在高端机上选择开启策略,中端机上选择延迟加载策略,低端机上选择关闭状态;
3.2.2 KV存储优化
SharedPreferences是Android平台轻量级的存储类,用来保存应用程序的配置信息,其本质是以“键-值”对的方式保存数据的xml文件,其文件保存在/data/data/pkg/shared_prefs目录下,优点是以键值对的方式进行存储,使用方便,易于理解;但SharedPreferences的缺点很明显,读写性能慢,IO读写使用xml数据格式,全量更新效率低;多进程支持差,存储数据易丢失;创建线程多,导致性能差。
读取性能差
private final Object mLock = new Object();
private boolean mLoaded = false;
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
写入性能差
commit:阻塞当前线程方式,修改提交到内存后,等待IO完成,如果主线程使用commit方式,极有可能出现卡顿;
apply:不阻塞当前线程,但也有隐藏的坑,可能会导致主线程的卡顿问题,主要原因为apply方式将写入Runnable加入到QueueWork中,而在Android 四大组件生命周期轮转时,会检查QueueWork是否完成,如果没有完成则会wait,代码如:
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
......
// 确保写任务都已经完成
QueuedWork.waitToFinish();
......
}
}
java.lang.Object.wait(Native Method)
java.lang.Thread.parkFor$(Thread.java: )
sun.misc.Unsafe.park(Unsafe.java: )
java.util.concurrent.locks.LockSupport.park(LockSupport.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java: )
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java: )
java.util.concurrent.CountDownLatch.await(CountDownLatch.java: )
android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java: )
android.app.QueuedWork.waitToFinish(QueuedWork.java: )
android.app.ActivityThread.handleServiceArgs(ActivityThread.java: )
android.app.ActivityThread. - wrap21(ActivityThread.java)
android.app.ActivityThread$H.handleMessage(ActivityThread.java: )
android.os.Handler.dispatchMessage(Handler.java: )
ndroid.os.Looper.loop(Looper.java: )
ndroid.app.ActivityThread.main(ActivityThread.java: )
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java: )
com.android.internal.os.ZygoteInit.main(ZygoteInit.java: )
多进程支持差
当使用MODE_MULTI_PROCESS这个字段时,其实并不可靠,因为Android内部并没有合适的机制去防止多个进程所造成的冲突,应用不应该使用它,推荐使用ContentProvider。上面这段介绍我们得知:多个进程访问{MODE_MULTI_PROCESS}标识的SharedPreferences时,会造成冲突,举个例子就是,在A进程,明明set了一个key进去,跳到B进程去取,却提示null的错误。
3.2.2.1 优化方案设计
提供颠覆性优化组件:UniKV,彻底解决原生SP一系列问题,核心场景极致体验,业务方主动接入;
在系统SP机制上做优化,解决写入时ANR等痛点问题,主要服务于未接入UniKV的SP文件;
3.2.2.1.1 UniKV设计
层级设计
2: 工程中包含原生实现和UniKV实现,代码中直接依赖原生实现,编译打包时替换为UniKV实现,保证业务中台输出能力;
文件存储格式设计
分位文件头、数据块。文件头40个字节,主要存储版本号、回写次数、保留字段、容灾数据长度、容灾CRC、实际数据长度、实际CRC。
3:通过保留字段可做功能拓展,比如是否从SP迁移成功标识;
数据块中存储主要数据体,以append形式写入,必要时再做数据整理
2:支持类型有:BOOL、INT、FLOAT、DOUBLE、SHORT、LONG、STRING、STRING_ARRAY、BYTE_ARRAY9种类型,相比于原生SP实现支持类型更多;
数据迁移
数据迁移过程需要先读取SP内容,再写入KV文件,耗时会较久,写入完成后KV文件才可用,这在线上会有隐患,需要解决。
UniKV中数据迁移采用不影响业务方式,如果迁移完成,则会直接使用KV文件,如果未迁移完成,则继续使用SP文件,并将数据迁移Runnable提交至线程池。为避免数据迁移期间SP文件出现改动导致数据丢失,注册SP文件更改的数据监听。迁移完成标记位由保留字段来存储,往往数据迁移时需要标记位来保存是否迁移完成的Flag,需要引入其他文件来保存,此处UniKV里的保留字段很好的解决了此问题。
多进程实现
采用mmap机制 + 自定义文件锁实现进程间数据同步,mmap文件至每个进程的内存空间,自定义文件锁主要实现的递归锁和锁的升降级,多进程读时共享锁,多进程写时排他锁,原生文件锁不支持递归锁,升降级容易死锁或锁会被完全释放,因此自定义文件锁实现进程间数据同步。关于多进程这块实现,主要学习了MMKV的多进程实现逻辑,感兴趣的可以参阅:https://github.com/Tencent/MMKV/wiki/android_ipc
实现效果
彻底解决原生SP的性能问题,读写性能显著提高,支持多进程读写,减少线程创建,整体性能指标和业务指标均出现了明显优化。
3.2.2.1.2 系统SP机制优化
优化方案:
目前百度App 在Android 12上暂未优化,主要原因是Android 12实现方式有变化,代理方式相对复杂,且开销较大,而SP引起的ANR问题较少,因此暂未上线优化。
优化效果:
此方案对全局均有优化,除了ANR指标有显著下降外,DAU和留存也出现正向。有同学会担心优化后数据写入是否会受影响,我们通过打点监控到SP写入及时性没有明显变化,而写入成功率出了正向,低端机提升明显,说明SP优化减少ANR的发生,更多任务被执行,写入成功率提升。
3.2.3 锁优化
多线程性能调优是性能优化中不可避免的话题,为了实现线程同步,加入了同步锁机制(Synchronized同步锁、Lock同步锁等),同步锁的诞生虽然保证了操作的原子性、线程的安全性,但是(相比不加锁的情况下)造成了程序性能下降。所以,我们这里要做的一件事就是“锁优化”,即既要保证实现锁的功能(即保证多线程下操作安全)又要提高程序性能(即不要让程序因为安全而损失太大效率)。
常见的锁优化方式:
下面以一个优化项,介绍百度App在锁优化中的实际优化落地。
在项目开展初期,通过Trace工具分析发现有较多的“monitor contentation XXX”,此部分信息是Android ART虚拟机输出的锁相关信息,其中会包括持有锁的线程、方法、等锁线程、等锁方法。具体如下图所示:
经分析,主要是基础组件的AB在初始化时由于synchronized关键字不正确使用导致,需对AB做性能优化,必要时做架构升级。而经过分析,AB基础组件在多线程、文件IO性能均存在性能问题,因此对AB基础组件做了重构升级,彻底解决性能问题。
通过优化后,读写采用无锁实现,彻底解决业务使用ABTest组件锁同步问题;兼容新老AB数据,缓存实验开关和实验sid数据,并采用JSON/PB数据格式存储,首次读取性能118ms,优化95%(小米5机器)。
3.2.4 其他基础机制优化
在百度App的启动性能优化中,开展过较多的基础机制相关优化,如:线程优化、IO优化、SO优化、主线程优先级优化、ContentProvider优化、类/图片预加载优化、图片预上传GPU优化等。
线程优化
2: 提供统一的线程池,避免各业务各一个线程池;
4: 线程池需避免线程频繁创建,参数标准化。
IO优化
主线程读写时间超过100ms,主线程读写时间过长会导致主线程长耗时问题,严重时可能会导致ANR问题;
读写buffer过小问题,如果buffer太小,会导致过多次系统调用和内存拷贝,read/wirte次数过多,从而影响性能。
SO优化
通过Hook能力编写插件,发现So加载问题,优化不必要的SO加载过程,对于必要的加载,尝试通过异步线程提前策略解决,达到优化性能的目的。
Binder优化
通过Hook能力编写插件,发现Binder通信相关问题,优化不必要Binder通信,必要时可通过内存缓存、文件持久化等方式,达到优化性能的目的。
主线程优先级
Thread t = new Thread();
t.start();
t.setPriority(3);
Android的离奇陷阱—设置线程优先级导致的微信卡顿惨案:
https://mp.weixin.qq.com/s/oLz_F7zhUN6-b-KaI8CMRw
在百度App排查优先级设置时,原生库也有更改线程优先级逻辑,也需主动修正,如facebook react库的部分逻辑:
ContentProvider/FileProvider优化
在Application.attachbaseContext和Application.onCreate之间,会执行installContentProviders方法,在此方法中会执行AndroidManifest中声明的ContentProvider/FileProvider,一般耗时较大的为FileProvider,主要原因是FileProvider初始化时有IO操作。主要优化为将ContentProvder/FileProvider移除,并通过android:process属性做控制,或者通过懒加载方式,必要进程中初始化。
图片prepareToDraw优化
在Trace工具有会看到RenderThread中执行syncFrameState时会upload XXX Texture相关耗时问题,首先检查在trace里面显示的图片的宽和高,确保图片的大小不比它显示出来的区域大太多。也可以通过prepareToDraw方法提前触发Bitmap上传GPU操作,这种方式可以使Bitmap在RenderThread空闲的时候提前完成。理想情况下,图片加载库会帮助你完成这些;如果你想要自己掌控图片加载,或者需要确保不在绘制的时候触发Bitmap上传,可以直接在代码里面调用 prepareToDraw。
可能有的同学比较疑惑,此优化没有优化主线程,会对启动性能有优化吗?答案是可以优化主线程,在启动的前几帧,每一帧耗时均会比较大,而每一帧的任务在RenderThread中以DrawFrame Task运行,如果上一帧的任务没有完成,则会阻塞当前帧的绘制,主线程中体现出来的就是draw过程变慢,如nSyncAndDrawFrame执行时长过长。
3.3 底层机制优化
主要通过探索底层的技术,来实现优化性能指标,进而撬动业务价值的目标,此方向风险性相对较高,成本也会较大,需依据具体人力情况及优化效果做最终决策。
百度App中目前已尝试过的有VerifyClass优化、CPU Booster优化、GC相关优化等,目前还在探索一些技术点,此部分优化基本为全局优化,会在后续的流畅度专题中为大家揭晓。
四、小结
GEEK TALK
启动性能优化是相对复杂的技术方向,不仅有较多的业务会和启动性能有千丝万缕的联系,在启动过程中也有非常多的系统行为值得关注与投入,目前百度App启动性能已逐渐步入瓶颈期,如何打破瓶颈并与业务紧密结合,是启动性能优化的挑战与机遇。启动性能的优化是不断学习、不断颠覆、不断进步的过程,中间可能会遇到非常多的挑战,也会出现非常多的机遇,因此,启动性能优化永无止境,任重而道远。
END
参考资料:
https://heapdump.cn/article/3624814
https://zhuanlan.zhihu.com/p/422859543
https://juejin.cn/post/7183144743411384375
https://github.com/Tencent/MMKV/wiki/android_ipc
推荐阅读: