全局视角下的APP性能优化经验

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

作为主端的研发在对飞书这一款大型APP做性能优化时,相比与仅仅对某一个业务,或者是针对一些中小型APP做性能优化需要考虑更多的东西,也会有一些新的方案,所以我在这一次分享中会介绍基于全局的视角是如何对飞书进行性能优化的,也希望能给大家在做性能优化上带来一些新的思路和启发。

1.为什么要基于全局视角?

1.1 大型和小型APP做性能优化的异同点

1. 相同点

中小型APP和大型APP的性能优化的本质和维度上都是相同的。

  • 本质:合理且充分的使用硬件资源,让程序表现的更好
  • 维度 :基于本质出发,从应用层、系统层、硬件层三个维度设计体系化的优化方案

2. 不同点

中小型APP 大型APP
不同点 资源充足,优化时主要是考虑如何将资源充分发挥出来中小型 app 业务少,这些业务尽量充分的使用更多的预加载逻辑,多线程拆解任务,缓存更多的数据等充分发挥硬件资源的方案,就能让程序体验更好 资源不足,优化时主要考虑的是合理的使用和分配资源大型 APP 业务多且复杂,如果每个业务也去追求自己业务的性能最优,同样是在自己的业务中使用更多的线程,使用更多的缓存,使用更多 cpu 的方式来使自己业务表现更好,那么就会导致 APP 整体的体验急剧劣化。

1.2 飞书做性能优化的难点

  • 业务多

飞书作为一款大型 APP ,他的业务非常的多,有IM,邮箱,日历,小程序,工作台等,各个业务都有自己的体验指标,比如小程序和文档的秒开率,IM消息的上屏速度等,业务为了提升自己的体验指标,采用的方案往往都是尽最大可能的预加载;尽可能多的缓存;更多的线程这三项。比如文档曾经为了提升秒开率,将预加载用到非常病狂的水平,在启动的时候先预加载三个webview,在进到消息界面时把消息里面的文档预渲染出来,在进到云文档tab时,先把第一屏的文档内容预渲染出来等等。不仅文档,还有小程序,邮箱,工作台,IM等等,都曾经为了提升性能而不计一切的使用这三种优化方案。

这样的后果自然就是带来整体的体验劣化,超低端端机上曾经一度卡到无法使用,启动要十几秒,那些拼命竞争资源的业务,实际上在很多机型上体验也没变好,反倒更差了。

  • 业务方不配合

针对飞书做性能优化时,很容易会出现业务不配合的问题,推动业务进行资源消耗优化时,业务团队往往都会说自己的业务很重要,就是需要更多的资源;推动业务去使用更优的一切方案时,比如切入到统一的公共线程池,切换到kotlin时,业务方也会以现在的又不是不能用来推脱;出现性能问题了,找业务方一起排查了,业务方也不愿意配合,大部分情况下会说不是我们的问题......这些问题都加大了针对飞书做性能优化的难度。

在这些难点下,要如何才能做好性能优化呢?这个时候基于全局的视角来做优化就很重要了,在全局的视角下,需要脱离具体的业务,且不偏袒某个业务,来管控业务对资源的使用,合理的分配资源,同时推动业务针对各种资源异常场景做出更优的响应,以此来让程序在整体上体验更好。所以我们的优化方案主要从这三点出发来进行考虑和设计:

  1. 如何管控和分配业务对资源的使用:当你能管控和分配资源时,业务方自然就没法滥用预加载,缓存或者线程了,再将这些资源进行更优的分配,程序的体验自然就好了。
  2. 如何让业务在资源紧张时做出更优的策略: 资源不足在大型APP时经常出现的问题,即使你管控和分配的再好,也会出现这个问题,所以当资源紧张的,让各个业务进行响应来让资源恢复正常,程序的体验就不会劣化太多。
  3. 如何度量业务对资源的消耗:如果仅仅是主端做性能优化,那么能产生的效果还是有限的,只有让所有的业务都能参与进来,性能优化才能真正的做好,所以如何度量各个业务对资源的消耗也是主端要做的事情,有了这个数据,才有了让各个业务团队参与和配合的前提。

