性能优化-渲染机制及优化

在手机上显示图片,播放视频,这是很常见的手机操作,也就是屏幕的绘制在软件开发中几乎是每个应用都会打交道的,这篇文章记录了渲染机制以及如何做优化

卡顿产生的原因

  • 在Activity中直接进行网络访问/大文件的IO操作还有就是自定义的View没有优化好,以上的情况都有可能造成卡顿,甚至是无响应

  • 当产生大量的垃圾时,GC回收大量垃圾的时候,也会造成卡顿

  • Android每隔16ms就会重绘一次Activity,也就是说必须在16ms以内完成屏幕刷新所要求的参数设置,之所以是16ms,那是因为大多说手机的刷新率是60Hz,那么也就是说1000ms/60=16.66ms,如果在这个时间内,没有完成参数设置,那么就会产生丢帧现象,在视觉上体现出来就是卡顿

内存抖动造成卡顿分析及解决办法

短时间内分配大量内存,会造成UI线程短时间阻塞,此时如果在绘制屏幕,那么此时就会造成卡顿
例如在绘制屏幕的时候有如下计算执行。那么此时会造成卡顿现象

/**
 * 排序后打印二维数组,一行行打印
 */
public void imPrettySureSortingIsFree() {
    int dimension = 300;
    int[][] lotsOfInts = new int[dimension][dimension];
    Random randomGenerator = new Random();
    for(int i = 0; i < lotsOfInts.length; i++) {
        for (int j = 0; j < lotsOfInts[i].length; j++) {
            lotsOfInts[i][j] = randomGenerator.nextInt();
        }
    }

    for(int i = 0; i < lotsOfInts.length; i++) {
        String rowAsStr = "";
        //排序
        int[] sorted = getSorted(lotsOfInts[i]);
        //拼接打印
        for (int j = 0; j < lotsOfInts[i].length; j++) {
            rowAsStr += sorted[j];
            if(j < (lotsOfInts[i].length - 1)){
                rowAsStr += ", ";
            }
        }
        Log.i(TAG, "Row " + i + ": " + rowAsStr);
    }
}

public int[] getSorted(int[] input){
	int[] clone = input.clone();
	Arrays.sort(clone);
	return clone;
}

上述代码在第二个for循环中,大量创建对象并弃用,产生了大量的垃圾,此时GC回收垃圾,占用主线程,直接就导致了GC回收占用时间加上屏幕绘制时间高于16ms,此时就发生了卡顿,其卡顿时间取决于GC的工作时间,故在主线程计算这种数据的时候(这种情况放在子线程最好),要尽量减少内存的分配,防止内存抖动,从而有效避免卡顿现象的发生
其改进方法就是使用StringBuilder,减少内存的分配

StringBuilder sb = new StringBuilder();
String rowAsStr = "";
for(int i = 0; i < lotsOfInts.length; i++) {
	// 清除上一行
	sb.delete(0, rowAsStr.length());
	//排序
	int[] sorted = getSorted(lotsOfInts[i]);
	//拼接打印
	for (int j = 0; j < lotsOfInts[i].length; j++) {
		sb.append(sorted[j]);
		if(j < (lotsOfInts[i].length - 1)){
			sb.append(", ");
		}
	}
	rowAsStr = sb.toString();
	Log.i(TAG, "Row " + i + ": " + rowAsStr);
}

计算性能占用CPU造成卡顿及解决办法

有时候在执行函数的时候,会占用大量CPU资源,尤其是递归函数的执行过程,会占用CPU大量时间,那么此时也有可能造成卡顿
例如在主线程执行斐波那契梳理的递归实现时,如果此时在执行屏幕绘制,那么也会造成卡顿

public int computeFibonacci(int pos) {
    if (pos <= 2) {
        return 1;
    } else {
        return computeFibonacci(pos - 1) + computeFibonacci(pos - 2);
    }
}

