查看原文
其他

百度App启动性能优化实践篇

百度Geek说 2023-12-27

The following article is from 百度App技术 Author 龙少


一、前言

GEEK TALK

启动性能是百度App最核心指标之一。用户希望应用能够及时响应并快速加载,启动时间过长的应用不能满足这个期望,并且可能会令用户失望,这种糟糕的体验可能会导致用户在应用商店针对您的应用给出很低的评分,甚至完全抛弃您的应用。启动性能的优化成为了体验优化中最关键的一环,百度App在此方向持续投入,不断优化,提升用户体验。
启动性能优化分为概述篇、工具篇、优化篇和防劣化篇,本篇文章主要阐述性能优化相关内容,前期已发表文章可以参阅:
百度App 低端机优化-启动性能优化(概述篇)
百度App Android启动性能优化-工具篇
百度App性能优化工具篇-Thor原理及实践


二、优化理论

GEEK TALK

对启动性能优化的认知,决定了启动性能优化的方向与思路,进而会决定优化的效果。较多开发者对启动过程的认知,来源于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信号处理、窗口的合成处理、帧缓冲区管理等。

有了全局的认知和视野后,我们就可以站在更高的角度,更加深入的思考与分析性能瓶颈,如手机负载合理性、系统资源使用等等,更加全面的考虑启动性能的优化方式,达到对启动性能的极致优化。


三、优化落地

GEEK TALK

百度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问题,主要包括:
  1. 主线程读写时间超过100ms,主线程读写时间过长会导致主线程长耗时问题,严重时可能会导致ANR问题;

  2. 读写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相关优化等,目前还在探索一些技术点,此部分优化基本为全局优化,会在后续的流畅度专题中为大家揭晓。


四、小结

GEEK TALK

启动性能优化是相对复杂的技术方向,不仅有较多的业务会和启动性能有千丝万缕的联系,在启动过程中也有非常多的系统行为值得关注与投入,目前百度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安全加固问题与分析

搜索语义模型的大规模量化实践

如何设计一个高效的分布式日志服务平台

视频与图片检索中的多模态语义匹配模型:原理、启示、应用与展望



一键三连,好运连连,bug不见👇
继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存