小明同学已经有过 1 年的 Android 开发经验了,一直都是按照网上的各种教程开发 App,突然有一天同学告诉他,Android4.4 的手机打开他开发的 App 页面展示极其的慢;信誓旦旦的小明展开了一系列的排查,发现网络请求还 OK,数据处理也不耗时,最终排查到时因为自己为了布局在 xml 文件中各种添加 LinearLayout , FrameLayout , TableLayout 和 AbsoluteLayout 导致页面绘制缓慢;经过一系列的google,发现自己对于Android绘制层面的优化还有所欠缺;于是展开了一波学习。

# Android 性能优化之绘制优化

# 一、避免过度绘制

过度绘制(OverDraw)是指在屏幕上在某一个像素,子安同一帧的时间内被多次的绘制,常见的是重复被组件或者布局添加背景颜色,等等。 主要原因:

  • XML 布局文件,控件有重叠且都有设置背景。
  • View 的自绘中,View.onDraw 里面同一个区域被多次绘制了。

# (一)、过度绘制的检测工具

打开手机设置中开发者人员选项,选择打开“显示 GPU 过度重绘”,对没有默认开启硬件加速的界面需要同时打开“强制进行 GPU 渲染”。

  • 无色:没有过度重绘, 每个像素只绘制一次
  • 蓝色:每个像素多绘制了一次(可接受)
  • 绿色:每个像素多绘制两次(可接受)
  • 淡红:每个像素多绘制三次,一般来说这个区域不超过屏幕的 1/4 是可以接受的。
  • 深红:每个像素被绘制四次或者更多。不可接受。

目标:减少红色,显示更多蓝色区域。

# (二)、布局上的优化

  • 移除 XML 中非必须的背景,或者根据条件设置
  • 移除 window 的默认背景(在 setContentView 前面加上 this.getWindow().setBackgroundDrawable(null));
  • 按需显示占位背景图片。

# (三)、自定义 View 的优化

在 View 的自绘中,会存在 View 的过度重绘,比如多个组件重叠在一起,如果我们只绘制出它显示的部分就解决了过度重绘问题,假设多个组件都按照原本大小绘制的话,将会到底过度重绘。

  • 在自定义 View 中可以使用canvas.clipRect()来帮助系统识别哪些可见的区域,这个方法指定一个矩形区域,只有在这个区域内才会被绘制,其他区域将不会被绘制。
  • 使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过哪些非矩形区域内的绘制操作。

RecyclerView或者ListView或者其他容器控件中,ItemView如果比较复杂,建议实现成一个自绘View,这样会让滑动更加顺畅。

以一个四个卡片叠加咋自定义 View 为例子

在实际的开发中布局的层次很容易超过 10 层如果超过 15 层就要重视并准备重新做优化,20 层就必须要修改了,实在没办法的话,就需要将复杂的布局层级使用自绘控件来实现。

# 二、布局优化

# (一)、减少层级

层级越少,测试和绘制的时间越短,通常减少层级的方法有

  • 1、尽量使用 RelativeLayout 和 LinearLayout
  • 2、在布局层级相同的情况下,使用 LinearLayout
  • 3、使用 LinearLayout 有时候会让层级变得更多,所以应该使用 RelativeLayout,尽量使界面扁平化

RelativeLayout 存在性能低的问题,原因是 RelativeLayout 会对子 View 做两次测量,在 RelativeLayout 中子 View 的排列方式是基于彼此的依赖关系的。因为依赖关系可能和布局中 View 的顺序不一样,在确定每一个子 View 的位置时,需要先给所有的子 View 做一次排序,如果再 RelativeLayout 中允许子 View 横向或者纵向相互依赖,就需要横向和纵向分别在进行一次测量。 LinearLayout 中有 weight 属性,也需要做两次测量,因为没有更多的依赖关系,所以会比 RelativeLayout 效率高。

# (二)、Merge 的使用

# 主要使用场景

