一.前言
起步性能是百度App最核心指标之一。用户期盼应用能够即时响应并快速加载,起步时间过长的应用不可满足这个期望,并且可能会令用户失望,这种糟糕的体验可能会引起用户在应用商店针对您的应用给出很低的评分,乃至完全抛弃您的应用。起步性能的优化作为了体验优化中最重要的一环,百度App这里方向连续投入,不断优化,提高用户体验。
起步性能优化分为概述篇、工具篇、优化篇和防劣化篇,本篇文案重点阐述性能优化关联内容,前期已发布文案能够参阅: 百度App 低端机优化-起步性能优化(概述篇):https://mp.weixin.qq.com/s/aomafRByyponPBiI79OoHQ 百度App Android起步性能优化-工具篇:https://mp.weixin.qq.com/s/kyTjEQutXQ7oqzN0htTOiA 百度App性能优化工具篇-Thor原理及实践:
https://mp.weixin.qq.com/s/kKRNpQ0UNP2m0_vOS6D8HA
二. 优化理论
对起步性能优化的认知,决定了起步性能优化的方向与思路,从而会决定优化的效果。较多研发者对起步过程的认知,源自于Google 研发者文档中有段对起步过程的描述:
创建应用对象;
起步主线程;
创建主 activity;
扩充视图;
布局屏幕;
执行初始绘制。
一旦应用进程完成第1次绘制,系统进程就会换掉当前表示的后台窗口,替换为主 activity。此时,用户能够起始运用应用。
上面重点介绍了应用在起步过程中的各个周期,但其实只是大致概括,其实起步方式会比较多,极有可能在区别的起步路径执行的规律有差异,因此呢全路径的认知在优化过程中起到了非常要紧的功效,如下图所示:
在起步过程中,点击桌面图标是主流冷起步方式,而Push调起,浏览器调起等端外转化亦是比较平常的调起方式,各样起步方式的起步过程基本可拆解为:进程创建、框架加载、首页渲染、预加载四个环节。而起步性能优化重点面对的不只是点击桌面图标这一种路径,更加多的必须起步全路径的优化,达到体验的极致优化。
起步过程亦必须结合系统层面来理解,从而挖掘优化点,探索优化的极限。起步过程是非常复杂的过程,必须较多系统级进程协同才可完成页面的展现,供用户正常运用,下图展示的点击icon的起步过程:
起步过程大概可概括为:
Launcher通告AMS起步APP的主Activity;
ActivityManagerService(以下简叫作AMS)记录要起步的Activity信息,并且通告Launcher进入pause状态;
Launcher进入pause状态后,通告AMS已然paused了,起始起步App;
App未开启过,AMS起步新的进程,并且在新进程中创建ActivityThread对象,执行其中的main函数办法;
App主线程起步完毕后通告AMS,并传入applicationThread以便通讯;
AMS通告App绑定Application并起步MainActivity;
起步MainActivitiy,并且创建和相关Context,最后调用onCreate办法,最后完成页面绘制和上屏; 重点进程的功能重点是:
Launcher进程:为手机桌面进程,负责接收用户的点击事件,并将事件通告到AMS
SystemServer进程:负责应用的起步流程调度、进程的创建和管理、窗口的创建和管理(StartingWindow 和 AppWindow) 等,比较核心的服务有AMS和WMS(WindowManagerService);
Zygote进程:经过fork创建应用程序进程,Zygote进程在初始化时就会会创建虚拟机,同期把必须的系统类库和资源文件加载到内存中。而Zygote在fork出子进程后,这个子进程亦会得到一个已然加载好基本资源的虚拟机,从而加速应用进程的起步;
SurfaceFlinger进程:重点和应用的渲染关联,如Vsync信号处理、窗口的合成处理、帧缓冲区管理等。
有了全局的认知和视野后,咱们就能够站在更高的方向,更加深入的思考与分析性能瓶颈,如手机负载恰当性、系统资源运用等等,更加全面的思虑起步性能的优化方式,达到对起步性能的极致优化。
三. 优化落地
百度App的起步性能的优化,大致分为三部分,常规优化、基本机制优化和底层技术优化三部分。
丨3.1 常规优化
倘若是业务发展初期,业务的快速迭代较快,此时的优化会相对简单,极有可能会显现短期内,起步速度提高秒级别的优化效果。起步性能的优化,亦是基于对冷起步的理解以及起步任务的梳理,达到快速优化的目的。可经过性能工具,如前文提过的Trace工具、Thor Hook工具,发掘耗时较为明显问题,评定是不是可经过延迟、异步、删除等方式优化,依据投入产出状况评定工作优先级,达到快速优化起步性能的目的。
随着起步场景承载业务逐步庞大,手百逐步成长为承载业务最多,体量巨大的航母级移动端应用,庞大业务的预加载不可能完全去除或经过异步来处理,此部分是起步性能优化中面临的很强困难,必须有机制批量处理业务预加载问题,因此呢基本机制中的调度机制逐步衍生出来,处理起步过程区别业务的预加载需求。
丨3.2 基本机制优化
基本机制优化重点分为调度机制优化、基本组件性能优化。
丨3.2.1 任务调度优化
业务多,预加载任务的执行诉求各有区别,平衡起步性能和业务预加载,百度App需建设任务调度框架,业务方经过接入可快速优化性能问题。
任务调度整体建设状况如下,日前还处在快速迭代中:
智能调度能够按照任务输入和信息输入,做出区别的调度反应,如:
个性化调度策略:识别出业务预加载任务ID和用户行径习惯相匹配,则会将任务提前做初始化,任务优先级会做提高,与此同期,在用户进入业务对应页面时,非业务关联任务需做避让;
分级体验策略:识别出在指定的机型配置中有对应的调度策略,则会执行对应的调度能力,如立即调度、延迟调度、不调度等,重点用于体验降级;
精细化调度策略:在区别的场景精细化调度业务预加载任务,如在闪屏场景,会识别闪屏关联业务信息并做预加载,在端外调起场景,会识别落地页所属业务信息并做对应预加载;
分优先级延迟调度:有很强量的任务初始化会依赖于延迟调度,需保证有序掌控业务初始化,因此呢在延迟调度基本上添加优先级概念,能够在延迟调度中亦分优先级调度,让更高优先级任务能够更快的执行;
首页UI并行渲染调度:重点服务于冷起步周期商场闪屏业务,商场闪屏是不是必须展现、展现哪个物料均是冷起步周期的实时网络请求决定的,需在冷起步周期尽可能加强商场网络请求的可用时间,从而加强网络请求成功率,百度App日前能够实现,首页能够先初始化,但不做上屏,待首页渲染业务提交的时候再检测商场闪屏是不是展现,做到了供给给商场网络请求更加多可用时间的同期不阻塞首页初始化,经过此项技术大幅提高商场网络请求的成功率,带来了商场收入的提高。
因为调度器框架中触及细节非常多,在这儿只简单介绍其中一种调度器的设计:分级体验调度器。
重点分为3个模块,机型评分、分级配置和分级调度机制,达到区别配置的手机上的最优体验。
机型评分:
经过设备信息计算评分信息,叫作为静态评分;
经过性能指标计算评分信息,叫作为动态评分;
依据模型训练评分信息,得出最后机型评分;
分级配置:
云端配置表:供给各业务级别按设备评分要求下的分级配置表,该表支持动态更新,增量更新,更新后端上即时生效。
本地预置表:本地会预置一份配置表,供首次安装时运用;
依据机型评分信息和分级配置信息得出掌控策略;
分级调度:
业务方按照机型评分掌控区别的业务规律,达到高端机所有功能最优体验,中端机部分功能良好体验,低端机核心功能流畅体验,如首页点赞动画在高端机上选取开启策略,中端机上选取延迟加载策略,低端机上选取关闭状态;
丨3.2.2 KV存储优化
SharedPreferences是Android平台轻量级的存储类,用来保留应用程序的配置信息,其本质是以“键-值”对的方式保留数据的xml文件,其文件保留在/data/data/p公斤/shared_prefs目录下,优点是以键值对的方式进行存储,运用方便,易于理解;但SharedPreferences的缺点很显著,读写性能慢,IO读写运用xml数据格式,全量更新效率低;多进程支持差,存储数据易丢失;创建线程多,引起性能差。
读取性能差 每加载一个SP文件均会创建子线程,源码如下:private finalObject 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 StringdefValue) { 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, intconfigChanges, 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机制的方法。
优化方法:
Android版本
ANR类型
ANR原由
性能平台ANR数据
优化思路 Android<8.0writenToDiskLatch.wait
writenToDiskLatch.wait等待子线程写入完成
Top 3,ANR占比:4.7%
动态代理QueueWork的sPendingWorkFinishers,使poll返回null
Android>=
8.0
writenToDiskLatch.wait
writenToDiskLatch.wait等待子线程写入完成
Top 15,ANR占比:0.5%
动态代理QueueWork的sFinishers,使poll返回null
processPendingWork
main线程已得到sProcessLock锁,在main正在执行写磁盘任务writeToFile,但耗时过大
Top 11,ANR占比:0.8%
代理sWork的clone函数都返回空队列;经过反射获取QueuedWork的mHandler的Looper对象,创建一个新的Hander,并将sWork中的任务提交到这个Handler去执行,从而实现了没阻塞运行
processPendingWork
main线程等在了获取 sProcessLock锁,子线程得到了sProcessLock锁在执行写磁盘任务,main等待时间过长
Top 7,ANR占比:1.8%
代理sWork的clone函数都返回空队列;经过反射获取QueuedWork的mHandler的Looper对象,创建一个新的Hander,并将sWork中的任务提交到这个Handler去执行,从而实现了没阻塞运行
日前百度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的离奇陷阱—设置线程优先级引起的微X卡顿惨案: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执行时长过长。
丨2.3 底层机制优化
重点经过探索底层的技术,来实现优化性能指标,从而撬动业务价值的目的,此方向危害性相对较高,成本亦会很强,需依据详细人力状况及优化效果做最后决策。
百度App中日前已尝试过的有VerifyClass优化、CPU Booster优化、GC关联优化等,日前还在探索有些技术点,此部分优化基本为全局优化,会在后续的流畅度专题中为大众揭晓。
小结
起步性能优化是相对繁杂的技术方向,不仅有较多的业务会和起步性能有千丝万缕的联系,在起步过程中亦有非常多的系统行径值得关注与投入,日前百度App起步性能已逐步步入瓶颈期,怎样打破瓶颈并与业务紧密结合,是起步性能优化的挑战与机遇。起步性能的优化是持续学习、持续颠覆、持续进步的过程,中间可能会遇到非常多的挑战,亦会显现非常多的机遇,因此呢,起步性能优化永没止境,任重而道远。
参考资料: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
|