一、前言
启动性能是百度App最外围指标之一。用户心愿利用可能及时响应并疾速加载,启动工夫过长的利用不能满足这个冀望,并且可能会令用户悲观,这种蹩脚的体验可能会导致用户在利用商店针对您的利用给出很低的评分,甚至齐全摈弃您的利用。启动性能的优化成为了体验优化中最要害的一环,百度App在此方向继续投入,一直优化,晋升用户体验。
启动性能优化分为概述篇、工具篇、优化篇和防劣化篇,本篇文章次要论述性能优化相干内容,后期已发表文章能够参阅:
百度App 低端机优化-启动性能优化(概述篇)
百度App Android启动性能优化-工具篇
百度App性能优化工具篇-Thor原理及实际
二、优化实践
对启动性能优化的认知,决定了启动性能优化的方向与思路,进而会决定优化的成果。较多开发者对启动过程的认知,来源于Google 开发者文档中有段对启动过程的形容:
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信号处理、窗口的合成解决、帧缓冲区治理等。
有了全局的认知和视线后,咱们就能够站在更高的角度,更加深刻的思考与剖析性能瓶颈,如手机负载合理性、系统资源应用等等,更加全面的思考启动性能的优化形式,达到对启动性能的极致优化。
三、优化落地
百度App的启动性能的优化,大抵分为三局部,惯例优化、根底机制优化和底层技术优化三局部。
3.1 惯例优化
如果是业务倒退初期,业务的疾速迭代较快,此时的优化会绝对简略,极有可能会呈现短时间内,启动速度晋升秒级别的优化成果。启动性能的优化,也是基于对冷启动的了解以及启动工作的梳理,达到疾速优化的指标。可通过性能工具,如前文提过的Trace工具、Thor Hook工具,发现耗时较为突出问题,评估是否可通过提早、异步、删除等形式优化,根据投入产出状况评估工作优先级,达到疾速优化启动性能的目标。
随着启动场景承载业务逐渐宏大,手百逐步成长为承载业务最多,体量微小的航母级挪动端利用,宏大业务的预加载不可能齐全去除或者通过异步来解决,此局部是启动性能优化中面临的较大难题,须要有机制批量解决业务预加载问题,因而根底机制中的调度机制逐步衍生进去,解决启动过程不同业务的预加载需要。
3.2 根底机制优化
根底机制优化次要分为调度机制优化、根底组件性能优化。
3.2.1 任务调度优化
业务多,预加载工作的执行诉求各有不同,均衡启动性能和业务预加载,百度App需建设任务调度框架,业务方通过接入可疾速优化性能问题。
任务调度整体建设状况如下,目前还处在疾速迭代中:
智能调度能够依据工作输出和信息输出,做出不同的调度反馈,如:
1、个性化调度策略:辨认出业务预加载工作ID和用户行为习惯相匹配,则会将工作提前做初始化,工作优先级会做晋升,与此同时,在用户进入业务对应页面时,非业务相干工作需做避让;
2、分级体验策略:辨认出在指定的机型配置中有对应的调度策略,则会执行对应的调度能力,如立刻调度、提早调度、不调度等,次要用于体验降级;
3、精细化调度策略:在不同的场景精细化调度业务预加载工作,如在闪屏场景,会辨认闪屏相干业务信息并做预加载,在端内查起场景,会辨认落地页所属业务信息并做对应预加载;
4、分优先级提早调度:有较大量的工作初始化会依赖于提早调度,需保障有序管制业务初始化,因而在提早调度根底上增加优先级概念,能够在提早调度中也分优先级调度,让更高优先级工作能够更快的执行;
5、首页UI并行渲染调度:次要服务于冷启动阶段商业闪屏业务,商业闪屏是否须要展示、展示哪个物料均是冷启动阶段的实时网络申请决定的,需在冷启动阶段尽量进步商业网络申请的可用工夫,进而进步网络申请成功率,百度App目前能够实现,首页能够先初始化,但不做上屏,待首页渲染业务提交的时候再查看商业闪屏是否展示,做到了提供给商业网络申请更多可用工夫的同时不阻塞首页初始化,通过此项技术大幅晋升商业网络申请的成功率,带来了商业支出的晋升。
因为调度器框架中波及细节十分多,在这里只简略介绍其中一种调度器的设计:分级体验调度器。
次要分为3个模块,机型评分、分级配置和分级调度机制,达到不同配置的手机上的最优体验。
- 机型评分:
- 通过设施信息计算评分信息,称为动态评分;
- 通过性能指标计算评分信息,称为动静评分;
- 根据模型训练评分信息,得出最终机型评分;
- 分级配置:
- 云端配置表:提供各业务级别按设施评分条件下的分级配置表,该表反对动静更新,增量更新,更新后端上及时失效。
- 本地预置表:本地会预置一份配置表,供首次装置时应用;
- 根据机型评分信息和分级配置信息得出控制策略;
- 分级调度:
- 业务方依据机型评分管制不同的业务逻辑,达到高端机全副性能最优体验,中端机局部性能良好体验,低端机外围性能晦涩体验,如首页点赞动画在高端机上抉择开启策略,中端机上抉择提早加载策略,低端机上抉择敞开状态;
3.2.2 KV存储优化
SharedPreferences是Android平台轻量级的存储类,用来保留应用程序的配置信息,其本质是以“键-值”对的形式保留数据的xml文件,其文件保留在/data/data/pkg/shared\_prefs目录下,长处是以键值对的形式进行存储,使用方便,易于了解;但SharedPreferences的毛病很显著,读写性能慢,IO读写应用xml数据格式,全量更新效率低;多过程反对差,存储数据易失落;创立线程多,导致性能差。
读取性能差
每加载一个SP文件均会创立子线程,源码如下:
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();}
然而在获取key-value时如果没有加载实现,则会wait期待SP文件加载实现:
public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; }}
写入性能差
SP采纳XML格局,每次写入是全量更新,效率低,写入提供两种形式:
- 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(); ...... }}
因而,在ANR/卡顿监控中能看到十分多的SharedPreferences堆栈,看堆栈是零碎级堆栈,但其实是SP apply形式引入的问题,堆栈如:
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 优化方案设计
目前各大厂商也对SP做了肯定优化,有激进优化,在SP以后机制根底上做优化,次要是解决写入导致的ANR问题;也有颠覆性优化,比拟有代表性的为MMKV和Data Store,但经评估后,可能均有肯定问题,因而在百度App的优化中,也是学习借鉴业界支流的解决形式,最终采纳两种优化并存的形式:
- 提供颠覆性优化组件:UniKV,彻底解决原生SP一系列问题,外围场景极致体验,业务方被动接入;
- 在零碎SP机制上做优化,解决写入时ANR等痛点问题,次要服务于未接入UniKV的SP文件;
3.2.2.1.1 UniKV设计
层级设计
1: 业务应用时间接依赖UniKV,UniKV继承SharedPreferences,对齐原生SP接口;
2: 工程中蕴含原生实现和UniKV实现,代码中间接依赖原生实现,编译打包时替换为UniKV实现,保障业务中台输入能力;
文件存储格局设计
分位文件头、数据块。文件头40个字节,次要存储版本号、回写次数、保留字段、容灾数据长度、容灾CRC、理论数据长度、理论CRC。
1:以4KB位单位调配空间,最小占用4KB空间,通过mmap映射文件,操作系统负责数据写入文件;
2:通过容灾数据长度和容灾CRC可做数据恢复;
3:通过保留字段可做性能拓展,比方是否从SP迁徙胜利标识;
数据块中存储次要数据体,以append模式写入,必要时再做数据整顿
1:反对类型存储,对齐SP原生getAll接口;
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机制优化
有些SP是在插件、第三方SDK中应用的,因而无奈应用UniKV对立优化,需提供一种优化原生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优化等。
线程优化
通过Hook能力编写插件,发现线程应用不标准问题,制订线程应用标准,如:
1: 业务禁止擅自设置线程优先级;
2: 提供对立的线程池,防止各业务各一个线程池;
3:优先选择线程池/任务调度器调度,业务禁止独自创立线程/线程池;
4: 线程池需防止线程频繁创立,参数标准化。
IO优化
通过Hook能力编写插件,发现不合理IO问题,次要包含:
- 主线程读写工夫超过100ms,主线程读写工夫过长会导致主线程长耗时问题,重大时可能会导致ANR问题;
- 读写buffer过小问题,如果buffer太小,会导致过屡次零碎调用和内存拷贝,read/wirte次数过多,从而影响性能。
SO优化
通过Hook能力编写插件,发现So加载问题,优化不必要的SO加载过程,对于必要的加载,尝试通过异步线程提前策略解决,达到优化性能的目标。
Binder优化
通过Hook能力编写插件,发现Binder通信相干问题,优化不必要Binder通信,必要时可通过内存缓存、文件长久化等形式,达到优化性能的目标。
主线程优先级
主线程的优先级决定了零碎为主线程调配的资源,如果线程优先级有问题,被改成了低优先级,极有可能呈现得不到CPU工夫片导致运行慢的问题。在主线程优先级的问题排查中,最有代表性的是业务在为相干子线程设置优先级时误设置了优先级,出问题形式:
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相干优化等,目前还在摸索一些技术点,此局部优化根本为全局优化,会在后续的晦涩度专题中为大家揭晓。
四、小结
启动性能优化是绝对简单的技术方向,不仅有较多的业务会和启动性能有千头万绪的分割,在启动过程中也有十分多的零碎行为值得关注与投入,目前百度App启动性能已逐步步入瓶颈期,如何突破瓶颈并与业务紧密结合,是启动性能优化的挑战与时机。启动性能的优化是一直学习、一直颠覆、不断进步的过程,两头可能会遇到十分多的挑战,也会呈现十分多的时机,因而,启动性能优化永无止境,任重而道远。
—— END——
参考资料:
1、抖音启动优化
https://heapdump.cn/article/3624814
2、快手TTI 治理教训分享
https://zhuanlan.zhihu.com/p/422859543
3、浅析Android启动优化
https://juejin.cn/post/7183144743411384375
4、MMKV:
https://github.com/Tencent/MMKV/wiki/android\_ipc
举荐浏览:
从php5.6到golang1.19-文库App性能跃迁之路
扫光动效在挪动端利用实际
Android SDK平安加固问题与剖析
搜寻语义模型的大规模量化实际
如何设计一个高效的分布式日志服务平台
视频与图片检索中的多模态语义匹配模型:原理、启发、利用与瞻望