Android UI优化

Android UI渲染机制

渲染模型分类

从Android 3.0开始(API L11),Android开始全面使用硬件加速来进行2D渲染,硬件加速是指Android中在View上进行绘制的图形图像都使用GPU来进行绘制,使用硬件加速,在大部分时候都让绘制更加流畅,但付出的代价是需要消耗更多的内存资源。

软件绘制模型

软件绘制模型,这里由CPU主导绘图,按照以下2个步骤绘图:

  • 让视图结构(view hierarchy)失效;
  • 绘制整个视图结构;

当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的invalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。但是,这种方法有两个缺点:

  • 绘制了不需要重绘的视图(与脏区域相交的区域)
  • 由于会重绘与脏区域相交的区域,可能会掩盖一些应用的bug

注意:在View对象的属性发生变化时,如背景色或TextView对象中的文本等,Android系统会自动的调用该View对象的invalidate()方法。

硬件加速绘制模型

Android UI绘制需要CPU与GPU协同工作,CPU通过运算生成绘制指令,GPU完成实际的绘制工作。具体流程为:当需要绘制UI时,计算布局位置、坐标等,然后生成显示列表(Display List,保存的是对应的GPU指令,opengl),最后依次调用Display List中的指令,通知GPU进行绘制。
这里写图片描述

硬件加速绘制模型,底层是由GPU来完成渲染,按照以下3个步骤绘图:

  • 让视图结构失效。
  • 记录和更新显示列表(Display List)。
  • 绘制显示列表。

这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象就简单重用先前显示列表记录的绘制指令来进行简单的重绘工作。

使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,然后再调用OpenGL完成绘制。

在这种绘制模型下,我们不能依赖一个视图与脏区(dirty region)相交而导致它的draw()方法被自动调用,所以必须要手动调用该视图的invalidate()方法去更新显示列表。如果忘记这么做可能导致视图在改变后不会发生变化。

硬件加速提高了Android系统显示和刷新的速度,但也有缺陷:兼容性(部分绘制函数不支持或不完全硬件加速)

这里写图片描述

官方文档参考:https://developer.android.google.cn/guide/topics/graphics/hardware-accel.html

UI渲染机制

60FPS

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。
这里写图片描述
如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。
这里写图片描述
用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致CPU或者GPU负载过重。

图像渲染双缓存机制

屏幕中显示的东西都会被放在一个称为显示缓存的地方,如果我们只有一个这样的缓存,也就是单缓存,在缓存中任何绘图的过程都会被显示在屏幕中,这也就是我们为什么会看到闪烁的愿意,双缓存能够解决这一问题。

双缓存就是在这个显示缓存 之外 再建立一个不显示的缓冲区,我们所有的绘图都将在这个不显示的缓冲区中进行,只有当一帧都绘制完了之后才会被拷贝到真正的现实缓冲区显示出来,这样中间过程对于最终用户就是不可见的了,那即使是速度比较慢也只会出现停顿而不会有闪烁的现象出现,双缓存之间就是通过垂直同步VSYNC来同步的。当GPU的帧率超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住。

这里写图片描述

屏幕刷新率和帧率:

  • Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。
  • Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。

GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
这里写图片描述

帧率大于刷新频率

帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。

帧率小于刷新频率

我们遇到更多的情况是帧率小于刷新频率,在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。

这里写图片描述

UI性能分析

每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。CPU计算布局位置、坐标等,然后生成Display List 生成绘制指令,最后依次调用display list中的指令,通知GPU进行绘制。

这里写图片描述

在CPU方面,最常见的性能问题是CPU通常存在的问题是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。在GPU方面,最常见的问题是我们所说的过度绘制(overdraw)。下面我们对GPU和CPU产生的两大问题进行优化。

  • CPU对应问题:不必要的布局,布局应尽量扁平化
  • GPU对应的问题:过度绘制(overdraw)

View层级优化

Hierarchy Viewer工具

Hierarchy Viewer能够很便捷可视化的查看各种View嵌套关系,是一个很好的研究xml视图结构的工具。

主要有两个用途:

(1)分析当前页面视图层级;

