每过几年,就会有传闻说某个博物馆在用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 :
Recyclerview item 的布局文件 :
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方法 :
@Override
protected 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性能。