前言
卡顿场景可分为以下四类:
- UI绘制:绘制、刷新
- 应用启动:安装启动、冷启动、热启动
- 页面跳转:页面间切换、前后台切换
- 事件响应:按键、系统事件、滑动
这四种卡顿场景的根本原因又可以分为两大类:
- 界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理。
- 数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况:
- 一是数据处理在UI线程(这种应该避免)。
- 二是数据处理占用CPU高,导致主线程拿不到时间片。
- 三是内存增加导致GC频繁,从而引起卡顿。
Android系统显示原理
Android的显示过程可以简单概括为:Android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到屏幕上,通过Android的刷新机制来刷新数据。
绘制原理
应用层
在Android的每个view绘制中又三个核心步骤:Mesasure、Layout、Draw。通过Measure和Layout来确定当前需要绘制的view所在的大小和位置,通过绘制(Draw)到surface。
Measure和Layout都是递归来获取view的大小和位置,并且以深度作为优先级,因此层级越深,元素越多,耗时也就越长。
系统层
应用层和系统层是两个不同进程,在Android的显示系统,使用匿名共享内存:SharedClient,每个应用和SurfaceFlinger之间都会创建一个SharedClient。在每个SharedClient中,最多可以创建31个ShardBufferStack,每个Surface都对应一个ShardBufferStack,也就是一个window。 一个SharedClient对应一个Android应用程序,意味着一个Android应用程序最多可以包含31个窗口。
显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。
知道绘制原理后,那么绘制一个单元多长时间才是合理的?
——在理想情况下,60FPS(Frames Per Second 每秒传递的帧数)就感觉不到卡,这意味着每个绘制时长应该在16ms以内。
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染。若每次都成功就能达到流畅画面的60FPS。若某个操作耗时较久,系统在得到VSYNC信号时就无法正常渲染,这样就会发生丢帧现象。
==卡顿的根本原因==
影响绘制的根本原因有以下两方面:
- ==绘制任务太重==,绘制一帧内容耗时太长。
- ==主线程太忙==,导致VSync信号来时还没有准备好数据导致丢帧。
性能分析工具
性能问题不容易复现,在分析性能问题时需要借助相应的调试工具,比如查看Layout层次的Hierarchy View、Android系统自带的 Profile GPU卡顿检测工具和静态代码检查工具Lint,以及性能分析常用的TraceView和SysTrace等。
卡顿检测工具
Profile GPU Rendering是Android4.1系统开始提供的开发辅助工具,可在开发者选项中打开(华为手机是:GPU呈现模式分析按钮)
特点:
- 是一个图形检测工具,实时反应当前绘制的耗时。
-
提供一个标准耗时,高于标准耗时,表示当前一帧丢失。
各种颜色含义
技巧:
在实际开发中,图形不便做数据分析,可通过:adb shell dumpsys gfxinfo com.##.##(包名)把具体的耗时输出到日志中来分析。
对大部分应用来说丢失几帧影响不大,只需保证大部分在警戒线下即可。通过Profile GPU Rendering发现有问题对页面后,可通过另一个工具Hierarchy Viewer来查看布局层次和每个view所花时间具体定位。
TraceView
TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以分析到应用具体每一个方法的执行时间。
- 使用方法
在使用TraceView分析问题之前需要得到一个*.trace的文件,然后通过TraceView来分析。trace文件的获取方法有两种:
- 通过Android Studio的Android Device Monitor,单击Start Method Profile按钮开始监控,操作要监控的界面,完成后stop就会跳到TraceView视图。
- 代码中加入调试语句保存trace文件:android.os.Debug类中提供类相应的方法,调用代码如下:
//在开始监控的地方,保存在"/sdcard/trace_name.trace"
Debug.startMethodTracing("trace_name");
//...
//stop trace
Debug.stopMethodTracing();
- TraceView 视图说明
TraceView视图分两个部分,上半部分为时间片面板,下半部分为分析面板。
- 时间片面板:X轴表示时间消耗,Y表示各个线程,每个线程中的不同方法用不同颜色表示,颜色越宽表示该方法占用CPU时间越长。
- 分析面板:主要关注Calls + Recur Calls/Total(该方法调用次数+递归次数)和Cpu Time / Call(该方法耗时)这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑。
SysTrace UI性能分析
Systrace是Android4.1以上版本提供的性能数据采样和分析工具。功能包括跟踪系统的I/O操作、内核工作队列、CPU负载等。能直观查看CPU周期消耗的具体时间,用不同颜色来突出问题严重性,并提供解决建议。
注意:由于Systrace从系统角度返回一些信息,并不能定位到具体耗时的方法,要具体分析原因要借助TraceView
- Systrace 使用方法
- 在DDMS上使用:
(1)打开Android Device Monitor,连接手机准备好需抓取界面;
(2)单击Systrace进入抓取前的设置,选择需跟踪内容;
(3)手机操作需跟踪过程;
(4)到设定时间后,生成Trace文件,使用Chrome打开文件。 - 使用命令行
cd android-sdk/platform-tools/systrace
python systrace.py --time=10 -o mytrace.html sched gfx view wm
具体命令查看官方文档
- 应用中获取:在应用中加入Trace跟踪需要注意两点:
(1)Trace嵌套时,endSection()方法只会结束离它最近的一个beginSection()。所以要保证endSection和beginSection调用次数匹配。
(2)Trace的begin和end必须在同一线程中执行。
public void ProcessPeople{
Trace.beginSection("ProcessPeople");
try{
Trace.beginSection("Process One");
try{
//code
}finally{
Trace.endSection(); //end Process One
}
Trace.beginSection("Process Two");
try{
//code
}finally{
Trace.endSection(); //end Process Two
}
}finally{
Trace.endSection(); //end ProcessPeople
}
}
- 分析Systrace报告
通过前面方法获取到的trace.html文件,需要用Chrome打开,其中和UI绘制关系紧密的是Alerts和Frame两个数据。
- Alerts:标记了性能有问题的点,可以看到问题的详细描述
- Frame:每个应用都有一行专门显示frame,它将任何它认为性能有问题的东西都高亮警告并提升怎么优化。
布局优化
布局是否合理主要影响的是页面测量时间的多少,如果层级太深,每增加一层则会增加更多的页面显示时间。
常用布局优化工具
1. Hierarchy View
Hierarchy View是Android SDK自带的调试工具,用来检查Layout嵌套及绘制时间,以可视化的布局角度获取Layout布局设计和各种属性信息。
使用:在Android Studio中打开Android Device Monitor菜单,直接打开Hierarchy View
- 查看层级图:在window窗口页,选择需要查看的组件,双击或单击Load View Hierarchy按钮即可。
- 查看某个view的耗时:在快捷键工具栏单击Obtain layout times for tree rooted at selected node。
一个应用界面非常多,如果一个个用Hierarchy View分析效率低,可以用另一个工具Lint,用于检查所有页面的层级,并把深度高于N的界面输出,然后在用Hierarchy View仔细分析。
2. 布局层级检查
Android Lint是ADT 16之后引入的代码检查工具,通过代码静态检查,可以发现潜在的代码问题,并给出优化建议。
使用前可在File -> Setting -> Inspections -> Android Lint中配置扫描规则和缺陷级别。
在Android studio中启动Lint:从菜单栏选择Analyze -> Inspect Code,进去后选择扫描范围扫描。
布局优化方法
通过减少Layout层级,减少测量、绘制时间,提高复用性三方方面来优化布局,,优化的目的是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性。
1. 减少层级
减少层级的两个常用方案:
- 合理使用RelativeLayout和LinearLayout
- 合理使用Merge
合理使用RelativeLayout和LinearLayout:
RelativeLayout相对LinearLayout能够减少布局层级,但也存在性能低的问题,原因是RelativeLayout会对子view做两次测量,因为依赖关系可能和布局中view顺序不同,在确定子view位置时,需先给所有子view做一次排序。
布局原则:
- 尽量使用RelativeLayout和LinearLayout
- 在层级相同时,使用LinearLayout
- 如果用LinearLayout会使层级变多,则应该用RelativeLayout。
合理使用Merge
Merge是合并的意思,可以有效优化某些符合条件的多余层级。使用场景如下:
- 在自定义view中使用,父元素尽量是FrameLayout或者LinearLayout。
- 在Activity中整体布局,根元素需要是FrameLayout。
Merge使用要求:
- Merge只能用在布局XML文件的根元素。
- 使用Merge加载布局时,必须指定一个ViewGroup作为其父元素,并且设置加载的attachToRoot参数为true。
- 不能在ViewStub中使用Merge元素。(原因是ViewStub的inflate方法中根本没有attachToRoot的设置)
2. 提高显示速度
有时需要某个布局在一开始不显示,在某个条件下才显示,可以通过visable属性来控制,但这样效率非常低,因为虽然布局隐藏来,但还在布局中,仍会解析这些布局。可以使用ViewStub控件来解决这个场景并提高效率。
ViewStub是一个轻量级的View,它是一个看不见的,并不占布局位置,占用资源非常小的视图对象。
使用ViewStub注意的点:
- ViewStub只能加载一次,之后ViewStub对象会被置空。也就是布局被加载后就不能再用ViewStub来控制它的显示隐藏。
- ViewStub只能用来加载一个布局文件,而不是某个具体的View。
- ViewStub不能嵌套Merge标签。
ViewStub主要使用场景:
- 在程序运行期间,某个布局被加载后,状态就不会有变化。
- 想要控制一个布局文件的隐藏/显示,而不是某个view
3. 布局复用
开发过程中可以将一些公共的布局抽离出来作为一个布局文件,然后在需要使用的地方通过<include/>标签来实现引入。
对布局优化的总结
- 尽量使用RelativeLayout和LinearLayout
- 尽可能少用wrap_content,会增加布局Measure时的计算成本,已知道宽高固定值时不用wrap_content。
- 将复用组件抽取出来并通过<include/>标签使用
- 使用<ViewStub/>标签加载按需显示的布局
- 使用<Merge/>标签减少布局嵌套层级
- 删除控件中无用属性
避免过度绘制
过度绘制的主要原因
- XML布局:控件有重叠且都有设置背景。
- View自绘:View.OnDraw里面同一个区域被绘制多次。
过度绘制检测工具
通过手机设置中开发者选项,打开Show GPU Overdraw选项(调试 GPU 过度绘制),打开后会有不同的颜色区域表示不同的过度绘制次数。不同颜色含义如下:
- 无色:没有过度绘制,每个像素只绘制来1次
- 蓝色:过度绘制 1 次(大片蓝是可以接受的)
- 绿色:过度绘制 2 次
- 粉色:过度绘制 3 次(不要超过1/4)
- 红色:过度绘制 4 次或更多次(需优化)
我们的目标是减少红色Overdraw,看到更多蓝色或无色区域。
如何避免过度绘制
- 布局上的优化
- 移除XML中非必需的背景,或根据条件设置
- 移除Window默认背景
- 按需显示占位背景图片
在Android自带的一些主题时activity往往会设置一个默认的背景,这个背景有DecorView持有。当自定义布局有一个全屏背景时,DecorView的背景此时对我们来说是无用的,但会产生一次Overdraw,因此可以移除
protect void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
this.getWindow().setBackgroundDrawaable(null);
}
- 自定义View优化
自定义view能减少layout的层级,但在实际绘制时容易出现过度绘制。可以通过canvas.clipRect()来帮组系统识别那些可见的区域,然后只在这个区域绘制。canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
启动优化
应用启动流程
启动分两种类型:冷启动和热启动
- 冷启动:系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化Activity,最后显示在界面。
- 热启动:会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化Activity。
启动 -> Application -> attachBaseContext() -> onCreate() -> Activity生命周期
启动耗时检测
- adb shell am:使用adb shell获取应用真实启动时间代码
adb shell am start -W [packageName]/[packageName.AppstartActivity]
执行后得到三个时间
- ThisTime:一般和TotalTime时间一样,如果启动时加来过度全透明的页面预先处理一些事,这样会比TotalTime小;
- TotalTime:应用启动时间,包括Application和Activity初始化到界面显示;
- WaitTime:包括系统影响的耗时。
但这个方法只能得到固定某个阶段耗时,不能知道具体方法耗时。可用代码打点方式来得到具体方法耗时。
- 代码打点
启动优化方案
启动主要完成三件事:UI布局、绘制和数据准备,因此优化启动速度也是优化这三个过程。
- UI布局优化
- 减少布局层级
- 避免过度绘制
- 启动加载逻辑优化
数据按需实现加载逻辑
- 分步加载:以大化小,优先级高的放前
- 异步加载:耗时多的异步化
- 延期加载:非必需的数据延时加载
合理的刷新机制
合理的刷新机制要注意以下几点;
- 尽量减少刷新的次数
- 尽量避免后台有高CPU线程运行
- 缩小刷新区域
减少刷新次数
- 控制刷新频率:比如刷新进度调可1%刷新一次,而不是实时刷新
- 避免没有必要的刷新:先判断是否需要刷新,比如数据没变化、控件不在可见区域就没必要刷新。
避免后台线程影响
后台线程如果开销很大,占用CPU过高,导致系统频繁GC和CPU时间片资源紧张,有可能会导致页面的卡顿。因此在需要迅速刷新的情况下避免这类线程在高峰工作。
缩小刷新区域
采用局部刷新来节约资源
- 自定义view时:可以使用两个局部更新数据的方法
invalidate(Rect dirty)
invalidate(int left, int top, int right, int bottom)
- 容器中的某个Item发生了变化,只需更新这个Item即可。
提升动画性能
从三个纬度来对比动画性能
- 流畅度:核心,控制每一帧动画在16ms以内完成
- 内存:避免内存泄漏,减小内存开销
- 耗电:减小运算量,优化算法,减小CPU占用
优化建议:
- 尽量使用属性动画
- 适当使用硬件加速
使用硬件加速注意几点:
- 在软件渲染时,可以使用重用Bitmap的方法来节省内存,但开启硬件加速后不起作用。
- 开启硬件加速的View在前台运行时,需要耗费额外的内存,加速的UI切换到后台时,产生的额外内存有可能不释放。
- 当UI中存在过度绘制时,硬件加速容易发生问题。
参考书籍:《Android应用性能优化最佳实践》