(2)分析布局的时间统计(Measrue、Layout、Draw)所需要的具体时间。

有利于发现潜在的渲染瓶颈,并解决之。

使用wiki:http://blog.csdn.net/LYRIC_315/article/details/59108255

FrameLayout、LinearLayout和RelativeLayout之间的选择

三者之间的比较

这里写图片描述

参考文档:http://www.codexiu.cn/android/blog/9893/

几点经验建议:

1、RelativeLayout会让子View调用2次onMeasure,在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。

2、如果在View树层级的末端,应尽量用一个RelativeLayout来代替两层LinearLayout或FrameLayout。降低View树的层级才是王道。

3、LinearLayout 在有weight时,可能会调用子View2次onMeasure,降低测量的速度,在使用LinearLayout 应尽量避免使用layout_weight。

LinearLayout 在有weight属性时,为什么是可能会导致 2次measure ?

分析源码发现,并不是所有的layout_weight都会导致两次measure:

Vertical模式下,child设置了weight(height=0,weight > 0)时将会跳过这一次Measure,之后会再一次Measure

//Vertical模式下,child设置(height=0,weight > 0)时将会跳过这一次Measure,之后会再一次Measure
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
   // Optimization: don't bother measuring children who are going to use
   // leftover space. These views will get measured again down below if
   // there is any leftover space.
   final int totalLength = mTotalLength;
   mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
   skippedMeasure = true;//跳过这一次measure
} 

但还有一个前提条件,LinearLayout为MeasureSpec.EXACTLY,MeasureSpect规则:
这里写图片描述
也就说,如果要使用Linearlayout且必须使用weight属性时,请尽量保证LinearLayout的宽(Horizontal)或者高(Vertical)大小为match_parent、具体值(如300dp)

android:layout_width="match_parent"
或者
android:layout_width="300dp"
使用include\merge\ViewStub标签

如果布局过于复杂,层级过深,不仅会影响阅读性,还会导致性能降低。Android官方给了几个优化的方法include、merge、ViewStub。

  • 标签是我们最常用的标签,能够重用布局文件。
  • 标签可以减少多余的层次结构,在UI的结构优化中起着非常重要的作用。
  • ViewStub标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。

参考文档:http://blog.csdn.net/lyric_315/article/details/52779406

Block使用优化

我所在的项目组,页面通常采用了模块化的结构,将一个页面分成几个Block,每个Block都可以通过布局文件引入,每个Block就是一个View。各个block负责维护自己的业务逻辑 。
这里写图片描述
Block的实现代码:

public class BusinessSectionBlock extends LinearLayout {
    ...
    private void init() {
      Context context = getContext();
      LayoutInflater inflater = LayoutInflater.from(context);
      mRootView = inflater.inflate(R.layout.business_list_row, BusinessSectionBlock.this);
}

Block的实现方式有很多好处,但也带来了一些问题。每一层Block都会使View层级增加一层,每一Block都会是增加一个View对象。如果某个页面使用了多个Block,Block内部又嵌套了其他Block,那这对View的渲染带来的问题也会相当可观。如下图所示,中间LinearLayout实际上是多余的一层View。

这里写图片描述

导致这个问题的原因是:Block本身就是一个ViewGroup,通过inflate方法添加视图时,又再次引入了一个ViewGroup(布局文件中的根布局)

优化思路:如果Block是一个LinearLayout,布局文件中的根布局也是一个LinearLayout,那我们是不是可以去掉布局文件中的根布局ViewGroup呢?

这个页面共包含8个Block
这里写图片描述
也就说View树中至少包含了8个多余的View对象,并且View树层级至少可以减少一层。

具体方案:

  • merge 标签删减多余或者额外的层级
    使用这个方法时需要注意,inflate()方法的最优一个参数attachToRoot必须为TRUE
 if (root == null || !attachToRoot) {
         throw new InflateException("<merge /> can be used only with a valid "
    + "ViewGroup root and attachToRoot=true");
    }
  • 在代码动态删除和添加子View
ViewGroup realView = (ViewGroup) LayoutInflater.from(context).inflate(resourceId, null);
    View[] views = new View[realView.getChildCount()];
    for (int i = 0; i < realView.getChildCount(); i++) {
        views[i] = realView.getChildAt(i);
    }

