AndroidUI显示原理及性能优化

来源:互联网 发布:淘宝店中国制造 编辑:程序博客网 时间:2024/06/06 07:23

  昨天看了泓洋大神的一片有关布局优化的文章,发现自己对ui布局显示方面还是非常的菜的,于是决定好好学一下这方面的知识。
  首先,学习一下Android显示原理。
  


安卓显示原理

  Android应用程序显示过程:Android应用程序调用SurfaceFlinger服务把经过测试、布局和绘制后的Surface渲染到显示屏幕上。

SurfaceFlinger:Android系统服务,负责管理Android系统的帧缓冲区,即显示屏幕。
Surface:Android应用的每个窗口对应一个画布(Canvas),即Surface,可以理解为Android应用程序的一个窗口。

  Android应用程序的显示过程包含了两个部分(应用侧绘制系统侧绘染)、两个机制(进程间通讯机制显示刷新机制)。

应用侧绘制
  一个Android应用程序窗口里面包含了很多ui元素,这些ui元素是以树形结构来组织的,即它们存在着父子关系,其中,子ui元素位于父ui元素里面,如下图:
  
这里写图片描述

  因此,在绘制一个Android应用程序窗口的ui之前,我们首先要确定它里面的各个子ui元素里面的大小以及位置。
  确定各个子ui元素在父ui元素里面的大小以及位置的过程又称为测量过程和布局过程。
  因此,Android应用程序窗口的ui渲染过程可以分为测量(Measure)布局(Layout)绘制(Draw)三个阶段。如下图所示:

这里写图片描述

Measure 测量:递归(深度优先)确定所有视图的大小(高、宽)
Layout 布局:递归(深度优先)确定所有视图的位置(左上角坐标为原点)
Draw 绘制:在画布canvas上绘制应用程序窗口所有的视图

绘制

  Android目前有两种绘制模式:基于软件的绘制模式和硬件加速的绘制模式(从Android3.0开始全面支持)。

基于软件的绘制模式
  
  在基于软件的绘制模式下,CPU主导绘图,视图按照两个步骤绘制:
  1、让View层次结构失效(invalidation)
  2、绘制View层次结构(layout)
  
  当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的incalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重会的屏幕区域(脏区)。然后 ,Android系统会在View层次结构中绘制所有跟脏区相关的区域。但是,这种方法有两个缺点:
  1、绘制了不需要重绘的视图(与脏区域相交的区域 [布局嵌套] )
  2、遮盖了一些应用的bug(由于回重绘与脏区域相交的区域) [ 这点没明白 ]
  (注意:在View对象的属性发生变化时,如贝京活着TextView对象中的文本等,Android系统会自动的调用该View对象的invalidate()方法。)

硬件加速的绘制模式

  在基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤绘制:
  1、让View层次结构失效
  2、记录、更新显示列表
  3、绘制显示列表

  这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录在一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有实效的View对象则能重放先前显示列表记录的绘制指令来进行简单的重绘工作。
  使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,以便下次重用,然后再调用OpenGL完成绘制。
  硬件加速提高了Android系统显示和刷新的速度,但它也不是万能的,它有三个缺陷:
  1、兼容性(部分绘制函数不支持或不完全硬件加速)
  2、内存消耗(OpenGL API调用就会占用8MB,而实际上会占用更多内存)
  3、电量消耗(GPU耗电)

系统侧绘染

  Android应用程序在图形缓冲区绘制好View层次结构后,这个图形缓冲区会被交给SurfaceFlinger服务,而SurfaceFlinger服务再使用OpenGL图形库API来讲这个图形缓冲区渲染到硬件帧缓冲区(显示屏)中。

进程间通讯机制
  Android应用程序为了能够将自己的ui绘制在系统的帧缓冲区上,它们就必须要与SurfaceFlinger服务进行通信,如图所示:
  Android应用程序与SurfaceFlinger服务是运行在不同的进程中的,因此,它们采用某种进程间通信机制来进行通信。由于Android应用程序在通知SurfaceFlinger服务来绘制自己的ui的时候,需要将ui数据传递给SurfaceFlinger服务,例如,要绘制ui的区域、位置等信息。一个Android应用程序可能会有多个窗口,而每一个Android应用程序与SurfaceFlinger服务之间,都会通过一块匿名共享内存来传递UI数据,如下所示:
  
这里写图片描述

  但是单纯的匿名共享内存在传递多个窗口数据时缺乏有效的管理,所以匿名共享内存就被抽象为一个更上流的数据结构SharedClient,如下图所示:

这里写图片描述

  每一个SharedClient中,最多有31个SharedBufferStack,每个SharedBufferStack都对应一个Surface,即一个窗口。这样,我们就可以知道为什么SharedClient里面包含的是一系列SharedBufferStack而不是单个SharedBufferStack:一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含多个窗口,即Surface。从这里也可以看出,一个Android应用程序最多可以包含31个窗口。
  每个SharedBufferStack中又包含了n个缓冲区( < Android4.1时n=2;>= Android4.1时n=3),即显示刷新机制中即将提到的双缓冲和三重缓冲技术。

显示刷新机制

  一般我们在绘制ui的时候,都会采用一个称为“双缓冲”的技术。双缓冲意味着要使用两个缓冲区(每个SharedBufferStack包含了2个缓冲区),其中一个称为Front Buffer,另一个称为Back Buffer。ui总是现在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中,理想情况下,这样一个刷新会在16m内完成(60FPS),下图就是描述这样的一个刷新过程(Display处理前Front Buffer,CPU、GPU处理Back Buffer)。
  
这里写图片描述

  但实际情况并非这么理想。
  1、时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三个互不干扰,一切正常。
  2、时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却并未及时去绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
  3、时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。
  
  通过上述分析可知,此处发生Jank的关键问题在于,为何第1个16ms段内,CPU/GPU没有及时处理第2帧数据?原因很简单,CPU可能是在忙别的事情,不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了!
  为解决这个问题,Android 4.1中引入了VSYNC,这类似于时钟中断。结果如下图所示:

这里写图片描述

  上图可知,每收到VSYNC(帧同步)中断,CPU就开始处理各帧数据。整个过程非常完美。
  不过,自私琢磨后却发现一个新问题:上图中,CPU和GPU处理数据的速度似乎都能在16ms内完成,而且还有时间空余,也就是说,CPU/GPU的FPS(Frames Per Second)要高于Display的FPS。确实如此,由于CPU/GPU只在收到VSYNC时才开始数据处理,所以它们的FPS相同。处理并没有什么问题,因为Android设备的Display FPS一般是60,其对应的显示效果非常平滑。

  如果CPU/GPU的FPS小于Display的FPS,会是什么情况呢?请看下图:

这里写图片描述

  由上图可知:
  1、在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。
  2、同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。
  
  为什么CPU不能在第二个16ms处开始绘制工作呢?原因就是只有两个Buffer(Android 4.1之前)。如果有第三个Buffer的存在,CPU就能直接使用它,而不至于空闲。出于这一思路就引出了三重缓冲区(Android 4.1)。结果如下图所示:

这里写图片描述

  由上图可知:
  第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就比较顺畅了。
  是不是Buffer越多越好呢?回答是否定的。由上图可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示,这比双Buffer情况多了16ms延迟。所以,Buffer最好还是两个,三个足矣。


UI性能优化技能

  好了,知道了Android显示原理后,我们就可以解释在编写App的时候,有时会感觉界面顿卡,尤其是自定义View的时候,大多数是因为布局的层次过多,存在不必要的绘制,或者onDraw等方法中过于耗时。
  Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。那么如果操作超过了16ms就会发生下面的情况:

这里写图片描述

  如果系统发生的VSYNC信号,而此时无法进行渲染,还在做别的操作,那么就会导致丢帧的现象,(大家在察觉到APP卡顿的时候,可以看看logcat控制太,会有drop frames类似的警告)。这样的话,绘制就会在下一个16ms的时候才进行绘制,即使只丢一帧,用户也会发现卡顿的。
  因此,我们顿卡的原因就在于CPU、GPU没有办法在16ms内完成该完成的操作。
  渲染的过程是由CPU与GPU协作完成,下面一张图很好的展示出了CPU和GPU的工作,以及潜在的问题,检测的工具和解决方案。

这里写图片描述

  从上图中,可以知道:
  1、通过Hierarchy Viewer去检测渲染效率,去除不必要的嵌套
  2、通过Show GPU Overdraw去检测Overdraw,最终可以通过移除不必要的背景以及使用canvas.clipRect解决大多数问题。

Overdraw的检测

  首先,按照以下步骤打开Show GPU Overrdraw的选项:设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制。
  好了,打开以后呢,你会发现屏幕上有各种颜色,此时你可以切换到需要检测的程序,对于各个色块,对比一张Overdraw的参考图:

这里写图片描述

Overdraw 的处理方案一:移除不必要的background

  在这里,有一点非常容易被忽略的就是:我们的Activity的布局最终会添加在DecorView中,而在我们设定了我们layout的背景色的时候,这个DecorView中的背景就没有必要了,所以我们希望调用mDecor.setWindowBackground(drawable);,那么可以在Activity调用getWindow().setBackgroundDrawable(null);。

Overdraw 的处理方案二:clipRect的妙用

  我们在自定义View的时候,经常会由于疏忽造成很多不必要的绘制,比如大家看下面这样的图:

这里写图片描述

  多张卡片叠加,那么如果你是一张一张卡片从左到右的绘制,效果肯定没问题,但是叠加的区域肯定是过度绘制了。
并且material design对于界面设计的新的风格更容易造成上述的问题。那么有什么好的方法去改善呢?
  答案是有的,android的Canvas对象给我们提供了很便利的方法clipRect就可以很好的去解决这类问题。
  示例代码:http://blog.csdn.net/lmj623565791/article/details/45556391

减少不必要的层次:巧用Hierarchy Viewer

  Hierarchy Viewer视图中点击LinearLayout,然后点击Profile Node,你会发现所有的子View上面都有了3个圈圈,取色范围为红、黄、绿色,这三个圈圈分别代表measure 、layout、draw的速度,并且你也可以看到实际的运行的速度,如果你发现某个View上的圈是红色,那么说明这个View相对其他的View,该操作运行最慢,注意只是相对别的View,并不是说就一定很慢。
  红色的指示能给你一个判断的依据,具体慢不慢还是需要你自己去判断的。
  Android Studio是这么打开Hierarchy Viewer视图的,如下图三步:

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述


Android布局优化技能

重用< include/>

  < include>标签可以在一个布局中引入另外一个布局,这个的好处显而易见。类似于我们经常用到的工具类,随用随调。便于统一修改使用。

  举个例子:titlebar的布局显示。
原先我都是这么写的!如以下代码所示:

main.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical" android:layout_width="match_parent"    android:layout_height="match_parent">    <RelativeLayout        android:background="#71cbe7"        android:layout_width="match_parent"        android:layout_height="48dp">        <ImageView            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:padding="5dp"            android:src="@drawable/icon"/>        <TextView            android:layout_centerInParent="true"            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:gravity="center"            android:textSize="18sp"            android:textColor="#FFF"            android:text="title"/>        <TextView            android:layout_alignParentRight="true"            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:gravity="center"            android:textSize="16sp"            android:textColor="#FFF"            android:paddingRight="15dp"            android:text="next"/>    </RelativeLayout></RelativeLayout>

效果图、运行速度如下所示:

这里写图片描述

接下来用< include/>来代替,代码如下:

title_bar.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:background="#71cbe7"    android:layout_width="match_parent"    android:layout_height="48dp">        <ImageView            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:padding="5dp"            android:src="@drawable/icon"/>        <TextView            android:layout_centerInParent="true"            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:gravity="center"            android:textSize="18sp"            android:textColor="#FFF"            android:text="title"/>        <TextView            android:layout_alignParentRight="true"            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:gravity="center"            android:textSize="16sp"            android:textColor="#FFF"            android:paddingRight="15dp"            android:text="next"/></RelativeLayout>

main.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content">    <include        layout="@layout/title_bar"/></RelativeLayout>

测试结果如图所示,与上图对比差不多快了三倍!
这里写图片描述

合并

减少嵌套
  首先我们心中要有一个大原则:尽量保持布局层级的扁平化。
  在这个大原则下我们要知道:
  1、在不影响层级深度的情况下,使用LinearLayout而不是RelativeLayout。因为RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,才会让子View调用2次onMeasure。Measure的耗时越长那么绘制效率就低。
  2、如果非要是嵌套,那么尽量避免RelativeLayout嵌套RelativeLayout。这简直就是恶性循环,丧心病狂。
< merge/>
  < merge/>主要用来去除不必要的FrameLayout。它的使用最理想的情况就是你的根布局是FrameLayout,同时没有使用background等属性。这时可以直接替换。因为我们布局外层就是FrameLayout,直接“合并”。

  接下来,在这个蓝色的titlebar例子中用merge来合并
  由于我要保持titlebar保持背景色,所以我merge最外层。

<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content">    <include        layout="@layout/title_bar"/></merge>

  从View Properities中可以看出与先前的相比少了一层!

这里写图片描述

用TextView同时显示图片和文字

<?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">    <TextView        android:layout_width="match_parent"        android:layout_height="48dp"        android:drawableLeft="@drawable/icon"        android:drawablePadding="4dp"        android:gravity="center"        android:text="title"        android:textSize="18sp"        android:textColor="#FFF"        android:background="#66c6e7" /></LinearLayout>

这里写图片描述

  EditView等也一样的,还有属性drawableBottom和drawableTop供你使用。同时利用代码setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)可以让我们动态去设置图片。

使用TextView的行间距

原本:

<?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">    <TextView        android:layout_width="match_parent"        android:layout_height="48dp"        android:drawableLeft="@drawable/icon"        android:drawablePadding="4dp"        android:gravity="center"        android:text="title\ntitle"        android:textSize="18sp"        android:textColor="#FFF"        android:background="#66c6e7" /></LinearLayout>

这里写图片描述

  加了一句android:lineSpacingExtra=“8dp”后