2. 飞书的性能优化方案

下面我会讲一讲针对这三个方向,在飞书做性能优化时的落地场景和方案。

2.1 管控业务对资源的使用

飞书曾经在中低端机上的启动场景,打开会话场景,切换tab等场景的体验非常差,很卡很慢,主要原因就是各个业务方疯狂的预加载导致,如果我们一个个的去推动和说服这些业务主动减少他们的预加载任务,这是一件不太可能的任务。所以为了解决针这个问题,我们基于全局的视角,设计了预加载框架,并通过该框架将所有业务方的预加载任务进行管控。下面我们看一下预加载框架是怎么设计的,又是怎么管控和分配资源的。

2.1.1 预加载框架

想要管控业务的预加载任务,我们需要考虑这三个点:

  1. 预加载任务的添加方式

  2. 预加载任务的调度及管理的机制

  3. 预加载管控效果度量

预加载任务的添加方式

为了能简化业务方的接入成本,预加载框架只需要通过单例提供一个 addPreloadTask 方法,业务方传入预加载任务 task 以及一些属性及配置参数即可。将预加载任务添加到预加载框架后,业务方就不需要进行任何其他操作了,是否执行、什么时候执行,都交给预加载框架来管理。但是对于添加进预加载框架的Task是有一些要求的,所以这一块是业务方唯一的接入成本了,对于预加载的Task,必须是要和业务的正常执行解耦的,也就是能做到即使该预加载任务不执行,也不会影响到业务的正常使用,并且业务方的预加载任务需要拆分成粒度最小的 task,这样才能防止某一个预加载任务太重导致框架管控的效果不佳。

预加载任务调度时机

任务的调度包括了执行,中断,抛弃,延迟等策略。预加载框架对于添加进来的 task 如何调度呢?这一块也是预加载框架最复杂的的地方的,里面涉及到了很多的策略,最主要的有这三类策略:

  1. 关键节点调度策略:关键节点有各个生命周期阶段,业务或页面各个阶段等
  2. 性能调度策略:比如判断 cpu 是否忙碌,温度是否过高,内存是否充足等,在资源充足的情况下都可以进行执行,资源紧张的时候进行中断或者延迟。
  3. 用户行为调度策略:结合用户的行为来进行调度,比如该用户是否会使用该业务,如果某一个用户从来不使用这个 app 里的这个功能,那么该业务添加进来的预加载任务进行抛弃,或者是采用端智能的方案来进行更精细化的进行控制

每种调度策略不是单独执行的,我们可以将各种策略整合起来,形成一套完善的调度策略。

效果度量

如果将预加载任务管控进来后,导致了业务方性能指标的下降,业务方肯定是不愿意配合的,所以在设计预加载框架时就需要考虑好如何对效果度量。度量的指标主要有四类:

指标 描述
整体的关键性能指标 比如整体的流畅性,启动速度等,管控后的结果,一定是这些整体的关键性能指标有大幅上涨的。
业务的体验指标 在整体指标优化较大的前提下,业务的性能指标有小幅下降是能够接受的,但是较大的降幅是不能接受的。通过对调度策略进行优化,其实是可以做到保证业务的性能指标不下降,甚至更好的。
业务的度量指标 业务方有很多的预加载任务,但是这些预加载任务是否是有效的呢?所以预加载框架也需要对预加载任务的效果进行评估和度量,这样才是符合全局视角的优化策略的。预加载框架使用了task召回率这一指标,也就是某个task预加载后被使用除以某个task被预加载的概率,对于召回率低的task,我们也会推动业务方进行关闭,或者直接通过预加载框架进行关闭。
调度框架自身效果的评估指标 如预加载成功率,这个成功率是业务在使用时,预加载已经成功了的概率,以及第一第二类的指标,都可以用来评估这个调度框架的好坏。

