转载请注明出处:http://blog.csdn.net/wl9739/article/details/57416433
在开发过程中,往往会听到 “性能优化” 这个概念,这个概念很大,比如网络性能优化、耗电量优化等等,对我们开发者而言,最容易做的,或者是影响最大的,应该是 View 的性能优化。一般小项目或许用不上 View 性能优化,然而,当业务愈加庞大、界面愈加复杂的时候,没有一个良好的开发习惯和 View 布局优化常识,做出来的界面很容易出现 “卡顿” 现象,从而严重影响用户体验。而对于我们开发者来说,了解一些 View 性能优化的常识,增强开发技巧,可以说是一门必备的功课。
为了更好地理解 View 性能优化的原理,以及造成 “卡顿” 的可能原因,我们从 View 的绘制流程开始讨论。之后,会介绍一些写界面布局常用的一些标签及使用注意事项。
我们都知道,View 的绘制分为三个阶段:测量、布局和绘制,这三个阶段各自的作用如下:
measure: 为整个 View 树计算实际的大小,即设置实际的高(对应属性:mMeasureHeight)和宽(对应属性:mMeasureWidth),每个 View 的控件的实际宽高都是由父视图和本身视图所决定的。layout:为将整个根据子视图的大小以及布局参数将 View 树放到合适的位置上。draw:利用前两部得到的参数,将视图显示在屏幕上。当一个 Activity 对象被创建完成之后,会将一个 DecorView 对象添加到 Window 中,同时会创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 对象建立联系,然后绘制流程就会从 ViewGroup 的 performTraversals() 方法开始执行,如下图所示:

整个绘制流程从 ViewRootImpl 的 performTraversals() 方法开始,在该方法内会调用 performMeasure() 方法进行测量子 View(也就是根 View,顶级的 ViewGroup)。然后在 performMeasure 中会调用 measure() 方法来执行具体的测量逻辑,这个时候,代码逻辑就从 ViewRootImp 跳转到了 View 类中了:
在 measure() 方法中,有一个 onMeasure() 方法,用于这个方法用来测量子元素的大小,也将测量流程从父元素传递到子元素当中去。紧接着子元素会重复父元素的测量流程,如此反复,就完成了一颗 View 树的遍历。当 measure() 方法完成后,会将结果存储在 LongSparseLongArray 类型的变量 mMeasureCache 中。
在 performTraversals() 方法中,调用完 performMeasure(),后,会接着调用 performLayout() 和 performDraw() 进行 View 的布局和绘制。这两个流程和测量的流程差不多,就不再叙述。
而这三个阶段分别作了什么呢?源码太长就不贴了,主要的作用如下:
Measure 过程
设置本 View 视图的最终大小。如果该 View 对象是个 ViewGroup 类型,需要重写该 onMeasure() 方法,对其子视图进行遍历 measure() 过程。 measureChildren(),内部使用了一个 for 循环对子视图进行遍历,分别调用了子视图的 measure() 方法。measureChild(),为指定的子视图 measure,会被 measureChildren 调用。measureChildWidthMargins(),为指定的子视图考虑了 margin 和 padding 的 measure。Layout 过程
layout() 方法会设置该 View 视图位于父视图的坐标轴,即 mLeft, mTop, mRight, mBottom.(调用 setFrame() 方法去实现),接下来回调 onLayout() 方法(如果该 View 是 ViewGroup 对象,需要实现该方法,对每个视图进行布局);如果该 View 是个 ViewGroup 类型,需要遍历每个子视图 childView。调用该子视图的 layout() 方法去设置它的坐标值。Draw 过程
绘制背景如果要视图显示渐变框,这里会做一些准备工作绘制视图本身,即调用 onDraw() 方法。在 view 中,onDraw() 是个空方法,也就是说具体的视图都啊哟覆盖该方法来实现自己的显示(比如 TextView 在这里实现了绘制文字的过程)。而对于 ViewGroup 则不需要实现该方法,因为作为容器是没有内容的,其包含了多个子 View,而子 View 已经实现了自己的绘制方法,因此只需要告诉子 View 绘制自己就行了,也就是下面的 dispatchDraw() 方法。绘制视图,即 dispatchDraw() 方法。在 View 中这是个空方法,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器必须实现该方法。如果需要,开始绘制渐变框。绘制滚动条。因此,如果我们去掉不必要的背景,去掉渐变框,去掉滚动条,在一定程度上是能加快绘制速度的。
帧率(frame per second,即 FPS),指的是每秒刷新的次数。一般电影的帧率为 24FPS、25FPS 和 30FPS。而游戏的帧率一般要保持 60FPS 才能叫做流畅,当游戏的 FPS 低于 30 时,我们就会感受到明显地卡顿。Android 系统每隔 16ms 触发一次 UI 刷新操作,这就要求我们的应用都能在 16ms 内绘制完成。如果有一次的界面绘制用了 22ms,那么,用户在 32ms 内看见的都是同一个界面。情况严重的就会让用户感受到应用运行”卡顿“。
因此,优化的目的,主要就是减少绘制时间,尽量保证每个界面都能在 16ms 内完成绘制。而优化的方案,从上面的分析,我们可以分两个方面:

从内优化
减少 View 层级。这样会加快 View 的循环遍历过程。去除不必要的背景。由于 在 draw 的步骤中,会单独绘制背景。因此去除不必要的背景会加快 View 的绘制。尽可能少的使用 margin、padding。在测量和布局的过程中,对有 margin 和 padding 的 View 会进行单独的处理步骤,这样会花费时间。我们可以在父 View 中设置 margin 和 padding,从而避免在子 View 中每个单独设置和配置。去除不必要的 scrollbar。这样能减少 draw 的流程。慎用渐变。能减少 draw 的流程。从外优化
布局嵌套过于复杂。这会直接 View 的层级变多。View 的过渡绘制。View 的频繁重新渲染。UI 线程中进行耗时操作。在 Android 4.0 之后,不允许在 UI 线程做网络操作。冗余资源及错误逻辑导致加载和执行缓慢。简单的说,就是代码写的烂。频繁触发 GC,导致渲染受阻。当系统在短时间内有大量对象销毁,会造成内存抖动,频繁触发 GC 线程,而 GC 线程的优先级高于 UI 线程,因而会造成渲染受阻。外部因素最为致命!日常开发中更多的应该关心布局的嵌套层级和冗余资源。
比如,当需要将一个 TextView 和一张图片放在一起展示时,我们可以考虑使用 TextView 的 drawableLeft(drawableRight、drawableTop、drawableBottom) 属性来设置图片,而不是使用一个 LinearLayout 来将 TextView 和 ImageView 封装在一起,这样就能减少 View 的绘制层级。
又比如,子元素和父元素都是相同的背景时,就不必在每个子元素中都添加背景属性,等等。
线性布局和相对布局是我们平时使用最多的布局方式。在一般开发场景中,两者的渲染效率没有明显差别,但是如果真要较真的话,他们之间还是有细微差别的。
RelativeLayout 在测量子 View 排列方式是基于彼此的依赖关系,这种依赖关系导致了子 View 的显示顺序不一定和布局中的 View 的顺序相同,在确定所有子 View 的时候,会先对所有的 View 进行排序,同时,由于 RelativeLayout 允许 “A在横向上依赖于 B,B 在纵向上依赖于 A“,因此会测量两次,导致测量效率较低。而 LinearLayout 由于有 orientation 属性,则测量就很简单了。
LinearLayout 在设置 weight 属性的时候,也会导致二次测量:首先会遍历测量没有 weight 属性的 View,然后再遍历测量包含 weight 属性的 View。
布局比较

