绘制优化
场景
UI
- 绘制
- 刷新
启动
- 冷启动
- 温启动
- 热启动
跳转
- 页面间切换
- 前后台优化
响应
- 按键
- 系统事件
- 滑动
根本原因
界面绘制
- 绘制层级太深
- 页面复杂:控件个数 > 80。
- 刷新不合理
数据处理
- 在UI主线程处理数据
- 数据处理占用CPU高,导致主线程拿不到时间片
- 内存增加导致GC频繁而引起的卡顿
Android系统显示原理
渲染简述
Android应用把经过测量,布局,绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到显示屏幕上,通过Android的刷新机制来刷新绘制。
- 应用层负责绘制
- 系统层负责渲染
- C/S架构
- SurfaceFlinger(C++)
- Java API
- C++底层具体实现
应用层
- Measure
- 深度优先:先遍历子节点,再遍历兄弟节点。
- 广度优先:先遍历兄弟节点,在遍历子节点。
- Layout:深度优先
- Draw
- 软件绘制
- 硬件加速(GPU)
- 比CPU耗电
- 兼容问题
- 内存大
系统层
- 主要工作
- 响应客户端事件,创建Layer与客户端的Surface建立联系
- 接收客户端数据及属性,修改Layer属性,如尺寸,颜色,透明度等
- 将创建的Layer内容刷新到屏幕上
- 维持Layer的序列,并对Layer最终输出做出裁剪计算
- SharedClient - SharedBufferStack 1 : 31 匿名共享内存
- SharedBufferStack 包含了N个缓冲区 < 4.1 N = 2 > 4.1 N =3
- FPS 60 16ms
- 刷新机制
- 双缓冲
- VSYNC
- Choreographer
- Callback_input:优先级最高,与输入事件有关
- Callback_animation:第二优先级,与动画有关
- Callback_traversal:最低优先级,与UI控件绘制有关
性能分析工具
- Systrace
- TraceView
- Profile GPU Rendering
- 蓝色:测量绘制的时间
- 红色:执行的时间 Display List
- 橙色:处理时间
- 紫色:将资源转移到渲染线程的时间
- shell dumpsys gfxinfo com_xxx_xxx
布局优化
工具
- Hierarchy View
- Layout Inspactor
避免过度绘制
- 定义:布局优化主要就是避免Overdraw。
- 什么是Overdraw?Overdraw就是过度绘制,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,而每次绘制都会对应到CPU的一组绘图命令和GPU的一些操作,当这个操作耗时超过16.67ms时,就会出现掉帧现象,也就是我们所说的卡顿,所以对重叠不可见元素的重复绘制会产生额外的开销,需要尽量减少Overdraw的发生。
- 查看Overdraw
- 在开发者选项-调试GPU过度绘制(Show GPU Overdraw)
- 没有颜色: 意味着没有overdraw。像素只画了一次。
- 蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)
- 绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
- 浅红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。
- 暗红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。
- 如何优化
- 合理选择控件容器
- LinearLayout
- TableLayout
- FrameLayout
- RelativeLayout
- ConstraintLayout
- 去掉window的默认背景
- 去掉其他不必要的背景
- ClipRect & QuickReject:为了解决Overdraw的问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
- ViewStub
- Merge
- 善用draw9patch
- 慎用Alpha
- 避免“OverDesign”
- 合理选择控件容器
启动优化
启动方式
- 冷启动:当启动应用时,后台没有该应用的进程,这时系统会首先会创建一个新的进程分配给该应用,这种启动方式就是冷启动。
- 热启动:当启动应用时,后台已有该应用的进程,比如按下home键,这种在已有进程的情况下,这种启动会从已有的进程中来启动应用,这种启动方式叫热启动。
- 温启动:当启动应用时,后台已有该应用的进程,但是启动的入口Activity被干掉了,比如按了back键,应用虽然退出了,但是该应用的进程是依然会保留在后台,这种启动方式叫温启动。
启动命令
- 如何对比启动时间
- adb shell am start -W packagename/MainActivity
- adb shell am start -S -R 10 -W packagename/.MainActivity
-S
表示每次启动前先强行停止-R
表示重复测试次数
- 参数:
- ThisTime:最后一个启动的Activity的启动耗时;
- TotalTime:新应用启动的耗时,包括新进程的启动和Activity的启动;
- WaitTime(5.0之前没有):ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)。
- 系统日志统计:过滤displayed输出的启动日志。
- WaitTime 是 startActivityAndWait 这个方法的调用耗时
- ThisTime 是指调用过程中最后一个 Activity 启动时间到这个 Activity 的 startActivityAndWait 调用结束。
- TotalTime 是指调用过程中第一个 Activity 的启动时间到最后一个 Activity 的startActivityAndWait 结束 如果过程中只有一个 Activity ,则 TotalTime 等于 ThisTime。
性能检查项
- Application的构造器方法——>attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量、布局、绘制显示在界面上
- 任务分优先级
- 优先级为1 启动加载
- 优先级为2 首页渲染完成后 开始加载
- 优先级为3 首页渲染后 延迟加载
- traceview
- 找出单个方法执行时间长的
- 找出执行次数多的
- 优化思路总结
- UI渲染,去除重复绘制
- 根据优先级划分初始化工作
- SharedPreference优化
- 网络错误优化,使用ViewStub
- Multidex优化
- 检查BaseActivity
任务
- 加载启动App
- App启动之后立即展示出一个空白的Window
- 创建App的进程
- 创建App对象
- 启动Main Thread
- 创建启动的Activity对象
- 加载View
- 布置屏幕
- 进行第一次绘制
- 完成第一次绘制后会把Main Activity替换已经展示的Background Window
启动加速的优化方向
- 利用提前展示出来的Window,快速展示出一个画面,给用户快速反馈的体验。
- 设置背景图Theme:程序启动快,界面先显示背景图,然后再刷新其他界面控件。给人刷新不同步感觉。
- 把样式设置为透明:给人程序启动慢感觉,界面一次性刷出来,刷新同步。
- 避免在启动时做密集沉重的初始化
- 四个维度:
- 必要且耗时:启动初始化,考虑用线程来初始化。
- MultiDex初始化
- Tinker初始化
- 必要不耗时:首页绘制
- 非必要耗时:数据上报,插件初始化
- 非必要不耗时:直接去掉,在需要用的时候再加载。比如:其他第三方组件的初始化
- 必要且耗时:启动初始化,考虑用线程来初始化。
- 思考方向:
- 分步加载:以大化小,优先级高的放前。
- 异步加载:耗时多的异步化。
- 延期加载:非必要的数据延时加载。
- 四个维度:
- 定位问题
- 避免IO操作
- 序列化、反序列化
- 网络操作
- 布局嵌套
通用启动优化方案
- 利用主题快速显示界面。
- 异步初始化组件。
- 通过梳理业务逻辑,延迟初始化组件、操作。
- 正确使用线程:开启线程池比单独开启一个线程耗资源。
- 去掉无用代码、重复逻辑等
如何查找优化点
- 开发阶段使用BlockCanary 或者ANRWatchDog等第三方监控sdk。
- Method Tracing
- Systrace
- nimbledroid
- Lint
常见问题
- 部分的数据库,I/O操作发生在MainActivity主线程。
- Application中创建了线程池。
- Application中做了大量的初始化操作。
- MainActivity网络请求密集:
- 是否有必要。
- 接口是否可以合并。
- 工作线程使用没设置优先级:AyncTask- thread_priority_background、AsyncQueryHandler - thread_priority_default、线程池。
- 信息没缓存,重复获取同样信息。
- 不合理的业务流程:高级与低级区别。
- 废弃的老代码。
刷新机制
合理的刷新需要注意的几点:
- 尽量减少刷新次数:控制刷新频率,进度条,变化没1%,完全没必要刷新。
- 避免没有必要的刷新:
- 数据没有变化,需要刷新的控件在不可见区域。
- 一个View从不可见到可见,一定要刷新一次。
- 尽量避免后台有高CPU的线程运行。
- 缩小刷新区域:
- invalidate(Rect dirty)。
- invalidate(int left,int top,int right,int bottom)。
- RecyclerView。
提升动画性能
- 帧动画:消耗资源最多
- 补间动画
- 属性动画
- 硬件加速
- Application级别:<application android:hardwareAccelerated=”true” …/>
- Activity级别:<activity android:haredwareAccelerated=”false/true” …/>
- Window级别:getWindow().setFlags(WindowManager.LayoutParams.FLAG_HAREDWARE_ACCELERATED);
- View级别:View.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
- LAYER_TYPE_NONE
- LAYER_TYPE_HARDWARE:绘制为硬件纹理。
- LAYER_TYPE_SOFTWARE:此View通过软件渲染为一个Bitmap。
- 动画流程总结:
- 将要执行动画的View的LayerType设置为LAYER_TYPE_HARDWARE
- 计算动画View的属性等信息,更新View的属性
- 若动画结束,将LayerType设置为NONE
- 需要注意的点:
- 在软件渲染时,可以重用bitmap的方式来节省内存,但开启了硬件加速,这个方案就不起作用。
- 开启硬件加速 需要额外的内存,加速的UI切换到后台时,产生的内存有可能不释放。
- 在UI中的过渡绘制时,硬件加速会比较容易发生问题。
卡顿监控方案
…
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LT的编程笔记!