客户端有效耗时优化方法

有变量输入的系统,就会持续往混沌状态发展。我们只有对抗熵增才能变得有序。

耗时优化是性能优化的一种,通过优化能给业务带来流畅的体验。下面我将分享我工作中用到的优化方法。这里我认为最有价值的部分在有效性怎么去衡量,及操作方法。

怎么做

我们觉得某个场景慢,是主观感受上的。我们没法在不熟悉代码或模块工作机理的情况下就能知道是哪里的问题。那么可以这样做,我画了一张图。

那么什么叫体验糟糕,个人认为是:

发烫

这三种描述都属于表象,它们对应到程序上的原因很杂,有诸多的原因会导致,这里我列举出部分我们开发过程中常见的原因,除此之外Android官方也给出了 常见的卡顿来源指导参考。

如何观测

通过上图,我们了解到一个App程序在运行过程中的性能问题的复杂性,那么我们有什么办法可以观察吗?
我们需要找到一个突破口,或者说是监事方法。这里我认为最主要的就是函数执行耗时(方法),至于硬件和系统层面不在我们能干涉的范围,这里不做展开。

好,函数耗时观测而言,我们有哪些手段呢?

本地调试

Android-Profiler

Android谷歌官方已经给我们提供了很好用的分析利器,Profiler工具,可以选择运行时调试生成trace记录,并通过在Android Studio中进行可视化分析,详情可以去官网进一步查看,

Debug MethodTracing

通过借助Profiler工具,我们还可以利用Debug类中提供的方法对某个执行片段进行抽样分析。它的好处是我们不会被应用整体的复杂性干扰,只对某块代码或函数调用进行分析。写法类似:

1
2
3
Debug.startMethodTracing("sample")
// 要观察的耗时代码或函数在中间
Debug.stopMethodTracing()

最终会在/sdcard/data/「包名」/files/xxx.trace路径下生成记录。我们要做的仅仅是把它导入到cpu-profiler中进行分析。

除了官方提供的工具,还有一些第三方的分析工具。原理大部分是通过APM插桩来实现,这里不做展开了,个人认为官方的已经很香了。能解决大部分本地可观察的问题够用。

大盘监控

反映真实情况

除了在开发调试阶段做优化,我们还需要关注到线上实际在用户手中的体验情况。主要原因是本地调试的设备和环境比较局限,即使是研发、测试、产品都体验不错任然是偏主观的。这时我们可以采取对线上的模块函数耗时进行监控。

指标可量化

如果满分是100分,从50分做到90分,可以很直观的通过录屏看到前后效果。但如果我们要知道从90分做到91分、95分怎么衡量?这里有一些基本的套路玩法。

1.可观察改造:通过对客户端代码的架构的调整,利用分治思想。把任务(函数)按业务拆分,颗粒度越精细越好。通过在框架层面设计进行手工(或自动)插桩,记录任务的耗时。

2.采集数据:通过对打点的数据进行收集,上报到数据仓库,这里需注意的是,如果业务的频率高、生产太快将会影响到后续报表的流计算的吞吐量,可以采取采样策略(比如圈出20%的用户)。

3.定基准线:假设我们这个业务模块有十个任务,测试单台设备累计耗时500ms。这个只是单台设备的。在第2步我们已经采集到了全面的数据,可以统计出采样用户的平均耗时公式:单次耗时累计 / 总次数,但一般我们不通过平均数来看,而是以分位数求正态分布情况,参考 hive percentile函数。这时统计出来会出现下面的情况。


比如20分位,它代表20%的用户的体验不会超过120ms,90分位代表90%的用户不超过500ms。假设我们以满足90%的用户有良好体验作为基准,但又不确定这里的500ms是优秀、良好、一般、糟糕?

我们可以这样做,找到10~90分对应的设备,或通过模拟耗时刻意延迟执行。找到权威人员(产品、交互设计老师等)体验进行判断。

我们发现大多数权威人员认为至少要达到60分的标准才能接受。这说明将会有40%的用户的体验都很糟糕,那么我们应该把90分位的基准线定为300ms。这种方法是非常贴近线上情况的,因为本身分位已经反映了正态分布的情况了。

4.优化任务:此前我们在第1步已经对不同的任务进行了拆分,并通过框架层面监控到了异常的任务节点。我们可以通过Debug MethodTracing 拿到trace记录进行分析优化。就像下面的磁盘访问耗时。

优化手段

