2.4 避免过度绘制
过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。
当设计上追求更华丽的视觉效果时,我们很容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳性能,必须尽量减少Overdraw情况发生。
我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:
XML布局->控件有重叠且都有设置背景
View自绘-> View.OnDraw里面同一个区域被绘制多次
2.4.1 过度绘制检测工具
要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项,打开Show GPU Overdraw选项,打开后会有不同的颜色区域表示不同的过度绘制次数,如图2-34所示。
具体步骤如下:
1)系统版本要求:需要Android 4.1以上版本。
2)在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。
3)在设置时,如果有App已经打开,需要终止App进程,重新启动。
4)然后即可通过界面的颜色判断界面重绘的严重程度。
打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下:
无色:没有过度绘制,每个像素绘制了1次。
蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。
绿色:每个像素多绘制了2次。
淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。
深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。
我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。
2.4.2 如何避免过度绘制
1.?布局上的优化
在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:
移除XML中非必需的背景,或根据条件设置。
移除Window默认的背景。
按需显示占位背景图片。
使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.getWindow().setBackgroundDrawable(null);
}
针对ListView中的Avatar ImageView的设置,在getView的代码中,判断是否获取对应的Bitmap,获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。
2.?自定义View优化
事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法),Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。但是在自定义View中可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。
2.4.3 案例:无过度绘制View的实现
我们来实现一个四张图片叠加的自定义View。为了方便看到效果,这四张图片都有一定的重合区域,如果直接绘制,由于系统是不知道有重合区域,就会导致过度绘制,打开Show GPU Overdraw后看到如图2-35所示的效果图,可看出叠加层次越多,过度绘制就越严重。
从图2-35中可以看出,重叠部分都有过度绘制的情况,接下来通过一个例子来避免这种情况,以下代码是描述其中一张图片的类。
public class SingleCard {
public RectF area;
private Bitmap bitmap;
private Paint paint = new Paint();
public SingleCard(RectF area) {
this.area = area;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, null, area, paint);
}
}
实现布局的Fragment及控制代码OverDrawFragement如代码清单2-5所示。
代码清单2-5 OverDrawFragement
protected View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fm_overdraw, container, false);
multicardsView = (MultiCardsView) view.findViewById(R.id.cardview);
multicardsView.enableOverdrawOpt(true);
int width = getResources().getDisplayMetrics().widthPixels;
int height = getResources().getDisplayMetrics().heightPixels;
int cardWidth = width /3;
int cardHeight = height /3;
int yOffset = 40;
int xOffset = 40;
for (int i = 0; i < cardResId.length; i++) {
SingleCard cd = new SingleCard(new RectF(xOffset, yOffset, xOffset + card
Width, yOffset + cardHeight));
Bitmap bitmap = loadImageResource(cardResId[i], cardWidth, cardHeight);
cd.setBitmap(bitmap);
multicardsView.addCards(cd);
xOffset += cardWidth / 3;
}
Button overdraw = (Button) view.findViewById(R.id.btn_overdraw);
overdraw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multicardsView.enableOverdrawOpt(false);
}
}
);
Button perfectdraw = (Button)
view.findViewById(R.id.btn_perfectdraw);
perfectdraw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multicardsView.enableOverdrawOpt(true);
}
}
);
return view;
}
实现图片叠加的自定义View:MultiCardsView,如代码清单2-6所示。
代码清单2-6 MultiCardsView
public class MultiCardsView extends View{
private ArrayList<SingleCard> cardsList = new
ArrayList<SingleCard>(5);
private boolean enableOverdrawOpt = true;
public MultiCardsView(Context context) {
this(context, null, 0);
}
public MultiCardsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiCardsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void addCards(SingleCard card) {
cardsList.add(card);
}
// 设置是否消除过度绘制
public void enableOverdrawOpt(boolean enableOrNot) {
this.enableOverdrawOpt = enableOrNot;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cardsList == null || canvas == null)
return;
Rect clip = canvas.getClipBounds();
GLog.d("draw", String.format("clip bounds %d %d %d %d", clip.left,
clip.top, clip.right, clip.bottom));
// 根据enableOverdrawOpt值来调用不同的绘制方法,对比效果
if (enableOverdrawOpt) {
drawCardsWithotOverDraw(canvas, cardsList.size() - 1);
} else {
drawCardsNormal(canvas, cardsList.size() - 1);
}
}
// 实现没有过度绘制的方法
protected void drawCardsWithotOverDraw(Canvas canvas, int index) {
if (canvas == null || index < 0 || index >= cardsList.size())
return;
SingleCard card = cardsList.get(index);
// 判断是否没和某个卡片相交,从而跳过那些非矩形区域内的绘制操作
if (card != null && !canvas.quickReject(card.area, Canvas.EdgeType.BW)) {
int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
// 只绘制可见区域
if (canvas.clipRect(card.area, Region.Op.DIFFERENCE)) {
drawCardsWithotOverDraw(canvas, index - 1);
}
canvas.restoreToCount(saveCount);
saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
// 只绘制可见区域
if (canvas.clipRect(card.area)) {
Rect clip = canvas.getClipBounds();
card.draw(canvas);
}
canvas.restoreToCount(saveCount);
}else{
drawCardsWithotOverDraw(canvas, index - 1);
}
}
// 普通绘制
protected void drawCardsNormal(Canvas canvas, int index) {
if (canvas == null || index < 0 || index >= cardsList.size())
return;
SingleCard card = cardsList.get(index);
if (card != null) {
drawCardsNormal(canvas, index - 1);
card.draw(canvas);
}
}
}
代码清单2-6在OnDraw时调用了两个不同的方法,常用绘制方法drawCards-Normal()以及无过度绘制方法drawCards-WithotOverDraw(),效果如图2-36所示。
可以看出,使用drawCardsWithotOver-Draw()避免了过度绘制,从代码清单2-6中可以看到,调用了两个关键的方法:
快速判断Canvas是否需要绘制:Canvas. QuickReject。
在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。
避免绘制越界:Canvas.ClipRect。
每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op. INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
这个案例可以避免层次很多的自定义View导致过度绘制的问题。
在listview或其他容器控件中,itemview如果比较复杂,建议实现成一个自绘View,使用此案例来绘制可以使listview滑动更流畅。