六、Android性能优化之UI卡顿分析之渲染性能优化
渲染功能是应用程序最普遍的功能,开发任何应用程序都是这样,一方面,设计师要求为用户展现可用性最高的超
然体验,另一方面,那些华丽的图片和动画,并不是在所有的设备上都能刘畅地运行。我们来了解一下什么是渲染性能。
首先,我们要知道Android系统每隔16ms就重新绘制一次Activity,也就是说,我们的应用必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60帧,然而这个每秒帧数的参数由手机硬件所决定,现在大多数手机屏幕刷新率是60赫兹(赫兹是国际单位制中频率的单位,它是每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60次=16.66ms)的时间去完成每帧的绘制逻辑操作,如果错过了,比如说我们花费34ms才完成计算,那么就会出现我们称之为丢帧的情况。
简单理解16ms应该完成所有事情
GC回收时间过长导致卡顿渲染管线
Android系统的渲染管线Android系统的渲染管线分为两个关键组件:CPU和GPU,它们共同工作,在屏幕上绘制图片,每个组件都有自身定义>的特定流程。我们必须遵守这些特定的操作规则才能达到效果。
渲染优化在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度。
在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。
接下来我们将讲解更多关于失效布局和重绘的内容,以及如何使用SDK中的工具找出拖累应用性能的原因
加快格栅化的操作想要开发一款性能优越的应用,我们必须了解底层是如何运行的。有一个主要问题就是,Activity是如何绘制到屏幕上的?那些复杂的XML布局文件和标记语言,是如何转化成用户能看懂的图像的?
实际上,这是由格栅化操作来完成的,栅格化就是将例如字符串、按钮、路径或者形状的一些高级对象,拆分到不同的像素上在屏幕上进行显示,栅格化是一个非常费时的操作。我们所有人的手机里面都有一块特殊硬件,它就是图像处理器(GPU显卡的处理器),目的就是加快栅格化的操作,GPU在上个世纪90年代被引入用来帮助加快栅格化操作。
GPU使用一些指定的基础指令集,主要是多边形和纹理,也就是图片,CPU在屏幕上绘制图像前会向GPU输入这些指令,这一过程通常使用的API就是Android的OpenGL ES,这就是说,在屏幕上绘制UI对象时无论是按钮、路径或者复选框,都需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行栅格化。
polygons多边形和textures纹理
我们要知道,一个UI对象转换为一系列多边形和纹理的过程肯定相当耗时,从CPU上传处理数据到GPU同样也很耗时。所以很明显,我们需要尽量减少对象转换的次数,以及上传数据的次数,幸亏,OpenGL ES API允许数据上传到GPU后可以对数据进行保存,当我们下次绘制一个按钮时,只需要在GPU存储器里引用它,然后告诉OpenGL如何绘制就可以了,一条经验之谈:渲染性能的优化就是尽可能地上传数据到GPU,然后尽可能长地在不修改的情况下保存数据,因为每次上传资源到GPU时,我们都会浪费宝贵的处理时间,Android系统的Honeycomb版本发布之后,整个UI渲染系统就在GPU中运行,之后各个版本都在渲染系统性能方面有更多改进。
Android系统在降低、重新利用GPU资源方面做了很多工作,这方面完全不用担心,举例说,任何我们的主题所提供的资源,例如Bitmaps、Drawables等都是一起打包到统一的纹理当中,然后使用网格工具上传到GPU,例如Nine Patches等,这样每次我需要绘制这些资源时,我们就不用做任何转换,他们已经存储在GPU中了,大大加快了这些视图类型的显示。然而随着UI对象的不断升级,渲染流程也变得越来越复杂,例如说绘制图像,就是把图片上传到CPU存储器,然后传递到GPU中进行渲染。路径使用时完全另外一码事,我们需要在CPU中创建一系列的多边形,甚至在GPU中创建掩蔽纹理来定义路径。绘制字符就更加复杂一些,首先我们需要在CPU中把字符绘制制成图像,然后把图像上传到GPU进行渲染再返回到CPU,在屏幕上为字符串的每个字符绘制一个正方形。
总结上述原因,在我们的绘制渲染机制里面比较耗时的:
1.CPU计算时间
CPU的优化,从减轻加工View对象成Polygons和Texture来下手
View Hierarchy中包涵了太多没有的View,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。
2.CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间
OpenGL ES API允许数据上传到GPU后可以对数据进行保存,缓存到display list。因此,我们平移等操作一个view是几乎不怎么耗时的。
3.GPU进行栅格化
CPU优化建议
针对CPU的优化,从减轻加工View对象成Polygons和Texture来下手:
View Hierarchy中包涵了太多的没有用的view,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。那么我们就需要利用工具进行分析。
如何找出里面没用的view呢?或者减少不必要的view嵌套。
我们利用工具:Hierarchy Viewer进行检测,优化思想是:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。
我们打开APP,然后打开Android Device Monitor,然后切换到Hierarchy Viewer面板。除了看层次结构之外,还可以看到一些耗时的信息:
Hierarchy Viewer三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。
1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。
2)黄色:渲染速度比较慢的50%。
3)红色:渲染速度非常慢。
优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。
优化前 优化后优化建议:
1.当我们的布局是用的FrameLayout的时候,我们可以把它改成merge,可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)。
2.使用ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。
GPU优化建议就是一句话:尽量避免过度绘制(overdraw)
过度绘制GPU的主要问题 -过度绘制(overdraw)
如果我们曾经粉刷过房子,我们应该知道,给墙壁粉刷工作量非常大,如果我们需要重新粉刷,第一次的粉刷就白干了。同样的道理,我们的应用程序会因为过度绘制,从而导致性能问题,如果我们想兼顾高性能和完美的设计,往往会碰到一种性能问题,即过度绘制。过度绘制是一个术语,指的是屏幕上的某个像素点在同一帧的时间内被绘制了多次。假如我们有一堆重叠的UI卡片,最接近用户的卡片在最上面,其余卡片都藏在下面,也就是说我们花大力气绘制的那些下面的卡片基本都是不可见的。
问题就在于此,因为每次像素经过渲染后,并不是用户最后看到的部分,这就是在浪费GPU的时间。目前流行的一些布局是一把双刃剑,带给我们漂亮视觉感受的同时,也造成过度绘制的问题,为了最大限度地提高应用程序的性能,我们必须尽量减少过度绘制。幸运的是,Android手机提供了查看过度绘制情况的工具,在开发者选项中打开“Show GPU overdraw”选项,手机屏幕显示会出现一些异常不用过于惊慌,Android在屏幕上使用不同颜色,标记过度绘制的区域,如果某个像素点只渲染了一次,我们看到的是它原来的颜色,随着过度绘制的增多,标记颜色也会逐渐加深,例如1倍过度绘制会被标记为蓝色,2倍、3倍、4倍过度绘制遵循同样的模式。所以当我们调试应用程序的用户界面时,目标就是尽可能的减少过度绘制,将红色区块转变成蓝色区块,为了完成目标有两种清楚过度绘制的方法,首先要从视图中清楚那些,不必要的背景和图片,他们不会在最终渲染图像中显示,记住,这些都会影响性能。其次,对视图中重叠的屏幕区域进行定义,从而降低CPU和GPU的消耗,接下来我们深入了解过度绘制
1、背景经常容易造成过度绘制。
手机开发者选项里面找到工具:Debug GPU overdraw,其中,不同颜色代表了绘制了几次:
Show GPU overdraw工具举例
由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。解决的办法:将主题添加的背景去掉:
//将主题的背景去掉
getWindow().setBackgroundDrawable(null);
又例如我们的根布局经常会设置重复的背景,那么这时候就应该去掉一些不必要的背景。
还有的就是,我们在写列表控件的时候,如果Item在没有图片的时候需要一个背景色的时候,那么我们这时候就需要灵活地利用透明色来防止过度绘制:
if (chat.getAuthor().getAvatarId() != 0) {
Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
chat_author_avatar);
}
chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
优化前
ListView item中设置图片的同时,要设置背景Color.TRANSPARENT 防止因复用导致的过度绘制
if (chat.getAuthor().getAvatarId() == 0) {
//没有头像的时候,需要把Drawable设置为透明,防止过度绘制(每次都要设置,因为Item会复用)
Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);
//没有头像的时候,需要设置默认的背景色
chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
} else {
//有头像的时候,直接设置头像,并且把背景色设置为透明,同样也是防止过度绘制
Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
chat_author_avatar);
chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);
}
优化后
发现设置的图片过度绘制颜色由红色变为绿色,减少了过渡绘制
2.自定义控件处理过度绘制。
如果我们的自定义控件存在一些被遮挡的不需要显示的区域,可以通过画布的裁剪来处理
public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();
private Paint paint = new Paint();
public DroidCardsView(Context context) {
super(context);
initCards();
}
public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (DroidCard c : mDroidCards){
drawDroidCard(canvas, c);
}
invalidate();
}
/**
* 绘制DroidCard
* @param canvas
* @param c
*/
private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}
}
裁剪前过度绘制情况
自定义控件中,通过画布的裁剪,处理掉不需要显示的区域
canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height); //裁剪函数
public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();
private Paint paint = new Paint();
public DroidCardsView(Context context) {
super(context);
initCards();
}
public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for(int i=0;i<mDroidCards.size() -1;i++){
DroidCard droidCard = mDroidCards.get(i);
drawDroidCard(canvas,droidCard,i);
}
int last = mDroidCards.size() -1;
if(last>=0){
drawDroidCard(canvas,mDroidCards.get(last));
}
invalidate();
}
/**
* 绘制DroidCard
* @param canvas
* @param c
*/
private void drawDroidCard(Canvas canvas, DroidCard c,int i) {
// canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.save();
canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height);
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.restore();//裁剪和绘制完毕后恢复画布
}
/**
* 绘制最后一张
* @param canvas
* @param c
*/
private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}
}
裁剪后过度绘制情况