飞书的实现方案

  • 添加任务

下面是飞书添加调度任务的代码实现:

java 复制代码
// 1. PreloadManager初始化
PreloadManager.init()

 // 2. 添加预加载任务
var taskId = PreloadManager.getInstance().addTask(TaskRequest.build {
    task = XMLPreloadTask()
    triggerConditions = ConditionSet().and(BatteryLevel(50)).or(CPUCondition(CPUState.IDLE))
    interruptConditions = ConditionSet().and(CPUCondition(CPUState.BUSY))
    cleanConditions = ConditionSet().and(MemoryCondition(MemoryState.CRITICAL))
 }.toTask())

TaskRequest中包含了触发条件,中断条件,清除条件,params参数信息等

java 复制代码
data class TaskRequest(
    val task: ITask<*, *>,
    val triggerConditions: ConditionSet?,
    val interruptConditions: ConditionSet?,
    val cleanConditions: ConditionSet?,
    val params: Any?,
    var callback: IGetDataCallback<*>?,
    var delayInMills: Long
) 

当业务方调用addTask添加预加载任务后,PreloadManager就会将这些task放在容器中,然后在恰当的triggerConditions进行触发。

  • 调度时机

调度时间包括执行条件和终端条件,主要如下:

  1. 执行条件
    • 设备状态
      • CPU Idle
      • Battery Level
      • Network Level
      • Device Level
    • 调度节点
      • 冷启首屏
      • 冷启一分钟
      • 应用无交互>30s
      • 页面打开前
      • 首Tab的业务
    • 用户行为
      • Feed列表滚动
      • 会话列表滚动
      • 使用过业务
      • 端智能
  2. 中断条件
    • 降级(TurboMode)
    • 用户持续操作

运行过程中规定的条件时,便会调用onConditionChanged进行触发onConditionChanged会对条件进行过滤,区分是执行条件,中断条件,恢复条件或者是清除条件,来对缓存中的task进行不同的处理

2.2 让业务在资源紧张时做出更优的策略

由于飞书的业务多,很多业务都有自己的定时任务或者后台任务,所以前台业务在使用的过程中经常会面临 cpu 忙碌 、内存不足导致卡顿,响应慢等性能问题。所以在做性能优化时,是需要推动业务方在资源不足时做出降级策略的,推动的时候会面临前面所的业务多和业务方不配合的问题,所以我们便设计了降级框架,来更好的实现该目标。

降级框架的设计需要解决这几个问题:

  1. 性能指标的采集
  2. 降级任务的调度
  3. 降级效果的度量

2.2.1 降级框架

性能指标的采集

想要再资源紧张时让业务做出优化策略,那么对性能指标的采集以及资源紧张的判断就是必不可少的一步。基本的性能指标有 cpu 使用率,温度,Java 内存,机型等,除机型外其他性能指标一般都是以固定的频率进行采集,如 cpu 使用率可以 10s 采集一次,温度可以 30s 采集一次,java 内存可以 1 分钟采集一次,采集的频率需要考虑对性能的影响以及指标的敏感度,比如 cpu 的使用率采集,需要读取 proc/stat 的文件并解析,是有一定性能损耗的,所以我们在采集时,不能太频繁;温度的变化是比较慢的,我们采集的频率也可以长一些。降级框架需要整合这些性能指标的采集,减少各个业务自己采集造成不必要的性能损耗。

当降级框架采集到性能指标,并判断当前资源异常时,通用的做法是通知各个业务,业务收到通知后再进行降级。比如系统的 lowmemorykiller 机制,都是采用通知的方式。

但是在大型 APP 中,仅仅将触发性能阈值的通知给到各个业务方,效果不会太好,因为业务方可能并不会去响应通知,或者是个别业务响应了,但是其他业务不响应,依然效果不佳。无法管控业务是否进行降级,这显然不符合基于全局视角进行性能优化的思路,那么我们依然要像设计降级框架一样的思路,将业务方进行降级的逻辑封装成task,并通过降级框架来进行调度。

