Android View性能优化

      最近被公司外派到兄弟公司支援某个app的重构,业务重构过程中,有测试反馈说重构后的版本相比之前的版本出现了严重的卡顿问题,于是开始了View层级的性能优化。本篇文章主要是讲述View性能的发现、调试,不详细介绍View渲染的原理、调试工具如何使用等。不过本文会给出相应知识点的相关链接,大家如需详细了解,可点击进入对应文章。

       本文从简略讲述View的渲染原理和三种常用ViewGroup性能分析出发,然后介绍发现APP运行过程中各种View性能检测的工具,并详细介绍如何在过度渲染、频繁渲染、渲染范围、布局层级、UI工作量等方面进行优化。

       每个View的渲染都包括Measure、Layout和Draw三个过程,其中Measure用来计算View的大小,Layout用来计算View的位置,Draw用来绘制View,每个步骤的具体流程可参见链接:http://www.cnblogs.com/jycboy/p/6066654.html

View的渲染原理

单个View的渲染

     每个View的渲染都包括Measure、Layout和Draw三个过程,其中Measure用来计算View的大小,Layout用来计算View的位置,Draw用来绘制View,每个步骤的具体流程可参见链接:http://www.cnblogs.com/jycboy/p/6066654.html    

     Measure和Layout过程我们在这不做描述,简单说下Draw的内容:
  1. 绘制自身背景
  2. 绘制自身内容
  3. 绘制子控件
  4. 控制滚动条、阴影等部分
    为了提高View渲染效率,应该尽可能减少View的渲染内容,比如自身背景。

View的渲染流程

    单独为一个View执行刷新操作,并不会仅仅更新该View,而是会逐级上报,一直到rootview,并通过各种条件判断是否需要重新绘制其它View,有此可以看出,稍微使用不当,会很容易导致全屏渲染,而我们发现的性能问题,也即是该原因引起的。为了能更好的了解优化方案,掌握了解View渲染的流程是非常有必要的。

Android View性能优化

    每次对View的刷新都会执行以上流程,详细情况请参见https://www.cnblogs.com/jycboy/p/6219915.htmlhttp://www.cnblogs.com/jycboy/p/6066654.html

ViewGroup的性能分析

    我们知道View通常都是依附ViewGroup存在,ViewGroup中存放View的个数以及相互依赖也是影响性能的一个重要原因。我们常用的ViewGroup为FrameLayout、LinearLayout和RelativeLayout,这三种Layout在布局时处理方式不同,所带来的效率也肯定存在差异。本文不详细介绍三种Layout的区别以及在渲染过程中的处理流程,这些大家应该都有深刻的理解,或者参考链接:https://blog.****.net/hejjunlin/article/details/51159419,在这我们仅仅描述一下三种Layout的性能情况,并给出使用建议,帮助在开发过程中使用最适合的Layout。下表是三种Layout的性能比较。

Layout Measure次数 计算效率 t特点
RelativeLayout 2 需要计算水平和垂直两个方向的依赖关系,效率较差 布局灵活,能够使用较少层级实现复杂布局,减少遍历的深度,但单层级计算效率低
LinearLayout 1,存在weight或measureWithLargestChild属性时计算2次 仅计算水平或垂直单个方向的依赖关系,效率较好 计算效率高,但实现复杂布局,必须多层嵌套
FrameLayout 2 第一次计算最大子控件大小,第二次调整其它match_parent属性的子控件 布局简单,计算效率高,但无法实现复杂布局

通过上面的比较分析,本文建议:

  1. 在三种布局在相同层次下能够实现同样效果时,使用优先级为FrameLayout>=LinearLayout>RelativeLayout
  2. 对复杂的UI,建议使用RelativeLayout,用来减少层级,提高遍历效率
  3. 使用LinearLayout时,慎用weight属性和measureWithLargestChild属性,会引起2次渲染
  4. 尽量减少RelativeLayout中包含太多的无依赖的控件数量。有的时候,RelativeLayout中分为上下部分控件,但由于每部分控件数量都较多,在使用时非常有可能因为上部分某个控件的改变导致下半部分的控件也进行无效刷新。这种情况下,建议将这些无依赖的控件依附相同层级的Layout,降低代码风险。

View性能调试

    前章节简单讲述了View的渲染原理和流程,希望能够帮助大家掌握View的基础知识,这部分也基本上是面试必考题。解决某个问题之前,流程通常为:发现问题->分析问题->确定问题->解决问题->验证问题。那我们就先从发现问题开始,如何来发现性能中存在的问题。