使用TraceView观察到,这个函数在执行的时候占用了CPU大量的时间,造成了卡顿
这种情况采用批处理和缓存思想,储存运算结果,更新数据来得到最终结果,避免递归调用造成的大量CPU时间占用

public int computeFibonacci(int pos) {
    int prev = 0;
    int current = 1;
    int newValue;
    for (int i = 1; i < pos; i++) {
        newValue = current + prev;
        prev = current;
        current = newValue;
    }
    return current;
}

渲染机制简单介绍

  • Android系统的渲染由两个关键组件构成:CPU和GPU
    在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度
    在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间
  • 图像的显示是通过栅格化来完成的,以下的图片说明了栅格化的过程
    性能优化-渲染机制及优化
  • CPU首先将布局文件转化为多边形或者纹理,然后交由GPU去负责栅格化
    性能优化-渲染机制及优化
  • 渲染性能的优化就是尽可能地上传数据到GPU,然后尽可能长地在不修改的情况下保存数据,因为每次上传资源到GPU时,我们都会浪费宝贵的处理时间

GPU存在的主要问题

  • GPU性能强大,然后其常见的一个问题就是过度绘制
    过度绘制:指屏幕上的某个像素点在同一帧的时间内被绘制了多次
  • 那么此时用户在界面上只能看到最上面一层,下面绘制的就属于过度绘制了
    要查看过度绘制很简单,在开发者选项里面打开Show GPU overdraw即可
    然后就能在屏幕上看到多种颜色,这些颜色代表过度绘制的倍数
    性能优化-渲染机制及优化
    最理想的情况就是全部都是蓝色,因此可以通过这个现象来优化我们的app

过度绘制的解决办法

  1. 清除不必要的背景和图片
    材料主题会绘制一遍屏幕,我们如果这时候再设置背景颜色,那么会产生2倍过度绘制,此时就需要取消其默认的背景图片,也就是将Activity的背景图片设为null,其方法如下:
getWindow().setBackgroundDrawable(null);
  1. 清除XML文件中,不必要的背景声明

CPU的工作部分

  • CPU将布局文件转化为GPU能够识别的对象,通过GPU的栅格化,从而显示在屏幕上,这是在DisplayList帮助下完成的,DisplayList将数据传递给GPU,GPU通过OpenGL将屏幕绘制出来
  • 任何时候View的绘制内容发生变化,都需要重新创建DisplayList并重新执行指令更新到屏幕
    CPU的view转化检测
    工具:Hierarchy Viewer检测(在Android Monitor里面)
    选中应用 -> 点击Load the view hierarchy into the tree view
    然后再点击Obtain layout times for tree rooted at selected node
    就会看到如下界面
    性能优化-渲染机制及优化
    三个圆点分别代表:测量、布局、绘制三个阶段的性能表现
    1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图
    2)黄色:渲染速度比较慢的50%
    3)红色:渲染速度非常慢
    观察其分布,自己写的布局杂乱无章,深层嵌套,而且红点很多,这样会降低CPU的解析效率
    优化策略:
    当我们的布局是用的FrameLayout的时候,我们可以把它改成merge,可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)
    ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置
    优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间
    性能优化-渲染机制及优化

总结

减少CPU计算时间

CPU的优化,从减轻加工View对象成Polygons和Texture来下手
View Hierarchy中包涵了太多的没有用的view,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。

减少CPU将计算好的Polygons和Texture传递到GPU的时间

OpenGL ES API允许数据上传到GPU后可以对数据进行保存,做了缓存

减少GPU进行格栅化

优化:尽量避免过度绘制(overdraw)
GPU如何优化:

  1. 背景经常容易造成过度绘制
    由于布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景
    解决的办法:将主题添加的背景去掉

  2. 自定义控件如何处理过度绘制(多张图片有重叠)
    可以通过裁剪来处理canvas.clipRect()
    前n-1张

private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
    DroidCard c = mDroidCards.get(i);
    canvas.save();
    canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
    canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    canvas.restore();
}

第n张

private void drawLastDroidCard(Canvas canvas,DroidCard c) {
    canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}