降级任务的添加和调度

添加任务:任务的添加始终保持简单减少业务调用成本,所以一般就提供一个全局的addDowngradeTask的方法即可。在业务方添加降级任务时,需要带上业务的名称,这样我们就能清楚的知道,哪些业务有降级处理逻辑,哪些业务没有,对于没有注册的业务,需要专门推动进行降级响应以及 task 的注册。除了业务名称,还需要带上其他的一些参数,如降级的场景,自定义的阈值等。

调度任务:和预加载框架一样,对于注册进来的 task,降级框架的任务调度要考虑清楚怎么进行调度,包括调度的时间和策略等等。

  • 调度时机:调度的时间基本就是资源紧张的时候,但是不同的设备下,资源紧张的判断条件是不一样的。高端机型可能 cpu 的使用率在 70%以上,app 还是流畅的,但是低端机在 50%以上就开始卡顿了,因此不同的机型需要根据经验值或者线上数据设置一个合理的阈值。
  • 调度策略:当当前环境到达某个场景的降级条件时,比如当 cpu 到达了降级阈值时,降级框架便开始执行注册到 cpu 列表中的降级任务,在执行降级任务时,不需要将队列里的 task 全部执行,我们可以分批执行,如果执行到某一批降级 task 时,cpu 恢复到阈值以下了,后面的降级任务就可以不用在执行了,这样就可以基于全局的视角,以部分业务的降级换来体验的提升。

效果度量

和预加载框架一样,降级框架的度量指标依然也是这四类

  • 第一类是整体的关键性能指标

  • 第二类是业务的体验指标

  • 第三类是业务的度量指标:这里的度量只要就是指业务降级逻辑的度量,比如某个业务对cpu的降级,对内存的降级的逻辑是否有效,降级框架在每执行一个降级任务后,都会重新检测一次该性能指标的变化,对于降级后性能指标依然没啥变化的,就说降级效果的差的,对这些降级效果差的,也会推动业务方去进行优化。

  • 第四类就是降级框架效果的评估指标:除了用第一类和第二类指标对降级框架效果进行度量外,我还增加异常持续时长这个指标来进行度量,即当设备发生异常后,比如 cpu 过载,设备发热,内存不足等状态会持续多长时间,这个时长也可以反应用户体验的好坏时长,框架的调度策略和业务的响应策略不断的优化,这个指标就能不断的进行改善。

飞书的实现方案

这里从如何注册降级任务和调度和时机策略来讲解飞书的实现

注册降级任务

vbnet 复制代码
//传入perfType,可以指定监听某一种(温度,内存,cpu等)状态。
DevicePerfManager.registerDevicePerfCallback(IDevicePerfCallback devicePerfCallback, String key, PerfType perfType, IDowngradeValve downgradeValve)
DevicePerfManager.registerDevicePerfCallback(IDevicePerfCallback devicePerfCallback, String key, PerfType perfType, boolean async)
DevicePerfManager.registerDevicePerfCallback(IDevicePerfCallback devicePerfCallback, String key, PerfType perfType, boolean async, IDowngradeValve downgradeValve)

业务方注册的降级任务会存入不同的容器中,当条件触发时,便从容器中选择callback来执行。