    //这里必须先将子View从父节点中删除,才能为子View重新添加到新的父View中
    for (int i = 0; i < views.length; i++) {
        View view = views[i];
        realView.removeView(view);//从根节点中删除子节点
        rootView.addView(view, i);//将子View加入到当前Block中
    }    

优化前后比较:
这里写图片描述
这里写图片描述
这里写图片描述
整体上大约节省了2ms

OverDraw优化

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。

查看OverDraw正确知识:开发者模式—-点击Debug GPU Overdraw—-选择Show overdraw areas
这里写图片描述
每个颜色的说明如下:

  • 原色:没有过度绘制
  • 蓝色:1 次过度绘制
  • 绿色:2 次过度绘制
  • 粉色:3 次过度绘制
  • 红色:4 次及以上过度绘制

最理想的情况当然是整个页面都不存在过度绘制情况,一般情况下,1~2层过度绘制是可以接受的。

过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。

经验:

1、去掉Window的默认背景

一般应用默认继承的主题都会有一个默认的 windowBackground,但是一般界面都会自己设置界面的背景颜色或者列表页则由 item 的背景来决定,所以默认的 Window 背景基本用不上,如果不移除就会导致所有界面都多 1 次绘制。

可以在应用的主题中添加如下的一行属性来移除默认的 Window 背景:

<item name="android:windowBackground">@null</item>

或者在 BaseActivity 的 onCreate() 方法中使用下面的代码移除:

getWindow().setBackgroundDrawable(null);

2、去掉布局或代码中的重复背景

eg:

//在代码中设置了背景
setBackgroundColor(getResources().getColor(R.color.datacenter_keydata_bg));

//又在子View布局中声明了背景
android:background="@color/datacenter_keydata_bg"

看看效果:
这里写图片描述
这里写图片描述

硬件加速导致的问题

硬件加速在API L14之上是默认开启的,对于基本的View绘制,通过硬件加速可以增加绘图的流程性,但是要注意的是,并不是所有的2D图形绘制API都支持硬件加速。

例子:

//在4.3记一下版本中,该方法是不能正常工作的,需要关闭硬件加速
canvas.clipPath(getFilledPath());

Canvas中的部分绘制操作支持硬件加速的API Level
这里写图片描述
由于硬件加速对某些2D绘图API的不支持,所以Android系统提供了四种级别的控制方式:Application、Activity、Window、View :

Application级别

在应用的Android清单文件中,把下列属性添加到元素中,来开启整个应用程序的硬件加速,代码如下所示:

<application android:hardwareAccelerated="true" ...>
Activity级别

如果你的应用程序开启或者关闭了全局(Application级别)的硬件加速功能,但是导致某些地方表现不一致,则可以使用Activity级别的硬件加速控制方式来对Activity进行单独控制。要启动或者禁用一个Activity的硬件加速,你可以使用activity的android:hardwareAccelerated属性。下面的一个列子使整个Application启用硬件加速,但是对一个Activity禁止使用硬件加速

<application android:hardwareAccelerated="true">

    <activity ... />
    <activity android:hardwareAccelerated="false" />

</application>
Window级别

对单个的Window,Android同样可以控制硬件加速,代码如下所示:

getWindow().setFlags(
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,          
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

but,现阶段你不能在Window级别对它禁用硬件加速。

View级别

View级别是用的最多的控制硬件加速的级别,我们可以通过如下所示的代码来禁止硬件加速,与Window级别相反,我们无法在View级别开启硬件加速。

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
其他建议

(1)在onDraw()里尽量避免分配内存、创建对象,会导致频繁的垃圾回收降低性能;可以在初始化做这些事情。

(2)减少没必要的invalidate()调用。

(3)尽可能少调用requestLayout(),requestLayout会导致系统遍历整个View树重新去measure和layout,如果layout嵌套复杂,这里也会产生性能问题。

(4)使用组合控件TextView+ImageView 可以替换为 TextView+ [left,right,top,bottom]drawab

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值