Android单排上王者系列之Android性能优化

Android视图的绘制流程

Android中视图的绘制会经历三个阶段即onMeasure()、onLayout()和onDraw()

一、OnMeasure()

Measure是测量的意思,测量视图的大小。Viewd的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ 
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}```

widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:
* 1、 EXACTLY表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小。
* 2、AT_MOST表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小。
* 3、 UNSPECIFIED表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
###二、onLayout()
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
super.onLayout(changed, left, top, right, bottom);
}

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的
###三、onDraw()
measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。

@Overrideprotected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}

通过上面的分析可以得知,view视图的测量绘制可以总结成如下的流程![view测量绘制](http://upload-images.jianshu.io/upload_images/1505616-134f52aaff88649d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
##Android视图刷新机制
###一、渲染原理
App的卡顿最主要的根源是渲染性能的问题,APP有更多的动画和图片的时候,但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。
![渲染](http://upload-images.jianshu.io/upload_images/1505616-583e9694ccc7b922?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
如果某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。
![](http://upload-images.jianshu.io/upload_images/1505616-a4e40c23eba7fd9c?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致CPU或者GPU负载过重。我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。
###二、过度绘制
过度绘制描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。![](http://upload-images.jianshu.io/upload_images/1505616-9e97f8d8e961e2ec?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)蓝色,淡绿,淡红,深红代表了4种不同程度的过度绘制情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
 过度绘制有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。
###三、VSYNC(垂直同步)
要理解App如何渲染的,就必须了解手机硬件是如何工作的,那么就必须理解什么是VSYNC。在讲解VSYNC之前,我们需要了解两个相关的概念:Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。刷新频率和帧率并不是总能够保持相同的节奏。

**帧率比刷新率快的情况**

如果发生帧率比刷新频率快的情况,就会容易出现Tearing的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。出现撕裂现象的原因是因为,当你的显卡正在使用,一个内存区正在写入帧数据(用来显示一帧的一个Buffer),从顶部开始,新的一帧覆盖前一帧,并立刻输出一行内容。现在,当屏幕开始刷新时,实际上并不知道缓冲区是什么状态(即不知道缓冲区中的一帧是否绘制完毕,即存在只绘制了一半的情况,另一半还是之前的那帧),因此它从GPU中抓住的帧肯可能并不是完全完整的。

**屏幕刷新率比帧速率快**

如果屏幕刷新率比帧速率快,屏幕会在两帧中显示同一个画面,当这种断断续续的情况发生时,你就遇到麻烦了。比如你的帧速率比屏幕刷新率高的时候,用户看到的是非常流畅的画面,但是帧速率降下来的时候(GPU绘制太多东西的时候),用户将会很明显地察觉到动画卡住了或者掉帧,然后又恢复了流畅。这通常会被描述为闪屏,跳帧,延迟。
###四、GPU渲染分析工具
使用Peofile GPU Rendering tool,你可以在手机上就可以看到究竟是什么导致你的应用程序出现卡顿,变慢的情况。这个工具在设置-开发者选项-Profile GPU rendering选项,打开后选择on screen as bars![](http://upload-images.jianshu.io/upload_images/1505616-69b1158386b10a03?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)然后手机屏幕上就会出现三个颜色组成的小柱状图,以及一条绿线![](http://upload-images.jianshu.io/upload_images/1505616-1c6f12353bb161c4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)这个工具会在屏幕上显示经过分析后的图形数据,最底部的图显示的是Navigation的相关信息。最上面显示的是Notification的相关信息。中间的图显示的是当前应用程序的图。当你的应用程序在运行时,你会看到一排柱状图在屏幕上,从左到右动态地显示,每一个垂直的柱状图代表一帧的渲染,越长的垂直柱状图表示这一帧需要渲染的时间越长。随着需要渲染的帧数越来越多,他们会堆积在一起,这样你就可以观察到这段时间帧率的变化。
**绿线**
下图中的绿线代表16ms,要确保一秒内打到60fps,你需要确保这些帧的每一条线都在绿色的16ms标记线之下。任何时候你看到一个竖线超过了绿色的标记现,你就会看到你的动画有卡顿现象产生。![](http://upload-images.jianshu.io/upload_images/1505616-5d79e1d1c34202f1?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**柱状图**
每一条柱状图都由三种颜色组成: 蓝-红-黄。这些线直接和Androi的渲染流水线和他实际运行帧数的时间关联。![](http://upload-images.jianshu.io/upload_images/1505616-36609985dfdc6b5f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**蓝色**
蓝色代表测量绘制的时间,或者说它代表需要多长时间去创建和更新你的DisplayList。在Android中,一个视图在可以实际的进行渲染之前,它必须被转换成GPU所熟悉的格式,简单来说就是几条绘图命令,复杂点的可能是你的自定义的View嵌入了自定义的Path。一旦完成,结果会作为一个DisplayList对象被系统送入缓存,蓝色就是记录了需要花费多长时间在屏幕上更新视图(说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象)。
当看到蓝色的线很高的时候,有可能是因为一堆视图突然变得无效了(即需要重新绘制),或者你的几个自定义视图的onDraw函数过于复杂。
**红色**
红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List。这些API有效地将数据发送到GPU,最总在屏幕上显示出来。 当你看到红色的线非常高的时候,这些复杂的自定义View就是罪魁祸首。 上面图中红色线较高的一种可能性是因为重新提交了视图而导致的。这些视图并不是失效的视图,但是有些时候发生了某些事,例如视图旋转,我们需要重新清理这个区域的视图,这样可能会影响这个视图下面的视图,因为这些视图都需要进行重新的绘制操作。
**橙色**
橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理。
##引起卡顿的因素
###一、布局层次太深
解决方案: 布局优化的思想就是尽量减少布局文件的层级,布局中层级少了,Android的绘制工作量就少了,程序的性能就会提高。 
首先要删除无用的控件和层级,其次选择使用性能较低的ViewGroup,例如RelativeLayout。如果使用LinearLayout实现布局需要大量嵌套的时候就需要改成RelativeLayout,因为布局的嵌套就是增加了布局的层级,同样会降低性能。 
另外一种就是需要采用<include>标签、<merge>标签和ViewStub。<include>标签主要用于布局重用,<merge>标签一般和<include>配合使用,它可以降低减少布局的层级,而ViewStub则提供了按需加载的功能,只有需要的时候采用将ViewStub布局加载到内存中,这样也就提高了程序的初始化效率。

 **<include>**
<include>布局可以将指定的布局加载到当前的布局文件中,一般用在titlebar,如果每个布局都需要这样的titlebar,则可以将其独立出来,然后通过<include>标签将titlebar添加到需要的布局中,<include>起到复用布局的特性。

**<merge>**
<merge>标签一般和<include>标签一起使用从而减少布局的层级。如果当前的布局是一个竖直方向的LinearLayout,而其包含的布局也是竖直方向的LinearLayout,则被包含的布局中的LinearLayout是多余的,通过<merger>标签就可以去掉多余的那一层LinearLayout。

**ViewStub**
ViewStub继承了View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。ViewStub的意义在于按需加载所需要的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,不如网络异常时的界面,这个时候就没有必要再整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。 
当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构中的一部分了。另外,目前ViewStub还不支持<merge>标签。
**ViewStub特点:**
* 1、ViewStub只能Inflate一次,之后ViewStub对象会被置为空。按句话说,某个被ViewStub指定的布局被Inflate后,就不能够再通过ViewStub来控制它了。所以它不适用 于需要按需显示隐藏的情况。
* 2、ViewStub只能用来Inflate一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的view,还是使用visibility属性吧。 
* 3、VIewStub中不能嵌套merge标签。
###二、绘制优化绘制优化
解决方案:在onDraw()方法中避免执行大量的操作,主要体现在两个方面
* 1、onDraw()中不要创建新的局部对象,因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时文件,这不仅占用了过多的内存而且还会导致系统更加频繁的gc,降低了程序的执行效率。
* 2、onDraw方法中不要做耗时的操作,也不能执行过多的循环,大量的循环会抢占CPU的资源这样就会造成View的绘制卡顿。按照Google官方给出的性能优化典范中的标准,View的绘制帧率保持在60fps是最佳的,这就要求每帧的绘制时间不超过16ms,虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法的复杂度方向是对的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容