调度时机和策略

  1. CPU: 默认10S的频率检测CPU使用率,如果使用率超过25%(pct90)便认为CPU使用异常,低于10%则认为恢复正常

  2. 内存: 每一分钟检测一次 Java

    • 当 Java 内存剩余30M以下时,overMemoryLevel 达到 SEVERE(3) 的程度,并触发内存降级逻辑,这个时候一定要进行内存的深度的降级了,不然马上就要crash了;
    • 当 Java 内存剩余50M以下时,overMemoryLevel 达到 MIDDLE(2) 的程度,并触发内存降级逻辑;
    • 当 Java 内存剩余70M以下时,overMemoryLevel 达到 LIGHT(1) 的程度,并触发内存降级逻辑;
    • 当 Java 内存剩余 70M以上时,overMemoryLevel 恢复NORMAL(0),并通知内存恢复降级逻辑
  3. 温度: 每一分钟检测一次稳定

    • 当电池温度大于 40° 时,overTemperatureLevel 达到 SEVERE(3) 的程度,并触发温度降级;
    • 当电池温度大于38°小于等40°时,overTemperatureLevel 达到 MIDDLE(2) 的程度,并触发温度降级;
    • 当电池温度大于36°小于等38°时,overTemperatureLevel 达到LIGHT(1) 的程度,并触发温度降级。
    • 如果温度恢复到36°以下,overTemperatureLevel 恢复到NORMAL(0) 的程度,并触发温度降级
  4. 逐步降级: 降级框架在进行降级操作时,是通过逐步降级的方式来实现的,降级框架会先降级一个或者一批任务,如果达到了效果,就不再继续降级,如果没达到效果,则接着降级。降级的顺序是有一个综合的评估的,比如业务的优先级,降级逻辑的注册顺序,降级任务的效果等进行综合的判断。

2.3度量业务对资源的消耗

如何度量每个业务对资源的消耗,是在做任何优化方案的设计时,都必须要考虑的,在预加载框架和降级框架里面,实际上也是加入了对业务的度量的,比如预加载的召回率度量,业务降级的效果度量。我接着在以内存优化,讲一讲如何度量业务对资源的消耗助。

有效的推动各个业务方去进行内存优化,是所有内存优化方案中ROI最高的一种方案。想要实现改方案就必须要能明确的知道各个业务对内存的消耗量。当 app 运行过程中,往往只能获得整体的内存的数据占用,没法获的各个业务消耗了多少内存的,因为各个业务的数据都是放在同一个堆中的,对于小型 app 来说这种情况并不是问题,因为就那么几个业务在使用内存,但是对于大型 app 来说就是一个问题了,当内存不足时,由于业务太多了,你不知道没法清楚的知道是哪些业务消耗了内存。

虽然我们通过线下的hprof 文件或者其他调试的方式来弄清楚每个 app 的内存占用,但是很多时候没有充足的时间在版本都去统计一下,或者即使统计了,也可能因为路径没覆盖全导致数据不准确。所以我们最好能通过线上监控的方式,就能统计到业务的内存消耗,并且在内存消耗异常的时候进行上报。

2.3.1 以Acitvity为纬度进行度量

由于大部分的业务都是以 activity 呈现的,所以我们可以监听全局的 activity 创建,在业务的 onCreate 最前面统计一下 java 和 native 内存的大小,作为这个业务启动时的基准内存。然后在 acitvity 运行过程中,固定采集在当前 activity 下的内存并减去 onCreate 时的基准内存,我们就能度量出当前业务的一个内存消耗情况了。在该 acitvity 结束后,我们可以主动触发一下 gc,然后在和前面的基准内存 diff 一下,也能统计出该业务结束后的增量内存,理想情况下,增量内存应该是要小于零的,由于 gc 需要 cpu 资源,所以我们只需要开取小部分的采样率即可。

当我们能在运行过程中,统计各个业务的内存消耗,那么就可以推动内存消耗高的业务进行优化,或者当某个版本的某个业务出现较大的劣化时,触发报警等。

除了上面提到的思路,我们也可以统计在业务使用过程中的触顶次数,计算出一个触顶率的指标,触顶及 java 内存占用达到一个阈值,比如 80%,我们就可以认为触顶了,对于触顶次数高的业务,同样也可以进行异常上报,然后推动业务方进行修改。这些数据和指标的统计,都是无侵入的,所以并不需要我们了解业务的细节。

  • 飞书的实现方案