业务是慢慢变复杂的。一开始业务简单不会受关注。在复杂后操刀会很痛苦。这类技术债偿还起来往往不轻松。

  • 1.盘点现状,按模块整理
  • 2.画出依赖关系图,确认优先级
  • 3.自顶向下设计
  • 4.找到可优化的点
  • 5.持续监控与优化,调整基准线,扩展影响因子

任务颗粒

绝大多数情况的不知道卡和慢在哪个环节是因为任务没有被拆分清晰。我认为极致的优化的前提,必然是先要整体架构清晰、业务清晰。再是监控切割的力度要细。对后续从监控报表上来看才会变得有意义。否则只知道整体性能糟糕,不知道具体在什么位置。

并行任务

串行改并行,复杂业务分治。利用有向无环图类设计,让有依赖的任务可以关联执行。尽力去压榨设备的性能。

类似框架可参考:

对于不可并发的任务,通常在产品体验上是存在优先级的。就像上面图中,任务2、3、4必须在任务1后。这种情况是肯定有的。那么我们只需要保证2、3、4对于1来说是串行。这样可以节约大量时间。

预加载

耗时任务提前做,可能还没到现场就要做。比如我们知道绝大多数用户打开我们的App,60%的情况下都是使用A页面。在A页面初始化过程中需要加载某个模块。而这个模块是耗时比页面打开要慢不少。我们就可以考虑在更早的时机将它初始化。比如在App启动时,放置在后台线程中。或者App进入相对空闲状态时,触发预判性任务加载。已达到A页面业务秒开的效果。不止用于模块加载,常见的场景还有:核心接口、缓存KV、插件、特效动画、皮肤资源等等。

懒加载

同预加载正好相反的是,我们知道有些业务不核心、优先级低、启用频率低。那么它就没有必要去同App竞争某个时刻上的CPU、内存、网络资源。这样做不仅可以提高App性能,同时还可以降低服务压力。举个例子:
App中通常都有皮肤特效,多套不同的皮肤对应不同的资源包。我们把额外的皮肤资源也都放在App启动时去下载。这时App收到了一条推送,拉活唤醒一大批用户。这个时候拉取皮肤资源的接口必然要面对一波压力,同时这里的资源下载也会消耗带宽,不仅用户要为此付钱,文件服务也要。

如何防劣化

除了前面提到的分位值以外,我们还可以监控的指标和维度还可以有很多,我们把这种影响结果的因素叫影响因子。更多时候做分析是需按叠加筛选条件来看。

防裂是个综合问题,我们总会发现一段时间不去关注或治理。容易从下面几个方面冒出问题。

客户端代码

即便我们当前已经做到了教优解的调教,也无法阻止产品去改需求,无法控制团队中其他同学因为各种原因不得不调整某些跟我们优化的部分相关的代码。他们必然会直接或间接的影响。比如:

  • 1.基础库升级
  • 2.更高优的任务,跑在了我们的任务前面

对于这种情况,我们可以在数据报表上选择筛查Debug包类型的数据,甚至可以把开发分支都加上标签,同时留意线上各个版本的变化。一旦在开发阶段就能发现的问题,及时通过告警通知到业务负责人,把问题扼杀在发版前。

业务链路

如果我们的模块不是很单一,对外部有依赖。很多时候很难知道在整个链路上谁会搞什么幺蛾子,比如:

  • 1.上游发起了一次技术重构,而我们完全不知道这件事。
  • 2.因为某些问题,业务链路中某个服务比较慢,导致拖慢业务层某个接口的响应。
  • 新业务在业务接口上进行了扩展,后来的业务变复杂了。
  • …..

这类问题一般只能在上线后案后才能被发现。而且回溯问题的成本非常高。这很考研当前业务负责同学对业务&技术全貌的的了解。并且需要相当敏感度。否则就是拉着一大帮人到处找问题,最后可能某一两个环节有关。

总结

做性能优化,在代码和技术方案都已经符合基本的有序性、抽象化设计后,如果还要持续压榨,我目前做过的项目基本上都在采用比较风骚非常规的手段在实现。这种做法短期收益上见效会很好。往往是牺牲了可读、可维护、可扩展的特性,打破常规的设计思路,甚至会增加线上出问题的风险。

当新人阅读这部分代码的时候会感到非常晦涩,弄不懂为什么要这样做,可能最后就会骂出,这谁写的沙雕代码,感觉好乱好复杂,明明这么简单的逻辑,瞎搞。

这需要我们对技术方案进行宣讲留档,正所谓攻城容易守城难。

随缘打赏!