这是我参与「第四届青训营 」笔记创作活动的的第8天
为什么要做性能优化?
- 性能优化带来体验的改善
- 进而帮助业务指标的提升
-
另外分享一个想法,从硬件的范围看 1980到现在 x86 偏pc 服务端的处理器单核性能性能提升的,早期飞快,到最近缓慢 再看arm平台,最近比intel好很多,两方面的因素
-
arm持续稳定的迭代架构,引入了很多优化,提升 二级缓存,流水线上 乱序执行,多发射硬件的分支预设单元
-
还有半导体工艺提升,台积电,我们可以看到迭代,架构和工艺的双重提升最近,再看曲线比之前斜率放缓
-
-
从更长的时间范围来看:多核带来的提升取决于可以真正并行执行的部分(受限于阿姆达尔定律)
多核性能的提升,受限于并行任务之间需要去串行执行或同步执行的时间占比。
-
未来? 未来会有新的材料和工艺来驱动芯片性能的进一步提升,但是目前还没有成熟
移动处理器还受到电池技术的限制
软件的性能优化仍可持续带来提升
性能优化是什么?
目标
快:启动的速度,响应速度等等;
稳:闪退,异常等等;
省:存储空间,功耗等等
分类
| 流畅性优化 | 快-极致的响应与流畅的体验 |
| 资源优化 | 省-最小负载带来最大的收益 |
| 稳定性优化 | 稳-稳定的实现,减少不必要打断 |
| 系统级优化 | 拓展-底层booster |
1. 流畅性优化
首先需要了解Android的线程结构
| 事件 | 影响因素 |
|---|---|
| System Events | 关闭某些页面通知系统,相应回调会通知系统事件 |
| Input Events | 如我们的手机屏幕,点击滑动等操作 |
| Application | 启动全局SDK等等,我们内部处理,肯定越早越好 |
| Services | 去执行那些不需要和用户交互而且还要求长期运行的任务 |
| Alarm | 定时事件(Timer),比如说消息轮训,倒计时等 |
界面是如何刷新的?
为什么16ms会发出一次VSYNC信号触发对UI进行渲染?
这是因为大多数的Android显示屏幕是以每秒60帧来刷新的(也就是60Hz)。
一帧可以看做是一张的独立图片,
60帧每秒就意味着:16ms=1000/60Hz,相当于60fps。这就是上面说的16ms
卡顿感知如何产生?
-
原因:你的主线程代码中,存在很重的操作,影响了输入事件的响应,输入事件无法及时响应 表现:滑不动的情况。
-
原因:输入事件操作过度,一些事件分发,什么的都在拿到触摸事件之后操作,
表现:滑一下卡一下
-
最开始提到的分析的诸多因素在主线中
表现:在观看短视频的时候,没有任何操作,突然卡顿了一下
如何解决卡顿?
将一些耗时的操作,放到其他线程去操作。
扩展:
问题1:人的肉眼能感知到不卡顿的最低帧数是多少?
A. 25 B.30 C.40 D.60
答案:选A(来源于抖音内部测试)
问题2:如果没有vsync信号会有什么问题
A.画面撕裂(一半显示A,一半显示B)
B.画面抖动(绘制很多个,卡一下,又绘制很多个)
C.画面丢帧(绘制了一段,突然没有响应,然后跳到新的东西继续渲染)
D.画面闪烁(页面一直在闪烁)
答案:A画面撕裂
简单示意图如下
核心点在与数据交换的时机由谁来控制,数据交换的发生点应该是在屏幕渲染完一帧后,而不是cpu写入一帧数据后,
所以,控制数据是否交换应该由屏幕来决定,但是计算机五大组成部分各司其职,
屏幕只是输出设备和输入设备(因为能触屏),
他不是控制器,如何控制数据的交换呢? 答案就是:VSYNC
2. 资源优化
什么是资源?
资源:即Android手机的软件和硬件资源
通俗意义上应用依效的移动终端的有限资源和系统设置的数值,即功耗、存烤、流量、系统参数、CPU、内存等
Android端能做哪些资源类优化?
先来看一下用户对各个优化点的敏感度:
除此之外,因为我们手机的内存是有限的,在启动APP的时候,这时候内存不足,就会调用GC清理后台应用
场景:当我们刚刷完抖音,去启动游戏的时候,在低内存手机中,我们就会感知启动较慢,就回去后台关闭抖音
小结:
端侧场景:
抖音用户上传视频,用户自身录制可能会一下声音大声音小
但是我们刷视频的时候大部分情况下是没有的
这时候就是抖音在端侧做了音量归一化。
服务测资源:
CDN资源:保证了各地数据的流畅性
API流量:内部技术治理,常见场景就是比如说突发了一个事情,这时候一堆人来到了微博查看,但是却发现微博崩了,这是去抖音搜一搜相关资源,发一发短视频,发现抖音是正常的,这就是一个内部技术的区别
带来的好处:
场景:抖音为什么是黑色的主题,目的之一就是降低功耗
下图包含每次运行的前 10 个数据点的样本,以及所有30 次测量的总体平均功耗。您可以很清楚地看到,在这种情况下,黑屏确实会减少手机使用的电量。在 Reddit Sync 中使用以黑色为主的界面时,总体功耗降低了41%。
3. 稳定性优化
需要关注什么?
Crash(崩溃),也就是我们常见的应用闪退。
ANR是(Application Not Responding ) 的缩写,即应用程序无响应。
如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发App ANR错误。如果应用位于前台,系统会向用户显示一个对话框,ANR 对话框会为用户提供强行退出应用或等待的选项。
4. 系统级别优化
从Android平台对性能影响比较大的feature出发,如下图
| 节点 | 原因 |
|---|---|
| zram ksm 内存提升进入Android 主分支 | 主要是为了提供内存的使用率 如系统只给了我们10%的内存,那么我们就只能用10%嘛? 哪肯定不是的,就提出了内存压缩,让我们的使用率提高 |
| Android Runtime(ART) | 2015年当时存在的一个现象: 当时打开一个应用Android普遍比IOS慢 引入了新的虚拟机 |
| HMP scheduler | 当时技术的进步,出现了八核处理器但是技术却没有更上, 当时比较流行的一段话就是:一核 干活七核围观的现象 |
| sdcardFS | 当时的一些手机存储还是比较低的,8G,16G 提出外置SD这个功能来解决这个问题 |
| Cpuset group | 前后台切换 比如当前台应用切换到后台的时候,对于负载会减少,这时候就要限制 所以就提出了这个概念帮助大小核使用 |
| SKGL & Vulkan | 替代opengl图形库,帮助图形渲染 |
| F2FS | 碎片化处理, 之前华为说手机可以用两年不卡,就是运用的这个功能 |
| ART: profile and image | 对ART进行优化,更精确的对代码进行精简 |
| binder重构 | 因为当时的谷歌有一套自研的IPC框架,但是体验一直不好 所以就重构了一套,使得binder并发性能得到大幅提升 |
| Cloud profile | 可以通过google play下发已经预编译好的speed profile 不用打开再去计算 |
| EAS 大小 | 大小核调度器引入解决功耗和性能平衡的痛点 |
| Generational Concurrent Copying GC | 2019引入分代的并行拷贝GC 被引入,提升GC性能 |
从什么角度去优化呢?
映射到软件栈上:app fram 都了解,虚拟机,核心库,kernel,芯片soc,更好利用这些,
这些模块怎么作用到我们的程序上,以及怎么影响我们的程序,
去更好的利用他们这些模块的一些能力来帮助我们提升性能,这就是系统级优化需要展开讨论的地方
小结
本章我们通过以下几个问题去分析我们的稳定性优化
- 性能优化的分类?
- 流畅性指的是什么?
- 资源优化都涉及哪些资源?
- 为什么性能问题会导致稳定性问题?
- 除了程序自身优化以外我们还可以做什么?
最佳工具选型
1. 性能工具的必要性
a. 监控和优化相生相伴
b. 监控有攻也有防
c. 攻是为了发现现有问题,指导优化方向
d. 防是为了发现劣化问题,及时止损
e. 线上监控发现问题并聚合排序,线下监控作为线上辅助,并发版前置发现问题
2. 快速流畅性找出问题-GPU呈现模式
原理:系统通过记录每一帧的相关数据,然后通过图形的形式呈现
优点:无需二次开发,简单易用
缺点:井不完全准确,且无法明确指出造成卡顿问题的具体原因
3. 快速找出布局问题-过度绘制(抖音自研工具Layertool)
原理:通过遍历View Tree信息,输出View层级关系
优点:清楚明了,可以宏观感知ViewTree现状,也可以定制,帮助分析overdraw
缺点:还不能够清楚明确的分析出U的性能瓶颈
3. 深入性能归因-初阶-CPU Profiler
原理:基于JVMTI
优点:完整的方法调用栈输出、支持Java、c、C++方法耗时检测、上手简单
缺点:性能损耗太大
4. 深入性能归因-中阶-Trace View
Instrument
虚拟监听函数入口回调,Enter/Exit/Unwind
耗时点:读时间、写数据到buffer、加锁等指令
Sample
通过定时抓取多次堆栈dff,近似确定函数的进入和退出时间
耗时点:堆栈diff、同instrument
间隔抓取堆栈的时间越长性能损耗越少,而越会导致短函数检测不到
5. 深入性能归因-高阶-Systrace
ftrace: debugfs采集和读取trace数据,记录trace events
atrace: 用户侧的trace跟踪,聚合所有的trace event
系统级的Trace数据:锁监控等
6. 抖音大杀器-btrace (aka rhea)-进阶
rhea-systrace:全函数插桩,自动生成Trace代码,对层数做限制,性能损耗50% rhea-mtrace:全函数插桩,抛弃systrace,自己统计函数耗时,最后数据展现同systrace rhea-atrace:优化systrace性能,聚合更多性能数据:类加载、Lock、10等
7. 如何查出功耗问题-Battery Historian
系统级功耗检测,更详细的数据
如何优化_成为一个优秀的屠龙少年
现状分析_掌握问题根源,才能彻底解决问题
耗时成因
CPU Time: 循环,反射,序列化/反序列化,类解析 IO Wait: 1o操作,等待l0返回结果 IPC: Binder调用耗时 Lock Wait: 主线程是等锁状态,等待其他线程或者自己超时 CPU Schedule: 主线程是可执行状态,但是获取不到cPU时间片
运行环境归因
- 根据耗时成因归类
- 根据运行所在线程环境采用不同的策略
启动耗时归因
Cold Start:Create process、ContentProvider init、Application#Create、Aim for <3 seconds Warm Start:Activity#Create、Inflate view hierarchy、Aim for <1 seconds Hot Start:Activity#onStart
渲染分析
根据渲染串行流程逐层分析
Choreographer VSync > UI Thread**>RenderThread>**Graphics
同时需要考虑并行影响,如途中 Ul Thread中红色方块和Other Process / Thread中的橙色方块block task
案例分析
问题:尝试描述一下一个布局从xml变成view对象的耗时点?
解答:
A. IO
B. 类反射
C. View初始化
D. AssertManager资源锁
拆解View显示流程中的耗时成因
耗时成因:xml 10、class反射、创建view、Asset资源大锁
那么如何去解决呢?
谷歌提供的UI构建的优化方案
官方解决方案:AsyncLayoutinflater
当用到一个布局先去异步Inflate,完成了之后在进行设置
抖音解决方案:
Andinflater:解决xm/性能问题外界方案x2C Legolnflate:高优先级的启动预加载方案 Asynclnflater:随时随地预加载,不与具体逻辑绑定,生命周期存活,自定义清理周期
数据请求+解析_优化
GSON解析优化,如果我们对使用的数据有复用,差距是十分大的
数据协议优化:json->protobuf
对比
渲染和布局优化方案
系统工具查看具体的overdraw情况,但并不能细化到具体的布局和位置,我们开发了LayerTool工具可以根据自定义需求过滤各个布局,然后在界面上提示对应的位置,可以有效的查找导致overdraw的布局。我们通过LayerTool工具查看哪些布局导致了过度绘制。然后我们可以逐个开始优化:
-
移除不必要的背景图 :去除冗余背景
-
修改不合理布局:精细化布局显示
-
写高效合理的布局:随着状态的变换,及时调整布局的绘制背景
-
移除默认的 Window 背景 :Activity其实是有一层默认背景windowBackground。如果需要做到极致的优化,可以考虑将windowBackground设置为空,但是同时需要处理好随之而来的各种问题。主要包括:把windowBackground设置为空后,如果有某些View没有设置背景,就会出现花屏/重影的现象。
渲染优化的异步化方案
如何彻底解决主线程的阻塞问题:
SurfaceView:采用独立的线程进行绘制和渲染,生命周期需要自己控制
Jetpack Compose:基于组合优于继承的思想,重新设计一套解耦的U框架
Litho:复杂UI下的高性能渲染框架
扁平化:Litho采用Yoga完成组件布局measure和layout,实现布局的扁平化
组件化:Litho 使用 Drawable作为Node绘制单元,实现了布局的细粒度复用和异步计算布局的能力
缺点:不支持现有逻辑,需要重新实现
总结
本节课我们通过从概念到例子的理解
-
现状分析?洞察程序内部的瓶颈,层层剥离,最终才能发现问题根源
-
经典案例: 入门:一个view的成长史 初阶:View 的构建优化 初阶:如何快速的填充View 初阶:View之间的组合优化 高阶:如何解决构建、填充、组合带来的主线程流畅问题->异步,多核并发
通过课上老师精彩的讲解 了解到了在Android中页面是如何去渲染的,我们利用工具去分析拆解问题,最后把问题解决!
参考资料
萌新初学,本文为笔记,大佬若有更好的见解欢迎评论区留言