发现问题

    研发开发过程中,由于关注点用于开发时的单个业务上,所以很容易忽略性能上的问题,即使在开发过程中发现了卡顿问题,但由于业务紧张,也不会放下当前的工作去处理性能方面的问题。所以性能问题通常是由QA,甚至是真实用户提出。

分析问题

    问题发生了,接下来就得分析。为了分析性能上的问题,本文整理了性能分析的各种工具,帮助我们确定问题。

    引起卡顿的原因从细节上分包括:过度绘制,频繁渲染,布局层级深,刷新范围大,UI线程工作量大,内存抖动等原因。接下来,我们对每一项都进行测试分析。

过度绘制(OverDraw)

    过度绘制是指屏幕中某个范围被多次渲染,比如父控件设置了背景色,子控件设置了图片显示或者文本显示,这样在子控件的对应区域,就会渲染两次。每次渲染都会带来性能消耗,同一区域渲染的次数越多,那么带来的消耗就越大。检测OverDraw,可通过打开手机“开发者选项”->"调试GPU过度绘制开关",就可以在手机屏幕上查看绘制情况。如下图所示。

Android View性能优化

    颜色越深代表绘制的次数越大,但一个屏幕大部分都被粉丝或红色占据时,我们就必须考虑优化了。

刷新频率和刷新范围

    手机的帧率为60fps,那么每帧渲染的周期为16ms。当屏幕不存在UI更新时,每一帧并不需要处理,但如果UI发生改变了,就会触发View的整个刷新流程。频繁的界面刷新,会导致手机电量的消耗,如果存在UI滑动的操作,也有可能导致滑动时的卡顿。所以在优化过程中,应该尽量降低View的刷新频率。检查刷新频率,可通过打开手机“开发者选项”中的“显示GPU视图更新”和“GPU呈现模式分析”两个开关进行分析。前者开关打开后,当屏幕中某个区域发生UI刷新时,会闪烁红色,通过该工具就能知道UI的刷新范围。

    “GPU呈现模式分析”开启选择“柱状视图”后,会在屏幕底部显示每一帧性能的详细情况。如下图所示。

Android View性能优化

    左上角的红色区域因为wifi信号的刷新而导致闪烁,底部的柱状表示每一帧的性能情况,其中每种颜色表示渲染的每一步骤的消耗时间,水平方向的绿线表示16ms,超过水平线的帧是导致卡顿的关键位置,是值得优化的地方。但从实际执行情况看,想确保每一帧都在16ms以下是不现实的,只能尽量减少。柱状图中每种颜色的含义如下:

 Android View性能优化

    优化过程中,我们重点优化蓝绿占比比较高的帧。

布局层级

    前面文章提到,View的渲染过程会遍历ViewTree,如果tree的层级越深,那么带来的消耗就越大,所以在实现UI布局的时候,我们尽量减少删除不必要的ViewGroup,减少布局层级。层级的检测工具可以使用Android Studio中的Layout Inspector和Hierarchy View。Layout Inspector的工具如下:

Android View性能优化

    Hierarchy View不仅能够展示布局的层次,而且能够计算每个节点的耗时情况,更能帮助找到耗时的关键View。但是该工具只能用于root的手机或者模拟器,如果想在非root手机上使用,需要自己增加ViewServer,具体使用方法可参加:https://www.cnblogs.com/hoolay/p/6248514.html

Android View性能优化

UI线程工作量大

    Android中UI更新必须要在主线程中执行(SurfaceView外),如果主线程执行了耗时操作,比如IO操作,大量for循环,或者和子线程共享一把锁,都有可能引起应用卡顿,甚至出现anr。可使用Systrace或TraceView进行跟踪分析。

    Systrace可以收集系统和应用的数据信息,生成的HTML报告中能够提示异常帧,并通过放大后,看到每一帧的执行细节和时间消耗,通过该步骤可以分析出耗时所在。之前我们通过这个工具分析出一个200ms的字符集初始耗时,大幅度提高首屏的启动时间。使用方法可参考https://blog.****.net/hfreeman2008/article/details/53538155

Android View性能优化

    TraceView和Systrace相似,但是该工具能够提供更加详细的执行信息,不仅仅包含本方法的执行情况,也包括调用方法和被调用发的执行情况,使用方法可参考https://blog.****.net/u011240877/article/details/54347396

Android View性能优化

