Android布局优化方法-避免过度绘制

布局优化的方式大概有如下几点:

  1. 减少层级:通过合理的使用RelativeLayout 、LineaLayout 和ConstraintLayout等,具体效率这里不再阐述。
  2. 合理使用Merge
  3. ViewStub的使用
  4. 使用布局复用 include
  5. 避免过度绘制
    本文主要讲述第五条避免过度绘制。

过度绘制。

过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构(如带背景的 TextView)中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的 CPU 以及 GPU 资源。
我们一般在 XML 布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:

  • XML 布局->控件有重叠且都有设置背景
  • View 自绘->View.OnDraw 里面同一个区域被绘制多次

过度绘制检测工具

要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项-打开GPU过度绘制(Show GPU Overdraw)。做了一个重叠的布局,每个布局都设置了一个白色背景色,打开以后可以看到如下页面。
Android布局优化方法-避免过度绘制_第1张图片
从上到下的颜色一次代表了:

  • 无色(白色是因为自己设置的背景是白色):没有过度绘制,每个像素绘制了一次。
  • 蓝色(也许是紫色):每个像素多绘制了一次。大片的蓝色还是可以接受的。如果整个窗口都是蓝色,可以尝试减少一次绘制。
  • 绿色:每个像素绘制了两次。
  • 淡红色:每个像素多绘制了三次。一般来说,整个位置不超过屏幕的1/4是可以接受的。
  • 深共色:每个像素多绘制了四次或者更多。严重影响性能,需要优化避免深红色区域。
    我们的目标是尽量减少红色区域,看到更多的蓝色/白色区域。

如何避免过度绘制

(1)布局上的优化
在 XML 布局上,如果出现了过度绘制的情况,可以使用 Hierarchy View 来查看具体的层级情况,可以通过 XML 布局优化来减少层级。需要注意的是,在使用 XML 文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:

  • 移除 XML 中非必需的背景,或根据条件设置。
  • 移除 Window 默认的背景。
  • 按需显示占位背景图片。

使用 Android 自带的一些主题时,activity 往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView 的背景此时对我们来说是无用的,但是它会产生一次 Overdraw。因此没有必要的话,也可以移除,代码如下:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().setBackgroundDrawable(null);
    }

(2)自定义View的优化
事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义 View 来实现,虽然 自定义 View 减 少了 Layout 的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义 View(通常重写了 onDraw 方法),Android 系统无法检测在 onDraw 中具体会执行什么操作,无法监控并自动优化,也就无法避免 Overdraw 了。但是在自定义 View 中可以通过 canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义 View 来控制显示的区域。clipRect 方法还可以帮助节约 CPU 与 GPU 资源,在 clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用 canvas.quickreject ()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义 View 避免 OverDraw 的案例。

  • 下面是四张图片叠加的效果。我选择的是四张白色的图片,打开查看过度绘制后,首先是正常未处理过度绘制的样子:可以看下图,有浅红色过度绘制的区域
    Android布局优化方法-避免过度绘制_第2张图片
  • 再看经过处理的效果
    Android布局优化方法-避免过度绘制_第3张图片
    效果是很明显的,接下来是自定义view处理过度绘制的代码:
// 卡片封装类
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);
    }
}

接下来是自定义View

public class MultiCardsView extends View {

    private ArrayList cardsList = new ArrayList<>();

    public boolean isEnableOverdrawOpt() {
        return enableOverdrawOpt;
    }

    private boolean enableOverdrawOpt = true;


    public MultiCardsView(Context context) {
        this(context, null);
    }

    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 setEnableOverdrawOpt(boolean enableOverdrawOpt) {
        this.enableOverdrawOpt = enableOverdrawOpt;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (canvas == null || cardsList == null || cardsList.size() == 0) {
            return;
        }
        Rect clip = canvas.getClipBounds();
        if (enableOverdrawOpt) {
            drawCardWithoutOverdraw(canvas, cardsList.size() - 1);
        } else {
            drawCardNormal(canvas, cardsList.size() - 1);
        }

    }

    private void drawCardNormal(Canvas canvas, int i) {
        if (canvas == null || i < 0 || i >= cardsList.size()) {
            return;
        }
        SingleCard card = cardsList.get(i);
        if (card != null) {
            drawCardNormal(canvas, i - 1);
            card.draw(canvas);
        }
    }

    private void drawCardWithoutOverdraw(Canvas canvas, int i) {
        if (canvas == null || i < 0 || i >= cardsList.size()) {
            return;
        }
        SingleCard card = cardsList.get(i);
        // 判断是否和其他卡片相交 从而跳过那些非矩形区域内的绘制
        if (card != null && !canvas.quickReject(card.area, Canvas.EdgeType.BW)) {
            int saveCount = canvas.save();
            // 只绘制可见区域
            if (canvas.clipRect(card.area, Region.Op.DIFFERENCE)) {
                drawCardWithoutOverdraw(canvas, i - 1);
            }
            canvas.restoreToCount(saveCount);
            saveCount = canvas.save();
            //只绘制可见区域
            if (canvas.clipRect(card.area)) {
                Rect clip = canvas.getClipBounds();
                card.draw(canvas);
            }
            canvas.restoreToCount(saveCount);
        } else {
            drawCardWithoutOverdraw(canvas, i - 1);
        }


    }
}

然后是在Activity中引用



    
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().setBackgroundDrawable(null);


        final MultiCardsView cardsView = findViewById(R.id.cardView);
        cardsView.setEnableOverdrawOpt(true);

        int width = getResources().getDisplayMetrics().widthPixels;
        int height = getResources().getDisplayMetrics().heightPixels;

        int cardWidth = width / 3;
        final int cardHeight = height / 3;
        int yOffset = 40;
        int xOffset = 40;

        Integer[] ids = new Integer[]{R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background};
        for (int i = 0; i < ids.length; i++) {
            SingleCard singleCard = new SingleCard(new RectF(xOffset, yOffset, xOffset + cardWidth, yOffset + cardHeight));
            Bitmap bitmap = getBitmap(this, R.drawable.aaa);
            singleCard.setBitmap(bitmap);
            cardsView.addCards(singleCard);
            xOffset += cardWidth / 3;
        }

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cardsView.setEnableOverdrawOpt(!cardsView.isEnableOverdrawOpt());
            }
        });
    }

    private static Bitmap getBitmap(Context context, int vectorDrawableId) {
        Bitmap bitmap = null;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
            Drawable vectorDrawable = context.getDrawable(vectorDrawableId);
            bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
                    vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            vectorDrawable.draw(canvas);
        } else {
            bitmap = BitmapFactory.decodeResource(context.getResources(), vectorDrawableId);
        }
        return bitmap;
    }
}

  • 上面的代码有两个方法
    (1)QuickReject:快速判断Canvas是否需要绘制。在绘制一个单元前,先判断是否在Canvas的剪切区域内,若不在则直接返回。
    (2)Canvas.ClipRect:避免绘制越界。每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op.INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。

你可能感兴趣的:(Android性能优化)