IBigerBiger的成长之路

Android性能优化(二)-自定义View优化

随着业务的增长,那么普通的布局和View在某些特殊情况下是不能满足需求的,所以对于开发者来说掌握一手自定义View的开发是很重要的事情,相信开始注意性能相关的小伙伴,应该都可以有一手开发自定义View的技能了,但是对于自定义View这种刷新率比较高的控件来说,如果不注意一些细节的话,其实很容易出现性能问题的。

至于怎么解决自定义View的性能问题,这里简单提一些需要开发中注意的点

一.降低刷新频率

为了提高view的运行速度,我们需要减少频繁调用的代码里的不必要的代码。在onDraw()方法中我们应该减少冗余代码,冗余代码会带来使我们的自定义view的垃圾回收产生问题。

所以我们要避免在onDraw()方法里面创建对象,这里包含我们可能由于业务需求的对象,也可能是Paint、Path、Shader等这些绘图对象(特别是这些绘图对象只需要在初始化的时候创建一次就可以了)。

对于onDraw()方法我们也要避免频繁调用它,因为onDraw()方法主要都是绘图的过程,这其实是开销很大的部分,如果太过于频繁,会导致卡死的情况产生,大部分时候调用 onDraw()方法就是调用invalidate()的结果,所以减少不必要的调用invalidate()方法。情况允许的情况下,调用四种参数不同类型的invalidate(),而不是调用无参的版本。无参需invalidate要刷新整个view,而四种参数类型的变量只需刷新指定部分的view.这种调用会更加高效,避免了落在矩形屏幕外的不必要刷新的页面。

当然刷新界面除了invalidate()方法还有requestLayout()方法,那么这两者有什么区别呢?

  • invalidate 当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

  • requestLayout 子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

另外还有postInvalidate()方法,这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。

所以说如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、布局、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效。

当布局处于很复杂的状态,我们其实可以选择用自定义ViewGroup来替代自定义View,因为自定义ViewGroup可以对于子控件的尺寸形状进行特定的假想,同时避免子类的尺寸计算。

二.clipRect避免不必要绘制

其实上面说的用invalidate带参数方法就是为了避免不必要地方的绘制,但是就算我们定义了绘制区域,但是在绘制区域内还是会出现各种重叠绘制的情况,

我们来举个栗子(来源Google性能优化):

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



多张卡片叠加,那么如果你是一张一张卡片从左到右的绘制,效果肯定没问题,但是叠加的区域肯定是过度绘制了。

接下来通过实例展示如下



代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CardView extends View
{
private Bitmap[] mCards = new Bitmap[3];
private int[] mImgId = new int[]{R.drawable.alex, R.drawable.chris, R.drawable.claire};
public CardView(Context context)
{
super(context);
Bitmap bm = null;
for (int i = 0; i < mCards.length; i++)
{
bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
mCards[i] = Bitmap.createScaledBitmap(bm, 400, 600, false);
}
setBackgroundColor(0xff00ff00);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
canvas.save();
canvas.translate(20, 120);
for (Bitmap bitmap : mCards)
{
canvas.translate(120, 0);
canvas.drawBitmap(bitmap, 0, 0, null);
}
canvas.restore();
}
}

这是叠加的效果图,看起来貌似没问题的,但是当我们打开Show Override GPU之后的效果效果如下



可以看到卡片叠加处明显的过度渲染了

那么其实我们只需要绘制第一张图片与第二张图片的展示部分与第三张图片就可以了,那些重叠的地方则不需要去绘制,那么怎么只绘制显示的部分呢?标题说明一切,clipRect方法就可以解决这个问题,

那么我们通过clipRect方法来优化一下onDraw方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
canvas.save();
canvas.translate(20, 120);
for (int i = 0; i < mCards.length; i++)
{
canvas.translate(120, 0);
canvas.save();
if (i < mCards.length - 1)
{
canvas.clipRect(0, 0, 120, mCards[i].getHeight());
}
canvas.drawBitmap(mCards[i], 0, 0, null);
canvas.restore();
}
canvas.restore();
}

除了最后一张需要完整的绘制,其他的都只需要绘制部分;所以我们在循环的时候,给i到n-1都添加了clipRect的代码。

最后效果图如下



可以看到现在的Override情况是比较好的,自然性能上会有所提升

三.适当使用硬件加速

Android从3.0(API Level 11)开始,在绘制View的时候支持硬件加速,充分利用GPU的特性,使得绘制更加平滑,但是会多消耗一些内存。硬件加速在target api大于等于14的情况下,是默认开启。

使用硬件加速其实就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。

那么我们需要了解一下CPU与GPU在绘制过程中的差异性

我们看一下CPU与GPU结构对比图



其中:

  • 黄色的Control为控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;

  • 绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;

  • 橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。

从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少。因此CPU擅长各种复杂的逻辑控制,但不擅长数学尤其是浮点运算。GPU就是为实现大量数学运算设计的。从结构图中可以看到,GPU的控制器比较简单,但包含了大量ALU。GPU中的ALU使用了并行设计,且具有较多浮点运算单元。

所以通过上面的结论我们可以在开发中根据实际的情况开选择是否开启硬件加速,当我们是控制逻辑居多的时候其实可以不使用硬件加速,当以图形浮点计算居多的时候选择开启硬件加速,掌握好这个度很重要

另外部分的绘图方法是不支持硬件加速的,大家可以通过这个地址查看使用的绘图方法是否支持硬件加深

写在后面的话

那么到这里就结束了关于自定义View的相关优化,但是其实很多我们代码上的习惯也会产生性能问题,这些问题不仅仅在自定义View中出现,所以不再自定义View这一篇中说明了,等后面文章会具体说明这些平时代码可能出现的问题,peace~~~