名词解释:
显卡
如: 桌面端使用的NVIDA AMD
独立显卡: 桌面端Windows macOS才有 好处是独立出主板,更好散热,更好替换,最主要的是有自己的显存,而不是使用系统内存
而Android这种就没有独立显卡,使用的是显卡驱动,占据的也是系统内存
显存(即显卡内存 显卡的一个参数) 也叫帧缓存 用于缓存画面
FPS(frame per second) 帧率 24是能看到动画 30更流畅 60以上是能很流畅的看画面没有任何卡顿感 (即不会掉帧)
丢帧(SF: Skipped Frame) 不是丢包 而是在16ms完成工作却因各种原因没做完,占了下n个16ms的时间,相当于丢了n帧
流畅度(SM: SMoothness) 和丢帧相对,在VSync机制中1s内Loop运行的次数。
从代码到画面渲染到显示器上:
OpenGL-ES
CPU 计算(负责把UI组件计算成Polygons(多边形),Texture(纹理))->
GPU 渲染(交给GPU进行栅格化渲染(即对每个正方形像素点安排渲染 因此像素点越高 细节越凸显),这里涉及到矢量图形,位图的转换等等 )->
显存 帧缓存 ->
视频控制器 数模转换 视频控制器会按照HSync信号逐行读取帧缓冲区的数据->
显示器 进行画面的显示渲染
画面撕裂现象
显示器还没读取完一帧的画面,GPU就已经完成了下一帧的渲染,然后放到了显存中,替换了前一帧,此时视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象
解决:
在GPU中引入一个VSync垂直同步,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新
引入的问题:
1 若CPU或者GPU计算渲染较慢,则会占据2个以上的VSync时间片,而导致显示器继续显示之前d帧的画面
2 限制了60帧率的刷新
Android帧率查看
1 可视化UI方式: 开发者选项->GPU呈现模式 绿色线即16.67ms分割线 但是这种对于surfaceView的帧率看不了
2 adb方式:adb shell dumpsys gfxinfo <PACKAGE_NAME>
命令
得到以下结果:
Total frames rendered: 30 // 渲染的帧数
Janky frames: 21 (70.00%) // 掉帧数
50th percentile: 32ms
90th percentile: 150ms
95th percentile: 150ms
99th percentile: 150ms // 99%的帧渲染时间
Number Missed Vsync: 4
Number High input latency: 0
Number Slow UI thread: 6
Number Slow bitmap uploads: 15
Number Slow issue draw commands: 10
3 adb shell dumpsys SurfaceFlinger --latency + <Component名称>
3 可视化方式: perfdog
UI界面挺好看,也是要装一个app到设备上
原理:
4 SurfaceFlinger
Android系统的一个服务,用来生成Surface,管理帧缓冲区,实际做的事:把不同z坐标的Window按顺序排放,将所有的window合成一张图,也就是一帧
该命令会有128行数据结果的,第一行表示刷新间隔,不同手机可能会有不同的值, 接下来的127行表示了最近的127帧的渲染情况,每行分三列,第二列比较重要,因为它对应的是这一帧渲染时垂直同步脉冲到来的时间
缺点:1 对于非连续绘制场景,获取的FPS不准确,比如有的画面就是停在那,这时并不进行新的渲染,FPS会很低
5 systrace 查看到系统渲染的帧时间
可以在代码中
也可以用命令行 执行一个python脚本
若>16ms则是有问题的 说明渲染耗时太久 造成UI卡顿
通过放大那一帧进一步分析耗时问题
6 choreographer(编舞者)
在每次VSync的时候,会通过Choreographer.postCallback(callback)
callback通知view去更新 而若此时view没绘制完,是不会响应这个callback的
https://juejin.im/entry/5ae1a4aef265da0b7e0bf94a
若非连续绘制场景,也是会回调,唯一不回调的就是view没绘制完的情况,即卡顿的情况
优化几大思想
所有的流畅度优化方法,本质都是:提高在下一次VSync到来之前,CPU/GPU完成绘制渲染的概率
1 直接砍功能
2 砍不了就懒加载
3 延迟加载
4 提前预加载 一般搭配5
5 子线程异步处理
6 减负
UI绘制慢的排查和解决
过渡绘制
过渡绘制即界面中存在多个layout重叠,上面的会显示,而底层的并不会显示,但是也执行了相关的计算和绘制渲染
一 过度绘制监测:
1 打开开发者选项的 显示过度绘制
- 原色:没有过度绘制
- 蓝色:1 次过度绘制
- 绿色:2 次过度绘制
- 粉色:3 次过度绘制
- 红色:4 次及以上过度绘制
2 Tracer for OpenGL ES
查看每一帧的绘制过程
在android-sdk/tools/monitor中,打开即是Android Device Monitor 里面有Tracer for OpenGL ES
查看app当前界面的布局情况
Hierarchy View(deprecate) 使用Layout Inspector代替
设备adb连接androidStudio,在Tool->layout inspector中可以查看该设备的app的某个界面的layout情况
如看laout的view树,view的大小 left和top坐标等
优化策略:
1 若你的界面有自己的背景覆盖了全屏,则可以去除Activity自带的背景色
<style name="AppTheme" parent="android:Theme.Light.NoTitleBar">
<item name="android:windowBackground">@null</item>
</style>
或者
getWindow().setBackgroundDrawable(null);
2 ImageView的background和imageDrawable重叠
ImageView的background设置了默认背景图
而 加载图片时 仍然会绘制这个背景图
解决: 把背景图和真正加载的图片都通过imageDrawable方法进行设置
3 限制 view的绘制区域
通过clipRect和clipPath方法 限制view的绘制区域
避免不必要的区域的绘制
4 使用 merge、include、ViewStub 标签进行布局优化
include设置layout=“xxx”进行布局设置
可以设置 长宽
<include layout="@layout/xxx_layout"></include>
使用 merge标签作根布局去除界面的多余层级
merge并不是一个ViewGroup 也不是View 而是直接将里面的控件加入到父布局中 减少布局嵌套
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
/>
<Button
/>
</merge>
ViewStub是惰性加载 用到的时候才加载布局
注意:
若要使用viewStub中的控件
不是用 viewStub.findxxx
而是直接 findxxx 并且要在 ViewStub加载后才可以
因为 ViewStub 加载了 ViewStub会移除自身 用相应的布局代替
所有的控件已经放到了 父布局里面了
实践: setVisibility会不会绘制
无论是 View.INVISIBLE 还是 View.GONE 都是不绘制的 也是不能被点击
但是 会调用 invalidate 因此
动态地inflation view性能要比SetVisiblity性能要好,当然ViewStub是最好的选择
前置同步屏障可以优化UI
通过Choreographer的postFrameCallback监听doFrame,记录doFrame时间mLastDoFrameTime
发起一个丢帧检测线程,每隔1帧时间验证
满足这两个条件,表明:因为同步屏障滞后导致了丢帧,需要前置同步屏障
- mLastDoFrameTime距离当前时间 是否超过1帧时间,也就是发生了丢帧
- 判断消息队列中是否有doFrame消息
确认发生丢帧后,post同步屏障到消息队列的队首,提前执行doFrame消息
实现则是修改MessageQueue,将同步屏障插入到队首
首页优化
1 View预加载
子线程中 异步预加载 inflate View
可能会与主线程加载View冲突,解决:
2 闪屏页和主页在同一个Activity 闪屏只作为一个View
3 数据到首帧渲染之间的优化:将一些任务放在doFrame之后,反射的方式将Sync Barrier提前
布局加载优化 X2C
问题背景:
通过XML编写布局,具体会有以下的性能瓶颈耗时:
1、通过I/O操作将XML加载到内存中,即读取xml很耗时
2、递归解析xml较耗时
3、通过反射生成对象耗时,是new的3倍以上
定义: 为了即保留xml的优点,又解决它带来的性能问题,在编译生成APK期间,将XML layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件。
原理
采用APT (AnnotationProcessor Tool) + JavaPoet技术来完成编译期间 [注解] ->[解注解] ->[翻译xml] ->[生成java]
整个流程的操作
优势:
省去了使用IO的方式去加载XML布局和解析XML的耗时。
采用Java代码直接new的方式去创建控件对象,所以它也没有反射带来的性能损耗
缺点:
兼容性和稳定性稍差
包体积增加
编译时间变长
存在问题:
不支持flavor,不支持< merge/ >标签,部分android属性不支持等
会存在很多跨模块的资源引用问题(A模块中的a.xml include B模块中的b.xml)。而apt/kapt插件是module级别的插件,跨模块的问题处理起来很困难。当时的方案是将< include/ >标签转换成< ViewStub/ > 处理,将编译期间的问题延迟到runtime处理。
代码层面检测
Lint工具和Inspect Code 静态代码扫描 用于发现XML布局等的问题
在项目的根目录中(有gradlew的)
执行
./gradlew lint
即可编译程序的同时开启lint检测 若lint不成功 则会中断编译
Android Studio中 Analyse->Inspect Code
这个是包含了lint检测 还有其他的资源检测等的
Android Profiler
可以提供更详细的方法调用关系链和对应耗时信息,通过层层分析,找到真正耗时的执行方法
** 此外本身堆栈采样会造成性能消耗,导致每个操作的耗时被放大了,不能代表真实耗时 **
TimiLogger 查看具体的执行时间
1 new一个TimiLogger对象
TimingLogger timings = new TimingLogger(TAG, "methodA");
TAG 标签 methodA 方法标签
2 在需要打印时间的地方调用addSplit(String splitLabel)方法
timings.addSplit("worked");
// ... do some work A ...
timings.addSplit("work A");
3 调用dumpToLog()打印日志
timings.dumpToLog();