Android UI性能优化——过度绘制篇

过度绘制(overdraw)

过度绘制介绍

每过几年,就会有传闻说某个博物馆在用x光扫描一副无价的名画之后,发现画作的作者其实重用了老的画布,在名画的底下还藏着另一副没有被发现的画作。有时候,博物馆还能用高级的图像技术来还原画布上的原作。android里面的view的绘制就是类似的情况。当android系统绘制屏幕的时候,先画父view,然后子view,再是更深的子view等等。这会导致所有的view都被绘制到了屏幕上,就像画家的画布一样,这些view都被他们的子view覆盖住了。

简单来说,过度绘制就是屏幕上的某个像素点在同一帧的时间内绘制了多次。

当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少 Overdraw 的情况发生。

当我们来绘制一个界面时,会有一个 windows,然后是建立 Activity,在 Activity 里可以建立多个 view,或 view group,view 也可以嵌套 view。这些组件从上到下分布,上面的组件是可以被用户看见的,而在下面的组件是不可见的,但是我们依然要花很多时间去绘制那些不可见的组件,因为在某些时候,它也可能会显示出来。

过度绘制检测

按照以下步骤打开 Show GPU Overrdraw 的选项:设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制


蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,

蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。
绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
淡红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。
深红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

我们的目标就是尽量减少红色 Overdraw,最理想的是蓝色,一个像素只绘制一次,合格的页面绘制是白色、蓝色为主,绿色以上区域不能超过整个的三分之一,颜色越浅越好。

可能有些人觉得不以为然,觉得没什么影响。话又说回来,GPU 绘制过渡对应用造成什么影响。
实际上,GPU 绘制影响的是界面的流畅度和用户体验,对于好的手机可能体验不到差距,对于差的手机,流畅度却起着关键的作用。

背景过度绘制优化

下面我们来通过一个Demo来检测过度绘制:

  • activity_overdraw :

    <?xml version="1.0" encoding="utf-8"?>          
  • Recyclerview item 的布局文件 :

    <?xml version="1.0" encoding="utf-8"?>                                                            
  • Activity 代码:

    public class OverdrawActivity extends AppCompatActivity {    private static final String TAG = "OverdrawActivity";    @BindView(R.id.overdraw_list)    RecyclerView mRecyclerView;    private OverdrawAdapter mAdapter;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_overdraw);        ButterKnife.bind(this);        initOverdrawView();        initOverdrawData();    }    private void initOverdrawView() {        mAdapter = new OverdrawAdapter(this);        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));        mRecyclerView.setAdapter(mAdapter);    }    private void initOverdrawData() {        List list = new ArrayList<>();        for (int i = 0; i < 15; i++) {            TestItemBean bean = new TestItemBean();            bean.setTitle("Title " + i);            bean.setTime("Time " + i);            bean.setMsg("Msg is ba la ba la ba la ba la ba la!" + i);            bean.setImgRes(R.mipmap.ic_launcher);            list.add(bean);        }        mAdapter.addItems(list);    }}
  • Recyclerview Adapter 代码:

    public class OverdrawAdapter extends CommonRecyclerAdapter {    public OverdrawAdapter(Context context) {        super(context);    }    @Override    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View view = mLayoutInflater.inflate(R.layout.layout_recycler_item, parent, false);        return new OverdrawViewHolder(view, mItemClickListener);    }    @Override    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {        bindView((OverdrawViewHolder) holder, getContentList().get(position));    }    private void bindView(OverdrawViewHolder holder, TestItemBean bean) {        if (holder != null && bean != null) {            holder.setTitleTxt(bean.getTitle());            holder.setTimeTxt(bean.getTime());            holder.setMsgTxt(bean.getMsg());            holder.setImageView(bean.getImgRes());        }    }    class OverdrawViewHolder extends RecyclerViewHolder {        @BindView(R.id.item_title)        TextView mTitleTxt;        @BindView(R.id.item_time)        TextView mTimeTxt;        @BindView(R.id.item_msg)        TextView mMsgTxt;        @BindView(R.id.item_img)        ImageView mImageView;        public OverdrawViewHolder(View itemView, OnItemClickListener listener) {            super(itemView, listener);            ButterKnife.bind(this, itemView);        }        public void setTitleTxt(String titleTxt) {            mTitleTxt.setText(titleTxt);        }        public void setTimeTxt(String timeTxt) {            mTimeTxt.setText(timeTxt);        }        public void setMsgTxt(String msgTxt) {            mMsgTxt.setText(msgTxt);        }        public void setImageView(int res) {            mImageView.setImageResource(res);            mImageView.setBackgroundColor(Color.WHITE);        }    }}
  • 整体效果以及开启过度绘制效果图:


对比上面的参照图,可以发现一个简单的 RecyclerView 展示 Item,竟然很多地方被过度绘制了4X 。 那么,其实主要原因是由于该布局文件中存在很多不必要的背景,仔细看上述的布局文件,那么开始移除吧。

  • 不必要的Background 1
    主布局的文件已经是 background 为 white 了,那么可以移除ListView的白色背景;

  • 不必要的Background 2
    主布局的文件已经是 background 为 white 了,Item主布局中的 LinearLayout 的白色背景可以移除;

  • 不必要的Background 3
    主布局的文件已经是 background 为 white 了,Item 文字布局中的 LinearLayout 的白色背景可以移除;

  • 不必要的Background 4
    主布局的文件已经是 background 为 white 了,Item 文字布局中的 RelativeLayout 的白色背景可以移除;

  • 不必要的Background 5
    主布局的文件已经是 background 为 white 了,Item 文字布局中下方的 TextView 的白色背景可以移除;

  • 不必要的Background 6
    Adapter的Bingview时,ImageView 的白色背景可以移除;

  • 不必要的Background 7
    最后一个,这个也是非常容易被忽略的,记得我们之前说,我们的这个 Activity 要求背景色是白色,我们的确在 layout 中去设置了背景色白色,那么这里注意下,我们的 Activity 的布局最终会添加在 DecorView 中,这个 View 会中的背景是不是就没有必要了,所以我们希望调用 mDecor.setWindowBackground(drawable);,那么可以在 Activity 调用 getWindow().setBackgroundDrawable(null); 或者 getWindow().setBackgroundDrawableResource(android.R.color.transparent);

    setContentView(R.layout.activity_overdraw);getWindow().setBackgroundDrawable(null);// getWindow().setBackgroundDrawableResource(android.R.color.transparent);

    下面有效果提,ActionBar上的蓝色已经消失了。
    这里有个比较重要注意点,使用了这个后一定要在自己的layout里面加上背景,有的机型会出现问题。

好了,优化了不必要的背景后,我们来看看优化的效果:

绘图过度绘制优化

下面我们继续Demo走起:

  • CardView 代码 :

    public class CardView extends View {    private Bitmap[] mCards = new Bitmap[3];    private int[] mImgId = new int[]{R.mipmap.a, R.mipmap.b, R.mipmap.c};    public CardView(Context context) {        super(context);        for (int i = 0; i < mCards.length; i++) {            Bitmap bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);            mCards[i] = Bitmap.createScaledBitmap(bm, 510, 510, false);        }    setBackgroundColor(Color.WHITE);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        canvas.save();        canvas.translate(50, 200);        for (Bitmap bitmap : mCards) {            canvas.translate(150, 0);            canvas.drawBitmap(bitmap, 0, 0, null);        }        canvas.restore();    }}
  • Activity 代码 :

    public class ClipOverdrawActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(new CardView(this));    }}
  • 整体效果以及开启过度绘制效果图:


对比上面的参照图,可以发现一个简单的三个图片展示,竟然也过度绘制了4X 。 那么,其实主要原因是由于该三个图片draw的时候绘制了全部区域导致互相重叠,仔细看上述的代码,那么开始优化吧。

  • 修改onDraw方法 :

    @Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    canvas.save();    canvas.translate(50, 200);    for (int i = 0; i < mCards.length; i++) {        canvas.translate(150, 0);        canvas.save();        if (i < mCards.length - 1) {            canvas.clipRect(0, 0, 150, mCards[i].getHeight());        }        canvas.drawBitmap(mCards[i], 0, 0, null);        canvas.restore();    }    canvas.restore();}

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

  • 去除windowbackground :

    getWindow().setBackgroundDrawable(null);// getWindow().setBackgroundDrawableResource(android.R.color.transparent);

优化了以上两点,可以看一下效果:

所以遇到类似的需要绘制的自定义view时,大家要多注意这方面的问题,这里只是举例clipRect方法,还有比如画两个圆重叠时,可以画一个圆以及一个圆环,以此类推,这样可以大大的减少过度绘制,优化UI性能。

过度绘制(overdraw)优化总结

  • 在布局中,如果存在多个线性布局重叠时,可以考虑只针对最上层的布局设置背景色,而不需要每一个布局(例如LinearLayout)都设置背景色,过多的相同的背景色会导致过度绘制;
  • 在设计到activity类中,如果牵涉到的布局存在背景色,可以考虑消除窗口的背景色,减少1X的绘制。
  • 在自定义view的onDraw中,如果涉及到重叠的绘制view时,可以考虑利用局部绘制避免过度绘制。
  • 考虑到效率和性能问题,界面是有一定刷新频率的,每一次刷新都会调用View的onDraw方法,而View提前绘制就是在onDraw中进行,避免在onDraw创建对象,避免在onDraw进行绘制,应在构造函数中画好,交给onDraw。

更多相关文章

  1. Android给布局设置阴影
  2. Android(安卓)OpenGLES2.0(五)——绘制立方体
  3. View那些事儿(1) -- View绘制的整体流程
  4. Android(安卓)首字母索引
  5. android listview 一行高亮
  6. Android最佳性能实践(四)——布局优化技巧
  7. android相对布局中@id和@+id的区别(原理)
  8. 【react-natvie】react-native Android(安卓)webview下h5使用rem
  9. 设置AlertDialog的列表样式

随机推荐

  1. Android(安卓)UI 问题随笔
  2. android 在xml中指定按钮点击事件
  3. Android(安卓)SDK和NDK区别
  4. 异步加载注意的事项
  5. Android(安卓)从一个应用打开另一个应用
  6. Android踩坑日记:FloatingActionButton的
  7. Android——Fragment A点击一个按钮跳转
  8. android 6.0(api 23) SDK,不再提供org.apa
  9. android事件分发(三)重要的函数requestDisa
  10. Android(安卓)消息推送通知指南