性能优化之渲染性能优化

1 知识储备

CPU::中央处理器,它集成了运算,缓冲,控制等单元,包括绘图功能。CPU将对象处理为多维图形、纹理(Bitmaps、Drawables等都是一起打包到统一的纹理)。
  GPU:一个类似于CPU的专门用来处理Graphics的处理器,作用用来帮助加快格栅化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制。
  OpenGL ES:是手持嵌入式设备的3DAPI,跨平台的、功能完善的2D和3D图形应用程序接口API,有一套固定渲染管线流程.。
  DisplayList在Android把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在DisplayList的帮助下完成的,DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
  格栅化:是将Button,Shape,Path,String,Bitmap等组件等矢量资源,转化为一格格像素点的像素图,显示到屏幕上(硬件是:图像处理器,GPU显卡的处理器),过程图如下:

  垂直同步VSYNC:让显卡的运算和显示器刷新率一致以稳定输出的画面质量。它告知GPU在载入新帧之前,要等待屏幕绘制完成前一帧。下面的三张图分别是GPU和硬件同步所发生的情况。①Refresh Rate:屏幕一秒内刷新屏幕的次数,由硬件决定,例如60Hz。②Frame Rate:GPU一秒绘制操作的帧数,比如:60fps。正常情况过程图如下:

2 渲染机制分析

2.1 渲染管线

Android系统的渲染管线分为两个关键组件:CPU和GPU,它们共同工作,在屏幕上绘制图片,每个组件都有自身定义的特定流程。我们必须遵守这些特定的操作规则才能达到效果。
性能优化之渲染性能优化_第1张图片
  在CPU方面,最常见的性能问题是:不必要和失效的布局,这些内容必须在视图层次结构中进行测量、清除并重新创建。引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度。
  在GPU方面,最常见的问题是我们所说的:过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。
性能优化之渲染性能优化_第2张图片
  CPU在屏幕上绘制图像前会向GPU输入指定的基础指令集,GPU使用这些指令集,主要是多边形和纹理(图片),这一过程通常使用的API就是Android的OpenGL ES。这就是说,在屏幕上绘制UI对象时无论是按钮、路径或者复选框,首先需要在CPU中计算并转换为Polygons和Texture(多边形和纹理),然后使用OpenGL ES将计算好的Polygons和Texture传递到GPU,最后GPU再进行格栅化。

2.2 渲染流程线

UI对象----->CPU处理为多维图形,纹理----->通过OpeGL ES接口调用GPU-----> GPU对图进行光栅化(Frame Rate )----->硬件时钟(Refresh Rate)----->垂直同步----->投射到屏幕。
性能优化之渲染性能优化_第3张图片
图片来源于:深入Android渲染机制

2.3 渲染时间线

Android系统每隔16ms发出VSYNC信号(1000ms/60=16.66ms),触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。
  正常情况:
性能优化之渲染性能优化_第4张图片
  渲染超时,计算渲染时间超过16ms。当这一帧画面渲染时间超过16ms的时候,垂直同步机制会让显示器硬件等待GPU完成栅格化渲染操作,这样会让这一帧画面,多停留了16ms、甚至更多。这样就这造成了用户看起来画面停顿。
  当GPU渲染速度过慢,就会导致如下情况,某些帧显示的画面内容就会与上一帧的画面相同:
  

3 渲染耗时的三个过程与优化方向

3.1 计算CPU渲染的耗时

优化方向:CPU的优化,从减轻加工View对象成Polygons(多边形)和Texture(纹理)来下手,测量、清除并重新创建不必要和失效的布局
  任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一 系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子:当View的大小发生改变,DisplayList就会重新创建,然后再渲染;而当View发生位移,则DisplayList不会重新创建,而是执行重新渲染的操作。当你的View过于复杂,操作又过于复杂,就会计算渲染时间超过16ms,产生卡顿问题。
  渲染耗时呈现工具—GPU呈现模式分析。每一条柱状线都包含三部分:蓝色代表测量绘制Display List的时间红色代表OpenGL渲染Display List所需要的时间黄色代表CPU等待GPU处理的时间。中间有一根绿色的横线,代表16ms,需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
性能优化之渲染性能优化_第5张图片

3.2 CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间

优化方向:OpenGL ES API允许数据上传到GPU后可以对数据进行保存,做了缓存。

3.3 避免GPU进行格栅化时,过度绘制

优化方向:尽量避免过度绘制(overdraw)
  GPU的绘制过程,就跟刷墙一样,一层层的进行,16ms刷一次。这样就会造成图层覆盖的现象,即无用的图层还被绘制在底层,造成不必要的浪费。
性能优化之渲染性能优化_第6张图片
  过度绘制查看工具使用步骤:设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制。图例展示过度渲染:
性能优化之渲染性能优化_第7张图片
  随着过度绘制的增多,标记颜色也会逐渐加深,例如1倍过度绘制会被标记为蓝色,2倍、3倍、4倍过度绘制遵循同样的模式。

4 渲染优化方法1——测量、清除并重新创建不必要和失效的布局

4.1 Hierarchy Viewer层次布局工具使用

4.1.1 减少不必要的层次,巧用Hierarchy Viewer工具

(1)Hierarchy Viewer在哪?
性能优化之渲染性能优化_第8张图片
(2)如何使用?一图胜千言(建议在模拟器中使用)
性能优化之渲染性能优化_第9张图片

4.1.2 无法显示3个圈圈怎么办?

(1)报错,提示如下log:

[2016-11-20 15:59:49 - ViewServerDevice]Unable to debug device: nem_ul10-52F4C16425002867

(2)解决方法:将开发者模式关闭,再重新打开。
性能优化之渲染性能优化_第10张图片

4.1.3 无法连接真机调试怎么办?

Android的官方文档中,有这么一句话:出于安全考虑,Hierarchy Viewer只能连接Android开发版手机或是模拟器。推荐方法:谷歌大神romainguy在github的项目ViewServer,可以实现用HierarchyViewer工具对查看和调试应用的UI。。
  可以下载下来导入到IDE中,里面有个ViewServer的类,类注释上也标注了用法,引入方法有两种:

// 1 添加依赖
dependencies {
    compile project(':viewserver')
}
	
// 2 将ViewServer.java添加到项目中

在BaseActivity中或者希望调试的Activity以下该三个方法中,添加几行代码:

public class BaseActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewServer.get(this).addWindow(this);
    }
    public void onResume() {
        super.onResume();
        ViewServer.get(this).setFocusedWindow(this);
    }
    public void onDestroy() {
        super.onDestroy();
        ViewServer.get(this).removeWindow(this);
    }
}

4.1.4 关注下面的三个圆圈

从左到右依次,代表View的:Measure, Layout和Draw的性能,不同颜色代表不同的性能等级:
(1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。
(2)黄色:渲染速度比较慢的50%。
(3)红色:渲染速度非常慢。

4.1.5 测量结果分析

红色节点是代表应用性能慢的一个潜在问题,下面是几个例子,如何来分析和解释红点的出现原因
(1)如果视图层级结构中的根视图,Messure阶段为红色,Layout阶段为红色,Draw阶段为黄色,这个是比较常见的,因为这个节点是所有其它视图的父类;
(2)如果一个视图组里面有许多的子节点,并且测量阶段呈现为红色,则需要观察下子节点的绘制情况
(3)如果在叶节点或者ViewGroup中,只有极少的子节点,这可能在设备上运行并不慢,但是需要指出为什么这个节点是红色的,可以借助Systrace或者Traceview工具,获取更多额外的信息
(4)如果视图结构中的一个叶子节点,有20个视图是红色的Draw阶段,这是有问题的,需要检查代码里面的onDraw方法

4.2 优化布局建议

4.2.1 布局层级扁平化

查看耗时情况和布局树的深度的工具,提升布局性能的关键点是:尽量保持布局层级的扁平化,避免出现重复的嵌套布局。使用线性布局LinearLayout导致布局层次变深,如果有这类问题,使用相对布局RelativeLayout代替LinearLayout,减少布局的层次;因为RelativeLayout的版本更优于LinearLayout的写法。
  例如下面的例子,有2行显示相同内容的视图,分别用两种不同的写法来实现,他们有着不同的层级。
性能优化之渲染性能优化_第11张图片

4.2.2 每一个layout的外层父容器是否需要?

没有用的根布局是指没有背景绘制或者没有大小限制的根布局,这样的布局不会对UI效果产生任何影响。我们可以**把没有用的根布局,通过merge标签合并来减少层次;把没有用的父布局,删除。**以下是例子:当它的布局代码如下:



    
        
            

性能优化之渲染性能优化_第12张图片
  把没有用的根布局,通过merge标签合并来减少层次;把没有用的父布局,删除后:



    

性能优化之渲染性能优化_第13张图片
  merge标签即是合并。merge必须放在布局文件的根节点上;merge并不是ViewGroup,也不是View,对merge标签设置的所有属性都是无效的。如果是merge标签,那么直接将其中的子元素添加到merge标签parent中,这样就保证了不会引入额外的层级。所以:
  
(1)merge只能作为XML布局的根标签使用;

//LayoutInflate类的源码说明:
} else if (TAG_MERGE.equals(name)) {
    // 如果merge不是根节点,报错
    throw new InflateException(" must be the root element");
}

(2)因为merge不是View,所以对merge标签设置的所有属性都是无效的。

(3)因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。如:

//layoutInflater.inflate(parser, root, true)
layoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