在使用 Merge 的场合主要有以下两处:

  • 1、在自定义 View 中使用,父元素尽量是 FrameLayout 或者 LinearLayout
  • 2、在 Activity 中整体布局,根元素需要时 FrameLayout。 - 在 Android 布局的源码中,如果是 Merge 标签,那么直接将其中的子元素添加到 Merge 标签的 Parent 中,这样就不会引入额外的层级了。 - 如果 Merge 代替的布局元素为 LinearLayout,在自定义布局代码中将 LinearLayout 属性添加到引用上,如垂直和水平布局,背景色等。

# 注意事项

  • Merge 只能用在布局 XML 文件的根元素。
  • 使用 Merge 来加载一个布局时,必须指定一个 ViewGroup 作为其父元素,并且要设置加载的 attchToRoot 参数为 true(也就是 inflate(int,ViewGroup,boolean))
  • 不能再 Viewstub 中使用 Merge 标签,原因是 ViewStub 的 inflate 方法中根本没有 attachToRoot 的设置

Activity 的总布局中使用 Merge,但又想设置整体的属性(比如布局方式和颜色),可以不使用 setContentView 方法加载 Layout,而使用(id/content)将 FrameLayout 取出。在代码中手动加载布局,但是如果层级压力不大(小于 10 级),则没有必要,因为代码的维护性就很差了。

# (三)、提高显示速度使用 ViewStub