选择布局容器的基本准则:
尽可能的使用 RelativeLayout 以减少 View 层级,使 View 树趋于扁平化。在不影响层级深度的情况下,使用 LinearLayout 和 FrameLayout 而不是 RelativeLayout。说到布局标签,我想大概很多人都用过一些。为了说明 Android 系统对于这些标签的处理,我们先看一下 xml 布局是如何解析绘制到屏幕的。
在 Activity 的 onCreate() 方法中,我们一般会调用 setContentView() 方法,这个方法负责将 XML 文件解析绘制到屏幕上,这个方法很简单:
这个方法第一行是调用 Window 类的 setContentView(),第二行是初始化 ActionBar。Window 类是一个抽象类,它是所有视图相关类的顶层类,其唯一一个实现类是 PhoneWindow,在 PhoneWindow 类的 setContentView() 方法中,会先移除掉所有的 view 视图,然后再调用 LayoutInflater.inflate() 方法绘制,在 LayoutInflater 的 inflate() 方法中,会创建一个 XmlResourceParser 解析器,然后再进行解析。我们来看看 inflate() 方法里面干了什么:
为了方便阅读,我将一些代码省略掉,从上面可以看出大致的解析流程:先判断是否有 merge 标签,然后检查其合理性,注意源码已经说明了,merge 标签只能用在 ViewGroup 的根布局中,并且 attachToRoot 必须要设置为 true。然后调用 rInflate() 方法;如果没有 merge 标签,就会调用 rInflateChildren() 方法生成子元素的布局,而这个 rInflateChildren() 方法最终也是辗转到前面的 rInflate() 方法中,我们来看一下这个方法:
这里面一共涉及到了四个标签:<requestFocus/>,<tag/>,<include/> 和 <merge/>。下面来分别说一下:
requestFocus 标签就是让标签内的 View 获取焦点,其内部就是使用 view.requestFocus() 方法实现的。
tag 标签是 API 21 里面新增的,用来给 View 对象添加额外的信息。从 Android 1.0 开始,Android 就开始支持给 View 对象调用 setTag(Object) 和 getTag(Object) 来添加和获取标签信息,到了 Android 1.6 ,添加和获取标签信息有了新的方法:setTag(int, Object)。而在 Android 4.0 之后,setTag(int, Object) 的内部实现改为非静态的 SparseArray 来实现。Android 5.0 的时候,提供了一种全新的写法,就是 tag 标签。
举一个例子来说明这个标签怎么用,先编写一个 XML 布局文件:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btn_negative" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="cancel"> <tag android:id="@+id/btn_state_negative" android:value="@string/btn_state_negative" /> </Button> <Button android:id="@+id/btn_positive" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="ok"> <tag android:id="@+id/btn_state_positive" android:value="@string/btn_state_positive" /> </Button></LinearLayout>然后我们就可以通过下面的方式获取标签信息:
Button btn_negative = (Button) findViewById(R.id.btn_negative);String tag = (String) btn_negative.getTag(R.id.btn_state_negative);tag 标签是这四个标签中唯一一个需要指定 id 属性的!
我们来看看 处理 include 标签的方法 parseInclude() 里面的逻辑:
在 parseInclude() 方法里面会判断是否需要处理 merge 标签,然后根据标签名(如 Button、TextView 等)调用 createViewFromTag() 方法创建一个 view 对象,然后生成该对象的布局参数,设置 id 属性,设置可见性等等。
然后注意到 createViewFromTag(),顾名思义,该方法会根据 XML 的标签来创建 View 对象,这个方法里面最终会调用到 createView() 方法,是使用反射来创建 View 对象的具体实现。
有个问题不知道大家注意到没有,这些 id、可见性等等的属性都是 view 对象的,而 include 标签和 merge 标签并并没有这些属性,也就是说,如果你在 include 或 merge 标签中设置了一个 id,然后在代码中通过 findViewById() 方法企图找到这个 include 或 merge 的布局,是会报空指针异常的!
我这里为了方便区分,将 include、merge 等标签称为“布局标签”,它们不能创建为 View 对象,设置 id 属性对它们没有意义。而将 XML 中的 Button、TextView 等标签称为“视图标签”(视图元素、控件等),因为它们能被创建为 View 对象,可以设置 id 等熟悉。
除了上面介绍的 merge、include、requestFocus 和 tag 等布局标签外,还有如下常用的 View 标签:
ViewStub
利用 ViewStub 标签可以让布局懒加载。当你界面要显示很多内容,而其中一些不用立即显示出来的时候(比如商品详情、下载进度条等等),可以使用 ViewStub 标签,让 ViewStub 引用这些不用立马加载的界面布局,当需要的时候再让它们加载出来。ViewStub 虽然是 View 标签,但是其本身没有大小,不会绘制任何东西,因此是一个非常轻量的 View 标签。
使用 ViewStub 和 include 标签类似,需要使用 android:layout 属性来确定哪些布局需要懒加载,同时,由于 ViewStub 是一个 View 标签,因此需要使用一个 id 来操作 ViewStub。比如使用 ViewStub 简单的 XML 如下:
在 java 代码中,对 ViewStub 的操作有两种方式:
设置 View 的可见性
findViewById(R.id.stab_view)).setVisibility(View.VISIBLE);调用 ViewStub 的 inflate() 方法
上面两种方法都可以加载由 ViewStub 引用的布局。使用 ViewStub 有两点需要注意:
当调用了inflate() 方法后,ViewStub 标签就从视图中移除了,也就是说,inflate() 方法不能对同一个 ViewStub 调用两次。ViewStub 所引用的布局的根标签不能为 标签。Space
这是是一个空控件,该 View 没有实现 onDraw() 方法,因此绘制效率比较高。该控件可以用来占用空白(比如代替 padding 和 margin)。
大概差不多了,View 的性能优化还有一些没有介绍,比如 Overdraw 等,这里就给个链接吧:OverDraw
同时,对于 View 性能优化有兴趣的同学,欢迎参加视频课程:有心课堂
新闻热点
疑难解答