内存抖动

    内存检测工具我们可以通过Android Studio的Monitor工具以及System Log检测。Monitor工具能够实时给出app占用内存的情况,当内存曲线呈现频繁波浪状时,就存在内存抖动的情况,另外,log中一直提示gc的有关信息,也有可能出现了内存抖动。内存的分析在本文中不做详细介绍,大家记住不要在for循环或者getView, onBindViewHolder等方法中频繁new内存,尽量做到内存复用。

确定问题

UI中控件较多,每个控件都有可能引起效率问题,仅通过分析工具,虽然能够看到性能展示,但通常无法直接确定问题原因,我们还需要一些其他的技术手段来辅助我们确定问题,可以参考以下几个手段进行:

  1. 单一View调试。UI布局中View较多,很难确定渲染频繁、渲染范围大是哪个控件更新引起的,这个时候可以通过删除其它View的加载,只保留目标View,跟踪该View在更新时带来的渲染影响,实现各个击破。有的时候TextView的text更新就会引起渲染问题。
  2. 单一业务调试。某个View或者某组View的更新可能跟多个业务相关,可通过注释其它业务代码,仅保留单一业务代码进行调试,寻找业务优化方案。比如当某个业务存在频繁更新UI的情况,我们可以通过比对前后数据是否发生改变,或者延迟刷新等手段来优化UI刷新频率。
  3. 自定义关键ViewGroup或View,尤其是根View。在自定义类中关键方法(onMeasure、onLayout、onDraw)中增加Log,打印关键方法的耗时情况和执行次数,能够方便统计出一次行为所带来的刷新次数,帮助发现不合理的UI刷新。
  4. 当确定某次刷新是因为View的某个API引起的,可以通过查看该View的源码,重点分析执行requestLayout或invalidate方法的判断条件,从条件中寻求优化方案。后面我们会通过RecyleView.notifyDataSetChanged方法来详细讲述。

解决问题

    通过各种手段将问题确定后,我们就要真正解决问题了。

过度渲染解决

    第一节我们提到了,View的渲染内容包括自身背景、自身内容、子控件、滚动条或阴影。优化过度渲染,就是尽可能减少每个View减少自身内容。建议从以下几个方面考虑:

  1. 不要设置无效的背景色。如果父控件被子控件完全遮盖,那么父控件即使设置了背景,那么在最终的UI中也不会被展示出来。但是不被展示和不会被渲染是两回事,真实情况是父控件的背景会依然被绘制,带来不必要的渲染消耗。由于业务的不断迭代和开发人员的更替,该问题非常普遍,另外,测试过程中通常不使用OverDraw检测工具,对于这种隐藏的性能问题,通过肉眼也无法发现。
  2. 尽量不要对背景使用Alpha值。在对带有Alpha属性的View渲染时,会先对View做一次rgb的渲染,然后在对第一次渲染的结果做Alpha处理,如此会对该View渲染两次。如下图左边所示的alpha设置。通过Hierarchy View分析会查出over_layer_fourth_section这个控件消耗了8ms用于draw,但是其它平级的view都是1ms以下。该控件虽然设置了alpha为0,但该属性和Visible:Gone是两种不同的属性,依赖会导致两次Draw。

Android View性能优化

    对Alpha的处理建议采用以下方式:

  • 尽量不要在初始化View进行设置,而是在需要使用该view时才真正设置;
  • 对于alpha变化,要使用View动画或者属性动画。
  • 尽量使用合适的alpha,对于alpha较低,并不影响用户体验时,去掉alpha,而是使用透明色。

3. 如果界面自定义了背景色,可以去掉系统自带的背景色,设置android:windowbackground="null"

4. 绘制时,可以考虑使用ClipRect或QuickReject方法限制渲染区域,减小绘制区域覆盖的概率。

5. 善用.9图,对于wrap_content属性的控件,如要设置边框,可考虑使用.9图,其中图中设置透明色,这样在渲染控件是会忽略透明区域的绘制。

6. 从设计上避免“OverDesigin”。

频繁渲染解决

    频繁渲染是影响性能的主要愿意之一。在优化过程中,我们需要确定哪些刷新是必须的,哪些是可以避免的。影响界面刷新的两个重要API:invalidate和requestLayout。其中invalidate会通知view及其祖先view重新渲染,requestLayout会通知view和祖先view进行onMeasure、onLayout和onMeasure,效率会更低。因此,为解决频繁渲染,主要注意以下几个方面:

  1. 尽量不要主动调用invalidate,postInvalidate和requestLayout,尤其是requestLayout;
  2. 各种View的很多API都会触发上述三个方法,比如setVisibility, addView, setRequestLayoutParams, setText等方法。在优化过程中,也要尽量少的减少这些方法的调用。以setText为例,如果本次更新的内容和上次显示的内容一致,那么就没有必要执行,执行一次内容比对的代价通常比刷新一次View的代价要小很多。可参考下面处理案例:

Android View性能优化

3. 优先使用固定大小或者match_parent属性。View的很多属性更改时,都会对非wrap_content属性做优化,以提高UI刷新效率。还是以TextView的setText方法为例,在该API中,会执行checkForReLayout方法来执行UI刷新。从代码中可以很容易看出对于wrap_content属性一定会执行requestLayout和invalidate方法,而对于其它属性,会根据高度是否变化而决定是否执行request。

Android View性能优化

4.当需要同时执行invalidate、requestLayout时,建议先执行requestLayout,后执行invalidate。原因是并不是每次请求刷新UI的事件都会得到响应,当目前正在执行UI刷新时,新的刷新事件会被忽略。由于invalidate仅仅是更改PFLAG_DIRTY属性,不会影响到下一次requestLayout属性,但是requestLayout更改的是PFLAG_FORCE_LAYOUT属性,会同时影响draw方法,所以有可能会忽略后面的invalidate。

5.当业务数据更新非常频繁时,可通过延迟更新策略降低刷新频率。比如当新数据来时,postDelay 5s进行刷新,在delay期间,新来的数据更改下一次显示的新值,直到5s结束后显示出最后一次更新的值。这样能够在不影响用户体验的基础上,降低UI刷新频率。

渲染范围优化

    评价一个布局是好是坏,不仅仅在于展示效果和层级,还包括是否能够将刷新影响范围降到最小。一个很差的布局中,一个简单控件的变化都有可能导致整屏界面的刷新,这个性能带来灾难性的损失。所以在设计布局中,需要考虑以下几个因素:

1. 尽量设置控件的大小。固定大小的控件不会导致其它View也跟随其改变,因此在刷新时范围能够有效的得到限制。

2. 尽量不要将所有的控件放在一个较大的RelativeLayout布局中,尤其是当子view不存在相互依赖的情况,因为一个控件的改变可能会引起该布局中所有View的刷新。可以适当的将部分子控件依附另外一个和父View同级的View。这样即没有增加层级,也能够将子View分组存放,降低了View依赖的概率。

3. 本文重点讲一下RecycleView的优化。RecycleView是频繁刷新的典型控件,处理不当,其带来的问题会非常严重。在优化RecycleView中,重点采用以下几个方式:

  • 相同于其它View,尽量控制RecycleView的大小;
  • RecycleView如果固定大小,可以使用setHasFixedSize(true)来优化。该属性表示当RecycleView大小固定时,子控件的增加、删除和刷新不会引起父控件的更改,因此在刷新时只处理子控件就够了。
Android View性能优化
  • 尽量少的调用notifyDataSetChanged方法,而是优先使用notifyItemXXXXX方法。原因是notifyDateSetChanged会一定执行requestLayout方法,但是notifyItemXXXXX会有条件的执行requestLayout,而这个条件就是sethasFixedSIze的属性。
Android View性能优化

  • 避免多次连续调用notify事件,每次更新应做到一次性通知。

Android View性能优化

4. 自定义onDraw方法。该方案适用于大小位置固定的动画控件。可参考美团的一个案例:https://tech.meituan.com/Dianping_Shortvideo_Battery_TestCase.html

5. 优先使用View动画或者属性动画来执行控件的动画效果。因为这两种动画通过矩阵变化来完成动画展示,而不会更改View的Layout属性,这样不会给其它控件带来刷新的影响。

布局层级优化

  1. 优先使用RelativeLayout或ConstanLayout来实现复杂布局
  2. 尽量使用单个View来完成UI效果,比如使用TextView的compound drawable和text来替代用LinearLayout实现;
  3. 善用merge、include、viewStub标签
  4. 利用第三方工具查找可以优化的布局,比如Android Lint。

Android View性能优化

UI线程工作和内存优化

  1. 将业务逻辑放于子线程,UI线程仅仅处理刷新操作;
  2. 将IO操作和图片转换操作都放于子线程中;
  3. 列表item中复用重复资源,避免每次bind都重新生成,减少内存抖动
  4. 慎用for循环,注意处理效率
  5. 避免主线程和子线程共享一把锁,尤其是耗时锁