1.Android UI的渲染机制
当我们感觉到的流畅画面,需要的画面帧数要达到40帧到60帧每秒。而一帧的时间大约是16.67ms,换句话说,在1000ms的时间内,16.67ms大约就是现实60帧画面的单位时间。在Android系统中,系统是通过VSYNC信号触发对UI的渲染的,如果系统每次渲染的事件都保持在16.67ms以内,那么我们看到的UI界面将是非常的流畅的,这也就需要我们将所有程序的逻辑都保证在16ms之内,如果不能在16ms内完成绘制,那么就将造成丢帧的现象。即当前该重绘的帧被未处理完成的逻辑阻塞,例如一次绘制任务耗时20ms,那么在16ms系统发出的VSYNC信号就无法绘制,该帧就会被丢弃,等待下次信号擦次开始绘制,这就是画面卡顿的原因。
Android系统提供了检测UI渲染时间的工具,在“开发者选项中”有“GPU呈现模式分析”,选择“在屏幕上显示为条形图”,如下所示(本测试机为魅族,其他手机可能略有不同):
每一个条形图都包含有三部分,蓝色部分表示测量绘制Display List的时间,红色代表的是OpenGL渲染Display List所需要的时间,黄色代表的是CPU等待GPU处理的时间,中间的绿色横线代表的是VSYNC时间16ms,需要尽量将所有条形图都控制在这条绿线之下。
2.overDraw
overDraw表示的就是过度绘制,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,而每次绘制都会对应到CPU的一组绘图命令和GPU的一些操作,造成CPU和GPU资源的浪费。在系统默认的绘制Activity的背景,如果再给布局绘制了重叠的背景,那么默认Activity的背景就是无效的过度绘制。
在我们的“开发者选项”中有这样一个检测工具“调用GPU过度绘制”,激活该功能之后可以通过界面上的颜色来判断overDraw的次数。
这个工具可以帮助我们检测当前区域的绘制次数,从而优化界面绘图层次,尽量增大蓝色的区域,减少红色的区域。
3.优化布局层次
在Android中,系统对View进行测量,布局和绘制时,都是通过对View数的遍历来进行操作的。如果一个View树的高度太高,就会严重影响到测量,布局和绘制的速度,因此,优化布局的第一个方法就会是降低View树的高度,Google也在API文档中建议View树的高度不宜超过10层。在现在的XML文件的根布局中,我们默认RelativeLayout来替换使用LineraLayout作为默认的根布局,其原因就是通过扁平的RelativeLayout来降低LineraLayout嵌套所产生的布局树的高度,从而提高UI的渲染速度。
1.使用 include 标签重用Layout
在一个应用程序的界面上,为了保持风格的统一,很多界面都会存在共通的UI,比如所说Topbar,Bottombar,Actionbar等等,如果在每一个界面上都进行赋值这一段共通的布局代码,不仅不利于后期代码的维护,还会增加程序的冗余。这时候就可以使用include标签来定义一这一个共通的UI。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="0dp"
android:textSize="28sp"
android:text="這是共通的UI界面"
android:gravity="center">
</TextView>
在该共通的布局中,我将layout_width和layout_height设置为0dp,这样就迫使调用者在使用时必须对控件的狂傲进行赋值,否者是无法看见该控件的。
下一步就是如何使用该共通的UI布局了。只需要在使用该共通的UI布局文件中使用include标签的layout属性对这个共通的UI的ID的引用即可。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/commen_ui"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:text="你好你好你好"/>
</LinearLayout>
这时候如果我们要对布局中的属性进行赋值,就要重新覆盖某一项属性,进行赋值即可。效果如下:
2. 使用ViewStub实现view的延迟加载
使用ViewStub标签来实现对一个view的引用并且实现延迟加载。ViewStub是一个非常轻量级的组件,它不仅不可视,而且大小为0,下面来演示如何使用ViewStub来进行实现延迟加载的目的。
首先创建一个布局,这个布局在初始化加载时是不需要显示的,只有在某些情况下才需要进行显示的,例如查看用户信息的时候,只有点击了某一个按钮时,用户详细信息才显示出来。
当运行程序后,我们发现ViewStub中的布局确实没有显示出来,那么要如何才能重新加载显示的布局呢?
首先要通过findViewById()方法找到组件ViewStub。
mStub = (ViewStub) findViewById(R.id.vs_viewstub);
接下来就有两种方式显示这个view:
- VISIBLE
通过调用ViewStub的setVisiblity()方法来显示这个view.代码如下:
mStub.setVisibility(View.VISIBLE);
- INFLATE
通过调用ViewStub的inflate()方法来显示这个view.代码如下:
View inflate = mStub.inflate();
这两种方式都是可以将ViewStub重新进行展开,显示引用的布局,而唯一的区别在于就是inflate()可以返回引用的布局,从而可以通过View.findViewById()方法来找到对应的控件。
View inflate = mStub.inflate();
TextView textview = (TextView) inflate.findViewById(R.id.textview);
textview.setText("我是点击后加载的");
注意:不管只用那种方式,一旦ViewStub被设置可见或者是inflate之后,ViewStub就不存在了,不能被反复的inflate,取而代之的就是被inflate的Layout,并将这个Layout的ID重新设置为ViewStub中通过android:inflateId属性所指定的ID,这也就是为什么两次点击之后会报错的原因:如下所示:
Caused by: java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
简要说明View.GONE和ViewStub标签的区别是什么?共同点都是初始时都不会显示,但是ViewStub标签只会在显示时才会去渲染整个布局,而View.GONE,在初始化布局树的时候就已经添加在布局树文件中了,相比之下ViewStub的效率会更高。
如下图所示:
4.hierarchyviewer.bat
hierarchyviewer.bat是无法在真机上进行使用的,只能在模拟器上使用或者是原生的模拟器上使用,也就是没有加密的设备上。当然真机上也是可以用的,可以到github上下载一个开源项目View Server,下面在模拟器上使用hierarchyviewer.bat。
hierarchyviewer.bat位于sdk\tools目录下,直接双击即可启动,如下:
注意:我们在使用hierarchyviewer的时候,一定要是模拟器是打开的,这样才能在hierarchyviewer中看到我们要显示的布局文件。
下面我们写一个非常冗余的布局进行显示文件,代码如下所示:
<?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="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="你好啊啊 啊啊啊啊 啊"
android:textColor="#efff0019"
android:textSize="20sp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
这个布局文件是三层LinearLayout嵌套之后里面装了一个button,很显然这些LinearLayout都是冗余的,利用hierarchyviewer可以打开这个布局文件,如下图所示:
通常情况下,我们只关注ID为content的Framlayout的分支,这也是setContentView()设置的内容,可以很明显的看出在layout布局文件中(红色布局),这三层LinearLayout没有任何的分支,说明是冗余嵌套,可以直接去掉的。
当点击其中一个view的时候,可以显示view的绘制情况的,不过第一次点击的时候各种显示的事件都是n/a,需要点击菜单中的Profile Node按钮重新进行计算,才能回去到绘制信息。在系统的右下方会给出不同颜色的小圆点,用来表示绘制的效率,绿黄红分别代表的是好中差的绘制效率。