1. 通过ActivityLifecycleCallbacks监听Activity的创建和销毁
2. 在Activity start和destoryed的统计内存数据,包括场景名,java内存的增量,native内存的增量,GC次数等
3. 方案扩展,除了内存外,还包括CPU使用率,温度,流量等都是通过同样的方案来对业务进行消耗度量的。当这些数据采集到后,会定期的推动TOP10或者超过阈值的业务去进行优化。

2.3.2 以对象为纬度进行度量

除了Activity纬度的,还可以基于对象纬度,对象对内存的消耗主要就有两块,一是大图片,而是大对象,因此可以通过字节码插桩,Hook对大图片,大集合的创建,发现后进行堆栈上报,也能有效的降低内存的消耗。

  • 飞书的实现方案
1. Hook Bitmap的创建,对于超过阈值的Bitmap进行异常上报,在低端机上进行强制兜底,也就是强制降低质量的操作
2. Hook 集合的添加接口,对于size超过阈值的集合,抛出异常

2.3.3 推动优化

当有效的度量出了各业务对资源的消耗后,接下来就是推动业务进行优化了。想要有效的推动业务进行优化,我总结出的方式有这些:

  1. 第一种方式是要站在业务的角度去思考,并选择能给业务带来收益的指标去进行推动。比如在内存优化上,我首选的指标是GC率,因为GC触发时,是会导致体验问题的,所以GC率能降低,那么业务的体验就能提升。通过让业务降级GC率,业务方能愿意去做的,当GC率下降了,那么业务对内存资源的消耗自然就下降。

  2. 第二种是用更高优先级和更紧急的因素去推动业务方进行优化,比如飞书有一段时间的优先战略是出海,特别是东南亚地区,而这一地区的低端机很多,所以为了能让飞书在低端机上流畅运行,降低内存消耗是很重要的一件事,此时在去推动业务方进行内存优化时,就会容易很多,但是推动的时候。还比如稳定性是最重要的指标之一,稳定性的一个指标就是OOM率,以OOM率的优化去推动业务方进行内存的优化,也是会比直接让业务方去做内存优化容易很多的。

2.4 优化小结

2.4.1 收益

飞书总共有接近100个预加载任务全部收敛到了预加载框架中,重资源消耗的业务如小程序,视频会议,IM等场景也全部都接入到了降级框架中。低端机上启动速度的pct90从15秒降到了6秒,核心业务的帧率,tab切换速度都有50%以上的优化。

启动速度
Fps pct99
Tab 切换耗时

2.4.2 其他优化

主端研发在对飞书进行性能优化的时候,除了上面提到的速度及流畅性,内存方向的优化外,还包括其他方向,如包体积,稳定性,功耗等等,基本都是基于管控业务对资源的使用;度量业务对资源的消耗;让业务在资源紧张时做出更优的策略这三个方向去进行优化的。

当然我这里讲的优化思路并不是针对飞书做性能优化的全部,除了站在全局视角下的优化方案,主端也会深入了解业务,基于业务逻辑去做分析和优化,比如分析 trace,优化业务策略,或者是基于系统层、硬件层的特性去做一些优化等等,从结果来看,基于全局视角并从本文中提到的三个方向去设计的优化方案获得的收益是最高的。

相关推荐
无尽的大道2 小时前
Java反射原理及其性能优化
jvm·性能优化
58沈剑6 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
萌面小侠Plus6 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
想进大厂的小王8 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
阿伟*rui9 小时前
认识微服务,微服务的拆分,服务治理(nacos注册中心,远程调用)
微服务·架构·firefox
ZHOU西口10 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
人工智能培训咨询叶梓10 小时前
探索开放资源上指令微调语言模型的现状
人工智能·语言模型·自然语言处理·性能优化·调优·大模型微调·指令微调
deephub12 小时前
Tokenformer:基于参数标记化的高效可扩展Transformer架构
人工智能·python·深度学习·架构·transformer
CodeToGym12 小时前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
无尽的大道13 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化