ViewStub 是一个轻量级的 View,它是一个看不见的,并且不占用布局位置,占用资源非常小的视图对象,可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,然后当 ViewStub 被设置为可见的时候,或者是调用 ViewStub,inflate()时候,ViewStub 所指向的布局就会加载和实例化,然后 Vi`ewStub 的布局属性就会传给他指向的布局,这样就可以使用 ViewStub 来设置是否显示某个布局。

# 主要使用场景

  • 在程序运行期间,某个布局在加载后就不会有变化,除非销毁该界面重新加载。
  • 想要控制显示与隐藏的是一个布局文件,而不是 View

# 注意事项

  • ViewStub 只能加载一次,之后的 ViewStub 对象将会被置空,换句话说,某个被 ViewStub 指定加载的布局被加载后,就不能再通过 ViewStub 来控制它了,所以它不适用于按需隐藏的情况(还是只有使用 setVisiable 的方式)。
  • ViewStub 只能用来加载一个布局文件,而不是一个具体的 View(除非将一个 View 放入一个布局文件中)。
  • ViewStub 中不能再嵌套 Merge 标签了。

# (四)、布局复用使用 include 标签

使用 include 标签。

# (五)、布局优化总结

影响布局效率的主要是以下几个点:

  • 布局层级越少,加载越快
  • 减少同一层级控件的数量,加载速度会变快
  • 一个控件的属性越少,加载会越快

总结:

  • 尽量多使用 RelativiLayoutLinearLayout,不要使用 AbsoluteLayout 布局。
  • 将可复用的组件抽取出来,并通过<include/>标签使用
  • 使用<ViewStub/>标签加载一个不变布局的的布局。
  • 使用<merge/>标签减少布局的嵌套层次
  • 尽可能少用 wrap_content,wrap_content 会增加布局 Measure 的计算成本,已知高为固定值得时候,不要使用 wrap_content
  • 删除控件中无用的属性。

# 三、合理的刷新机制

在应用的开发中,因为数据的变化,需要刷新页面来展示数据,但是频繁的刷新会增加资源的开销,并且可能导致卡顿。 合理的刷新机制需要注意以下几点:

  • 尽量减少刷新的次数
  • 尽量避免后台有高 CPU 线程运行
  • 缩小刷新区域

# (一)、减少刷新次数

  • 控制刷新的频率 比如在显示进度条的时候,不需要每一个进度的时候都需要显示,可以做到每隔多少刷新显示一次,这样就减少了刷新的频率。
  • 避免没有必要的刷新 首先需要判断是否需要刷新,比如数据没有变化的时候,需要刷新的控件不在可见区域内,就没必要刷新,但是需要注意的是:如果一个 View 从不可见到可见,一定要刷新一次。

# (二)、避免后台线程影响

后台线程过大会导致 CPU 占比过高,导致频繁的 GC 和 CPU 时间片资源紧张,还是优肯会导致页面的卡顿。比如在 RecyclerView 或者 ListView 在滑动的时候就可以暂停其他的 UI 操作: 通过监听 ListView 的 onScrollStateChanged 事件,在滚动的时候暂停下载图片和下载进程工作,结束后再刷新可以提高 ListView 的滑动平滑度。(具体详见:Android 缓存策略和 Bitmap 高效加载)。

# (三)、缩小刷新的区域

# 在自定义 View

在自定义 View 中一般采用 invalidata 方法刷新,如果需要更新数据,只是在某个区域的话,在调用了 invalidata 就会更新整个视图,这就浪费了不需要更新区域的资源。我们采用两种更新局部数据的方法。

invalidata(Rect dirty)
invalidata(int left,int top,int right,int bottom)

# 容器中的某个 Item 发生了变化

只需要更新这个 Item 就行了。比如在 ListView 中或者 RecyclerView 中,如果是单条操作,就必须调用 AdapternotifyDataChanged()刷新。

# invalidate()函数的区域更新例子

public class FingerView extends View {

private static final float STROKE_WIDTH = 5f;

/** Need to track this so the dirty region can accommodate the stroke. **/
private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;

private Paint paint = new Paint();
private Path path = new Path();

 /**
  * Optimizes painting by invalidating the smallest possible area.
  */
 private float lastTouchX;
 private float lastTouchY;
 private final RectF dirtyRect = new RectF();

  public <span style="font-family: Arial, Helvetica, sans-serif; font-size: ;">FingerView </span>(Context context, AttributeSet attrs) {
   super(context, attrs);

   paint.setAntiAlias(true);
   paint.setColor(Color.BLACK);
   paint.setStyle(Paint.Style.STROKE);
   paint.setStrokeJoin(Paint.Join.ROUND);
   paint.setStrokeWidth(STROKE_WIDTH);
 }

 /**
  * Erases the signature.
  */
 public void clear() {
   path.reset();

   // Repaints the entire view.
   invalidate();
 }

 @Override
 protected void onDraw(Canvas canvas) {
   canvas.drawPath(path, paint);
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
   float eventX = event.getX();
   float eventY = event.getY();

   switch (event.getAction()) {
     case MotionEvent.ACTION_DOWN:
       path.moveTo(eventX, eventY);
       lastTouchX = eventX;
       lastTouchY = eventY;
       // There is no end point yet, so don't waste cycles invalidating.
       return true;

     case MotionEvent.ACTION_MOVE:
     case MotionEvent.ACTION_UP:
       // Start tracking the dirty region.
       resetDirtyRect(eventX, eventY);

       // When the hardware tracks events faster than they are delivered,
       // event will contain a history of those skipped points.
       int historySize = event.getHistorySize();
       for (int i = 0; i < historySize; i++) {
         float historicalX = event.getHistoricalX(i);
         float historicalY = event.getHistoricalY(i);
         expandDirtyRect(historicalX, historicalY);
         path.lineTo(historicalX, historicalY);
       }

       // After replaying history, connect the line to the touch point.
       path.lineTo(eventX, eventY);
       break;

     default:
       debug("Ignored touch event: " + event.toString());
       return false;
   }

   // Include half the stroke width to avoid clipping.
   invalidate(
       (int) (dirtyRect.left - HALF_STROKE_WIDTH),
       (int) (dirtyRect.top - HALF_STROKE_WIDTH),
       (int) (dirtyRect.right + HALF_STROKE_WIDTH),
       (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

   lastTouchX = eventX;
   lastTouchY = eventY;

   return true;
 }

 /**
  * Called when replaying history to ensure the dirty region includes all
  * points.
  */
 private void expandDirtyRect(float historicalX, float historicalY) {
   if (historicalX < dirtyRect.left) {
     dirtyRect.left = historicalX;
    } else if (historicalX > dirtyRect.right) {
      dirtyRect.right = historicalX;
    }
    if (historicalY < dirtyRect.top) {
      dirtyRect.top = historicalY;
    } else if (historicalY > dirtyRect.bottom) {
      dirtyRect.bottom = historicalY;
    }
  }

  /**
   * Resets the dirty region when the motion event occurs.
   */
  private void resetDirtyRect(float eventX, float eventY) {

    // The lastTouchX and lastTouchY were set when the ACTION_DOWN
    // motion event occurred.
    dirtyRect.left = Math.min(lastTouchX, eventX);
    dirtyRect.right = Math.max(lastTouchX, eventX);
    dirtyRect.top = Math.min(lastTouchY, eventY);
    dirtyRect.bottom = Math.max(lastTouchY, eventY);
  }
}

# 四、提升动画性能

Android 提供三个平台框架,帧动画(太耗资源能不用就不用),补间动画(包括:淡入淡出 AlphaAnimation,缩放 ScaleAnimation,平移 TranslationAnimation,和旋转 RotateAnimation),和属性动画。

# (一)、使用属性动画

# 1、补间动画局限性:

  • 补间动画只能用于 View 对象,也就是继承于 View 或者 View 的控件。
  • 只有四种动画操作,淡入淡出,缩放,平移,旋转
  • 不见动画是改变 View 的显示效果,但是没有改变真正的属性。也就是假设 View 移动了,要是点击移动后的地方是没有用的,还是只能点击期初的位置。

# 2、属性动画的特性:

  • 持续时间:默认长度 300Ms
  • 时间插值器(Time Interpolation)。能够指定为计算当前动画运行时间的函数的属性值来觉得动画范围内的变化频率。
  • 重复次数和行为。
  • 动画集合(AnimationsSet)
  • 帧刷新延迟:默认 10s 刷新一次。

具体的使用:查看 View 相关技术-->Animation 相关--->属性动画

# (二)、硬件加速

# 1、硬件加速原理

在硬件加速渲染模型中有一个重要的核心类:DisplayList,每个 View 内部都会维护一个 DisplayList

在不支持硬件加速的版本中,View 的更新通过 drawinvalidate()方法通知更新并重新渲染。

在支持硬件加速的版本中,其中执行绘制的 draw 方法会把所有绘制命令记录到一个新的显示列表(DisplayList),这个 DisplayList 包含输出的 View 层级绘制代码,但是并不是加入到显示列表立即执行,当 ViewTree 的 DisplayList 全部记录完毕后,由 OpenGLRender 负责将 root view 的 DisplayList 渲染到屏幕上,而 invalidate()只是显示列表中的记录和更新显示层级就可以了,不用更新 View。

# 2、硬件加速的级别

  • Application 级别:在 Mnifest.xml 文件中的 application 中添加属性:android:hardwareAccelerated="true",这样整个应用都采用硬件加速
  • Activity 级别:在<activity android:hardwareAccelerated="true"/>
  • Window 级别:getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED).
  • View 级别:View.setLayerType(View.LAYER_TYPE_SOFTWARE,null).

# 3、在动画上使用硬件加速

使用 view.setLayerType(type,Paint)方法,第一个参数是使用的层类型,第二个参数为可选参数,可以吧 paint 参数应用到颜色过滤上,特别是混合模式或者是对一个 Layer 进行不透明处理。 类型有以下几种:

  • LAYER_TYPE_NONE:默认渲染方式,不会返回一个离线缓冲,默认值
  • LAYER_TYPE_HARDWARE:使用硬件加速
  • LAYER_TYPE_SOFTWARE:此 View 通过软件渲染为一个 Bitmap。

设计动画的流程: 将要执行动画的 ViewTYPE 设置为 LAYER_TYPE_HARDWARE。 计算动画 View 的属性和信息,更新 view 的属性 若动画结束,将 LayerType 设置为 None。

# (三)、硬件加速的问题

  • 在软件渲染的时候,可以重用 Bitmap 的方法来节省内存,但是如果开启硬件加速就不行了。
  • 开启硬件加速的 view 在前台运行,需要耗费额外的内存,加速的 UI 切换到后台时,产生的额外内存可能不会释放。
  • 当 UI 中存在过度绘制时候,硬件加速会比较容易发生问题。
【未经作者允许禁止转载】 Last Updated: 11/20/2021, 8:33:35 AM