这里写图片描述

  其中:lineSpacingExtra属性代表的是行间距,他默认是0,是一个绝对高度值。同时还有lineSpacingMultiplier属性,它代表行间距倍数,默认为1.0f,是一个相对高度值。
  这两条属性可以同时使用,他们的高度计算规则为mTextPaint.getFontMetricsInt(null) * 行间距倍数 + 行间距。

使用Spannable或Html.fromHtml

<?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">    <TextView        android:id="@+id/text"        android:layout_width="match_parent"        android:layout_height="48dp"        android:drawableLeft="@drawable/icon"        android:drawablePadding="4dp"        android:gravity="center"        android:text=""        android:textSize="18sp"        android:textColor="#FFF"        android:background="#66c6e7"        android:lineSpacingExtra="8dp"/></LinearLayout>
        String text = "bula~ ! bula~";        int index = text.lastIndexOf("!");        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);        spannableStringBuilder.setSpan(new AbsoluteSizeSpan(14),0,index, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);        spannableStringBuilder.setSpan(new ForegroundColorSpan(Color.YELLOW),0,index, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);        spannableStringBuilder.setSpan(new AbsoluteSizeSpan(20),index,text.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);        ((TextView)findViewById(R.id.text)).setText(spannableStringBuilder);

这里写图片描述

按需载入ViewStub

  在开发中经常会遇到这样的情况,会在程序运行时动态根据条件来决定显示哪个View或某个布局。那么通常做法就是把用到的View都写在布局中,然后在代码中动态的更改它的可见性。但是它的这样仍然会创建View,会影响性能。
  这时就可以用到ViewStub了,ViewStub是一个轻量级的View,不占布局位置,占用资源非常小。
  有一点需要记住:一旦ViewStub被inflate或可见了,ViewStub就不存在了,取而代之的是被inflate的Layout。所以它也被称做惰性控件。

  接下来举一个例子:

<?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">    <Button        android:id="@+id/text"        android:layout_width="match_parent"        android:layout_height="48dp"        android:drawableLeft="@drawable/icon"        android:drawablePadding="4dp"        android:gravity="center"        android:text="点击显示ViewStub"        android:textSize="18sp"        android:textColor="#FFF"        android:background="#66c6e7"        android:lineSpacingExtra="8dp"/>    <ViewStub        android:layout_width="match_parent"        android:layout_height="48dp"        android:id="@+id/hint_view"        android:inflatedId="@id/hint_view"        android:layout="@layout/title_bar"/></LinearLayout>
    private View hintView;    private ViewStub viewStub;        viewStub = (ViewStub) findViewById(R.id.hint_view);        ((Button)findViewById(R.id.text)).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                hintView = viewStub.inflate();                ((TextView)hintView.findViewById(R.id.hint_view_title)).setText("biu!");                hintView.setVisibility(View.VISIBLE);            }        });

用LinearLayout自带的分割线(Android3.0及以上版本)

  实现的核心部分是LinearLayout的两行代码:

android:divider="@drawable/divider"android:showDividers=“middle"

  showDividers 是分隔线的显示位置,beginning、middle、end分别代表显示在开始位置,中间,末尾。
  还有dividerPadding属性这里没有用到,意思很明确给divider添加padding。

Space控件(Android3.0及以上版本)

  如果要给条目中间添加间距,怎么实现呢?当然也很简单,比如添加一个高10dp的View,或者使用android:layout_marginTop=“10dp”等方法。但是会影响性能。使用过多的margin其实会影响代码的可读性。这时就可以使用Space,它是一个轻量级的。使用方法和View一样。

  接下来利用LinearLayout自带的分割线和Space控件写一个例子:

divider.xml

<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"    android:shape="rectangle">    <size android:width="1dp"        android:height="1dp"/>    <solid android:color="#e1e1e1"/></shape>

main.xml

<?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"    android:divider="@drawable/divider"    android:showDividers="middle|beginning|end">    <TextView        android:layout_centerInParent="true"        android:layout_width="match_parent"        android:layout_height="48dp"        android:gravity="center"        android:textSize="18sp"        android:textColor="#888"        android:text="title"/>    <TextView        android:layout_centerInParent="true"        android:layout_width="match_parent"        android:layout_height="48dp"        android:gravity="center"        android:textSize="18sp"        android:textColor="#888"        android:text="title"/>    <Space        android:layout_width="match_parent"        android:layout_height="18dp"/>    <TextView        android:layout_centerInParent="true"        android:layout_width="match_parent"        android:layout_height="48dp"        android:gravity="center"        android:textSize="18sp"        android:textColor="#888"        android:text="title"/></LinearLayout>

这里写图片描述


ok!AndroidUI性能优化的相关技能就差不多结束了!

0 0
原创粉丝点击