(4)如果Activity的布局文件根节点是FrameLayout,可以替换为merge标签,这样,执行setContentView之后,会减少一层FrameLayout节点;
  ①根节点是FrameLayout的效果



    

性能优化之渲染性能优化_第14张图片
  ②根节点是merge的效果



    

性能优化之渲染性能优化_第15张图片

(5)自定义View如果继承LinearLayout,建议让自定义View的布局文件根节点设置成merge,这样能少一层节点。
  ①根节点是FrameLayout的效果



    
    

//自定义的View,竖直方向的LinearLayout
public class MergeLayout extends LinearLayout {
    public MergeLayout(Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.merge_activity, this, true);
    }
}

性能优化之渲染性能优化_第16张图片
  ②根节点是merge的效果



xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    

//自定义的View,竖直方向的LinearLayout
public class MergeLayout extends LinearLayout {
    public MergeLayout(Context context) {
        super(context);
        // 设置为数值方向的布局
        setOrientation(VERTICAL);
        LayoutInflater.from(context).inflate(R.layout.merge_activity, this, true);
    }
}

性能优化之渲染性能优化_第17张图片

(6)学习链接:Android 布局优化之include与merge

4.2.3 include布局重用

在Android的应用程序开发中,标题栏是必不可少的一个元素,大部分页面都要用到,而且布局都是一样的,这时候使用include标签就显得极其的方便。使用时通常需要注意以下几点:

include标签的layout_*属性会替换掉被include视图的根节点的对应属性。
include标签的id属性会替换掉被include视图的根节点id,如果获取根节点id视图会抛出空指针



    

    

Android 布局优化之include与merge

4.2.4 ViewStub标签懒加载不常用的布局

ViewStub标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的布局像:进度条、显示错误消息等,可以使用ViewStub标签,以减少内存使用量,加快渲染速度。

  
//setVisibility可以被调用多次,但不建议这么做  
//如果引用的视图已经被垃圾回收器回收,则抛出异常这也就是为什么setVisibility可以调用多次,但是并不推荐这样做的原因
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

// or  

//只能被调用一次,第二次调用会抛出异常"ViewStub must have a non-null ViewGroup viewParent"
//因为ViewStub成功执行inflate方法后会调用parent.removeViewInLayout(this);
//将自己从父节点移除 所以ViewStub的inflate只能调用一次
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate(); 
// 方式1,获取ViewStub,  
ViewStub listStub = (ViewStub) findViewById(R.id.stub);  
listStub.setVisibility(View.VISIBLE);  
ListView commLv = findViewById(R.id.stub_comm_lv);  
if (listStub.getVisibility() == View.VISIBLE) {  
	// 已经加载, 否则还没有加载  
}  
// 方式二  
View commLv2;//全局变量

ViewStub listStub2 = (ViewStub) findViewById(R.id.stub) ;  
if (commLv2 == null) {  
	commLv2 = (ListView)listStub2.inflate();  
} else {  
    // ViewStub已经加载  
}  

Android UI布局优化之ViewStub:使用+源码分析

4.2.5 ListView不使用包裹内容属性

//避免ListView的3个圆圈报红,尽可能不使用包裹内容属性。
 

5 渲染优化方法2——OpenGL ES系统优化

// 等待

6 渲染优化方法3——避免过度绘制(overdraw)

6.1 清除不必要的背景和图片

性能优化之渲染性能优化_第18张图片
  开始清除前的渲染图:
性能优化之渲染性能优化_第19张图片
  清除的内容:不必要的Background:

// 我们主布局的文件已经是background为white了,那么可以移除ListView的白色背景
// Item布局中的LinearLayout的
android:background="@android:color/darker_gray"
// Item布局中的RelativeLayout的
android:background="@android:color/white"
// Item布局中id为id_msg的TextView的
android:background="@android:color/white"

清除后的渲染图:
性能优化之渲染性能优化_第20张图片

6.2 保留Activity的灰色背景,又没过度绘制

我们的这个Activity要求背景色是灰色,我们的确在layout中去设置了背景色灰色,那么这里注意下,我们的Activity的布局最终会添加在DecorView中,这个View会中的背景是不是就没有必要了,所以我们希望调用mDecor.setWindowBackground(drawable),那么可以在Activity调用getWindow().setBackgroundDrawable(null),可以避免过度绘制一层

// 在Activity的onCreate方法后
setContentView(R.layout.activity_overdraw_01);
getWindow().setBackgroundDrawable(null);
// 在layout中去设置了背景色colorPrimary



性能优化之渲染性能优化_第21张图片

6.3 清除ImageView加载后不必要的背景色或图片

(1)设置方法
  这个背景比较难发现,主要需要看Adapter的getView的代码,上述代码你会发现,首先为每个icon设置了背景图片(主要是当没有icon图的时候去显示),然后又设置了一个头像。那么就造成了overdraw,有头像的完全没必要去绘制背景,所有修改代码:

Droid droid = getItem(position);
if(droid.imageId ==-1) {
	holder.icon.setBackgroundColor(0x4400ff00);
    holder.icon.setImageResource(Color.TRANSPARENT);
} else {
    holder.icon.setImageResource(droid.imageId);
    holder.icon.setBackgroundResource(Color.TRANSPARENT);
}

(2)看最后的Show GPU Overdraw 与最初的比较
性能优化之渲染性能优化_第22张图片

6.4 优化自定义View的计算

6.4.1 过度绘制的情况

(1)经常会由于疏忽造成很多不必要的绘制,比如大家看下面这样的图。多张卡片叠加,那么如果你是一张一张卡片从左到右的绘制,效果肯定没问题,但是叠加的区域肯定是过度绘制了。并且material design对于界面设计的新的风格更容易造成上述的问题。那么有什么好的方法去改善呢?
性能优化之渲染性能优化_第23张图片
(2)答案是有的:Android的Canvas对象给我们提供了很便利的方法clipRect裁剪掉View的覆盖部分,增加cpu的计算量来优化GPU的渲染。

6.4.2 源码演示

(1)Activity代码

public class OverDrawActivity02 extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new CardView(this));
    }
}

(2.1)未修改前CardView 代码

/**
 * CardView 
 */
public class CardView extends View {
    private Bitmap[] mCards = new Bitmap[3];
    private int[] mImgId = new int[]{R.drawable.alex, R.drawable.chris, R.drawable.claire};
   
    public CardView(Context context) {
        super(context);;
    }

    public CardView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        if (!isInEditMode()) {
            initView(context, attrs);
        }
    }

	public void initView(Context context, AttributeSet attrs) {
        Bitmap bm = null;
        for (int i = 0; i < mCards.length; i++) {
            bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
            mCards[i] = Bitmap.createScaledBitmap(bm, 200, 300, false);
        }
        setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(20, 120);
        for (Bitmap bitmap : mCards) {
            canvas.translate(120, 0);
            canvas.drawBitmap(bitmap, 0, 0, null);
        }
        canvas.restore();
    }
}

(2.2)修改后的CardView 代码

/**
 * 分析得出,除了最后一张需要完整的绘制,其他的都只需要绘制部分;所以我们在循环的时候,给i到n-1都添加了clipRect的代码。
 */
 @Override
 protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     canvas.save();
     canvas.translate(20, 120);
     for (int i = 0; i < mCards.length; i++) {
         canvas.translate(120, 0);
         canvas.save();
         if (i < mCards.length - 1) {
             canvas.clipRect(0, 0, 120, mCards[i].getHeight());
         }
         canvas.drawBitmap(mCards[i], 0, 0, null);
         canvas.restore();
     }
     canvas.restore();
}

6.4.3 最后的效果图

性能优化之渲染性能优化_第24张图片

7 总结

7.1 性能优化

性能优化其实不仅仅是一种技术,而是一种思想,你只听过它的高大上,却不知道它其实就是各个细节处的深入研究和处理。当然,有的时候也需要自己进行权衡效果和性能,根据需求进行选择。还有,Android Device Monitor是个好东西,简直就是性能优化大本营,性能优化的工具基本都在其中。
  所以在平时的开发过程中,养成良好的思考习惯是第一步:

//写代码的时候要想:
(1)我的代码是不是多余? 
(2)我的对象有没有必要在循环中创建? 
(3)我的计算方法是不是最优?
//画界面的时候要想: 
(1)布局是否有背景? 
(2)是否可以删掉多余的布局? 
(3)布局是否扁平化,移除非必需的UI组?
(4)自定义View是否进行了裁剪处理? 

7.2 如何选择Layout

(1)RelativeLayout会让子View调用2次onMeasure,LinearLayout在有weight时,也会调用子View2次onMeasure。
(2)RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
(3)在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
(4)能用两层LinearLayout,尽量用一个RelativeLayout,在时间上此时RelativeLayout耗时更小。另外LinearLayout慎用layout_weight,也将会增加一倍耗时操作。由于使用LinearLayout的layout_weight,大多数时间是不一样的,这会降低测量的速度。这只是一个如何合理使用Layout的案例,必要的时候,你要小心考虑是否用layout weight。
(5)总之减少层级结构,才是王道,让onMeasure做延迟加载,用viewStub,include等一些技巧。

8 参考链接

那些 Android 程序员必会的视图优化策略

深入Android渲染机制

Google《Android性能优化》学习笔记

Hongyang-Android UI性能优化实战 识别绘制中的性能问题

Hierarchy Viewer使用详解

https://blog.csdn.net/bboyfeiyu/article/details/45869393

ListView滑动删除实现之一——merge标签与LayoutInflater.inflate()

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