美文网首页
性能优化-绘制优化

性能优化-绘制优化

作者: Vinson武 | 来源:发表于2020-02-18 23:06 被阅读0次

    前言

    卡顿场景可分为以下四类:

    1. UI绘制:绘制、刷新
    2. 应用启动:安装启动、冷启动、热启动
    3. 页面跳转:页面间切换、前后台切换
    4. 事件响应:按键、系统事件、滑动

    这四种卡顿场景的根本原因又可以分为两大类:

    1. 界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理
    2. 数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况:
    • 一是数据处理在UI线程(这种应该避免)。
    • 二是数据处理占用CPU高,导致主线程拿不到时间片。
    • 三是内存增加导致GC频繁,从而引起卡顿。

    Android系统显示原理

    Android的显示过程可以简单概括为:Android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到屏幕上,通过Android的刷新机制来刷新数据。

    绘制原理

    应用层

    在Android的每个view绘制中又三个核心步骤:Mesasure、Layout、Draw。通过Measure和Layout来确定当前需要绘制的view所在的大小和位置,通过绘制(Draw)到surface。

    Measure和Layout都是递归来获取view的大小和位置,并且以深度作为优先级,因此层级越深,元素越多,耗时也就越长

    系统层

    应用层和系统层是两个不同进程,在Android的显示系统,使用匿名共享内存:SharedClient,每个应用和SurfaceFlinger之间都会创建一个SharedClient。在每个SharedClient中,最多可以创建31个ShardBufferStack,每个Surface都对应一个ShardBufferStack,也就是一个window。 一个SharedClient对应一个Android应用程序,意味着一个Android应用程序最多可以包含31个窗口

    显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。

    知道绘制原理后,那么绘制一个单元多长时间才是合理的?
    ——在理想情况下,60FPS(Frames Per Second 每秒传递的帧数)就感觉不到卡,这意味着每个绘制时长应该在16ms以内。

    Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染。若每次都成功就能达到流畅画面的60FPS。若某个操作耗时较久,系统在得到VSYNC信号时就无法正常渲染,这样就会发生丢帧现象。

    ==卡顿的根本原因==

    影响绘制的根本原因有以下两方面:

    1. ==绘制任务太重==,绘制一帧内容耗时太长。
    2. ==主线程太忙==,导致VSync信号来时还没有准备好数据导致丢帧。

    性能分析工具

    性能问题不容易复现,在分析性能问题时需要借助相应的调试工具,比如查看Layout层次的Hierarchy View、Android系统自带的 Profile GPU卡顿检测工具和静态代码检查工具Lint,以及性能分析常用的TraceView和SysTrace等。

    卡顿检测工具

    Profile GPU Rendering是Android4.1系统开始提供的开发辅助工具,可在开发者选项中打开(华为手机是:GPU呈现模式分析按钮)

    特点:

    • 是一个图形检测工具,实时反应当前绘制的耗时。
    • 提供一个标准耗时,高于标准耗时,表示当前一帧丢失。


      image.png

    各种颜色含义


    image.png

    技巧:
    在实际开发中,图形不便做数据分析,可通过:adb shell dumpsys gfxinfo com.##.##(包名)把具体的耗时输出到日志中来分析。

    对大部分应用来说丢失几帧影响不大,只需保证大部分在警戒线下即可。通过Profile GPU Rendering发现有问题对页面后,可通过另一个工具Hierarchy Viewer来查看布局层次和每个view所花时间具体定位。

    TraceView

    TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以分析到应用具体每一个方法的执行时间

    1. 使用方法

    在使用TraceView分析问题之前需要得到一个*.trace的文件,然后通过TraceView来分析。trace文件的获取方法有两种:

    • 通过Android Studio的Android Device Monitor,单击Start Method Profile按钮开始监控,操作要监控的界面,完成后stop就会跳到TraceView视图。
    • 代码中加入调试语句保存trace文件:android.os.Debug类中提供类相应的方法,调用代码如下:
    //在开始监控的地方,保存在"/sdcard/trace_name.trace"
    Debug.startMethodTracing("trace_name");
    //...
    //stop trace
    Debug.stopMethodTracing();
    
    1. TraceView 视图说明
      TraceView视图分两个部分,上半部分为时间片面板,下半部分为分析面板。
    • 时间片面板:X轴表示时间消耗,Y表示各个线程,每个线程中的不同方法用不同颜色表示,颜色越宽表示该方法占用CPU时间越长。
    • 分析面板:主要关注Calls + Recur Calls/Total(该方法调用次数+递归次数)和Cpu Time / Call(该方法耗时)这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑。

    SysTrace UI性能分析

    Systrace是Android4.1以上版本提供的性能数据采样和分析工具。功能包括跟踪系统的I/O操作、内核工作队列、CPU负载等。能直观查看CPU周期消耗的具体时间,用不同颜色来突出问题严重性,并提供解决建议。

    注意:由于Systrace从系统角度返回一些信息,并不能定位到具体耗时的方法,要具体分析原因要借助TraceView

    1. Systrace 使用方法
    • 在DDMS上使用:
      (1)打开Android Device Monitor,连接手机准备好需抓取界面;
      (2)单击Systrace进入抓取前的设置,选择需跟踪内容;
      (3)手机操作需跟踪过程;
      (4)到设定时间后,生成Trace文件,使用Chrome打开文件。
    • 使用命令行
    cd android-sdk/platform-tools/systrace
    python systrace.py --time=10 -o mytrace.html sched gfx view wm
    

    具体命令查看官方文档

    • 应用中获取:在应用中加入Trace跟踪需要注意两点:

    (1)Trace嵌套时,endSection()方法只会结束离它最近的一个beginSection()。所以要保证endSection和beginSection调用次数匹配。

    (2)Trace的begin和end必须在同一线程中执行。

    public void ProcessPeople{
      Trace.beginSection("ProcessPeople");
      try{
          Trace.beginSection("Process One");
          try{
              //code
          }finally{
              Trace.endSection(); //end Process One
          }
          Trace.beginSection("Process Two");
          try{
              //code
          }finally{
              Trace.endSection(); //end Process Two
          }
      }finally{
          Trace.endSection(); //end ProcessPeople
      }
    }
    
    1. 分析Systrace报告

    通过前面方法获取到的trace.html文件,需要用Chrome打开,其中和UI绘制关系紧密的是Alerts和Frame两个数据。

    • Alerts:标记了性能有问题的点,可以看到问题的详细描述
    • Frame:每个应用都有一行专门显示frame,它将任何它认为性能有问题的东西都高亮警告并提升怎么优化。

    布局优化

    布局是否合理主要影响的是页面测量时间的多少,如果层级太深,每增加一层则会增加更多的页面显示时间。

    常用布局优化工具

    1. Hierarchy View

    Hierarchy View是Android SDK自带的调试工具,用来检查Layout嵌套及绘制时间,以可视化的布局角度获取Layout布局设计和各种属性信息。

    使用:在Android Studio中打开Android Device Monitor菜单,直接打开Hierarchy View

    • 查看层级图:在window窗口页,选择需要查看的组件,双击或单击Load View Hierarchy按钮即可。
    • 查看某个view的耗时:在快捷键工具栏单击Obtain layout times for tree rooted at selected node。

    一个应用界面非常多,如果一个个用Hierarchy View分析效率低,可以用另一个工具Lint,用于检查所有页面的层级,并把深度高于N的界面输出,然后在用Hierarchy View仔细分析

    2. 布局层级检查

    Android Lint是ADT 16之后引入的代码检查工具,通过代码静态检查,可以发现潜在的代码问题,并给出优化建议。

    使用前可在File -> Setting -> Inspections -> Android Lint中配置扫描规则和缺陷级别。

    在Android studio中启动Lint:从菜单栏选择Analyze -> Inspect Code,进去后选择扫描范围扫描。

    布局优化方法

    通过减少Layout层级,减少测量、绘制时间,提高复用性三方方面来优化布局,,优化的目的是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性。

    1. 减少层级

    减少层级的两个常用方案:

    • 合理使用RelativeLayout和LinearLayout
    • 合理使用Merge

    合理使用RelativeLayout和LinearLayout

    RelativeLayout相对LinearLayout能够减少布局层级,但也存在性能低的问题,原因是RelativeLayout会对子view做两次测量,因为依赖关系可能和布局中view顺序不同,在确定子view位置时,需先给所有子view做一次排序。

    布局原则:

    • 尽量使用RelativeLayout和LinearLayout
    • 在层级相同时,使用LinearLayout
    • 如果用LinearLayout会使层级变多,则应该用RelativeLayout。

    合理使用Merge

    Merge是合并的意思,可以有效优化某些符合条件的多余层级。使用场景如下:

    • 在自定义view中使用,父元素尽量是FrameLayout或者LinearLayout。
    • 在Activity中整体布局,根元素需要是FrameLayout。

    Merge使用要求:

    • Merge只能用在布局XML文件的根元素。
    • 使用Merge加载布局时,必须指定一个ViewGroup作为其父元素,并且设置加载的attachToRoot参数为true。
    • 不能在ViewStub中使用Merge元素。(原因是ViewStub的inflate方法中根本没有attachToRoot的设置)
    2. 提高显示速度

    有时需要某个布局在一开始不显示,在某个条件下才显示,可以通过visable属性来控制,但这样效率非常低,因为虽然布局隐藏来,但还在布局中,仍会解析这些布局。可以使用ViewStub控件来解决这个场景并提高效率。

    ViewStub是一个轻量级的View,它是一个看不见的,并不占布局位置,占用资源非常小的视图对象。

    使用ViewStub注意的点:

    • ViewStub只能加载一次,之后ViewStub对象会被置空。也就是布局被加载后就不能再用ViewStub来控制它的显示隐藏。
    • ViewStub只能用来加载一个布局文件,而不是某个具体的View。
    • ViewStub不能嵌套Merge标签。

    ViewStub主要使用场景:

    • 在程序运行期间,某个布局被加载后,状态就不会有变化。
    • 想要控制一个布局文件的隐藏/显示,而不是某个view
    3. 布局复用

    开发过程中可以将一些公共的布局抽离出来作为一个布局文件,然后在需要使用的地方通过<include/>标签来实现引入。

    对布局优化的总结

    • 尽量使用RelativeLayout和LinearLayout
    • 尽可能少用wrap_content,会增加布局Measure时的计算成本,已知道宽高固定值时不用wrap_content。
    • 将复用组件抽取出来并通过<include/>标签使用
    • 使用<ViewStub/>标签加载按需显示的布局
    • 使用<Merge/>标签减少布局嵌套层级
    • 删除控件中无用属性

    避免过度绘制

    过度绘制的主要原因

    • XML布局:控件有重叠且都有设置背景。
    • View自绘:View.OnDraw里面同一个区域被绘制多次。

    过度绘制检测工具

    通过手机设置中开发者选项,打开Show GPU Overdraw选项(调试 GPU 过度绘制),打开后会有不同的颜色区域表示不同的过度绘制次数。不同颜色含义如下:

    • 无色:没有过度绘制,每个像素只绘制来1次
    • 蓝色:过度绘制 1 次(大片蓝是可以接受的)
    • 绿色:过度绘制 2 次
    • 粉色:过度绘制 3 次(不要超过1/4)
    • 红色:过度绘制 4 次或更多次(需优化)

    我们的目标是减少红色Overdraw,看到更多蓝色或无色区域。

    如何避免过度绘制

    1. 布局上的优化
    • 移除XML中非必需的背景,或根据条件设置
    • 移除Window默认背景
    • 按需显示占位背景图片

    在Android自带的一些主题时activity往往会设置一个默认的背景,这个背景有DecorView持有。当自定义布局有一个全屏背景时,DecorView的背景此时对我们来说是无用的,但会产生一次Overdraw,因此可以移除

    protect void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        this.getWindow().setBackgroundDrawaable(null);
    }
    
    1. 自定义View优化

    自定义view能减少layout的层级,但在实际绘制时容易出现过度绘制。可以通过canvas.clipRect()来帮组系统识别那些可见的区域,然后只在这个区域绘制。canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

    启动优化

    应用启动流程

    启动分两种类型:冷启动和热启动

    • 冷启动:系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化Activity,最后显示在界面。
    • 热启动:会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化Activity。

    启动 -> Application -> attachBaseContext() -> onCreate() -> Activity生命周期

    启动耗时检测

    1. adb shell am:使用adb shell获取应用真实启动时间代码
    adb shell am start -W [packageName]/[packageName.AppstartActivity]
    

    执行后得到三个时间

    • ThisTime:一般和TotalTime时间一样,如果启动时加来过度全透明的页面预先处理一些事,这样会比TotalTime小;
    • TotalTime:应用启动时间,包括Application和Activity初始化到界面显示;
    • WaitTime:包括系统影响的耗时。

    但这个方法只能得到固定某个阶段耗时,不能知道具体方法耗时。可用代码打点方式来得到具体方法耗时。

    1. 代码打点

    启动优化方案

    启动主要完成三件事:UI布局、绘制和数据准备,因此优化启动速度也是优化这三个过程。

    1. UI布局优化
    • 减少布局层级
    • 避免过度绘制
    1. 启动加载逻辑优化
      数据按需实现加载逻辑
    • 分步加载:以大化小,优先级高的放前
    • 异步加载:耗时多的异步化
    • 延期加载:非必需的数据延时加载

    合理的刷新机制

    合理的刷新机制要注意以下几点;

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

    减少刷新次数

    1. 控制刷新频率:比如刷新进度调可1%刷新一次,而不是实时刷新
    2. 避免没有必要的刷新:先判断是否需要刷新,比如数据没变化、控件不在可见区域就没必要刷新。

    避免后台线程影响

    后台线程如果开销很大,占用CPU过高,导致系统频繁GC和CPU时间片资源紧张,有可能会导致页面的卡顿。因此在需要迅速刷新的情况下避免这类线程在高峰工作。

    缩小刷新区域

    采用局部刷新来节约资源

    • 自定义view时:可以使用两个局部更新数据的方法
    invalidate(Rect dirty)
    invalidate(int left, int top, int right, int bottom)
    
    • 容器中的某个Item发生了变化,只需更新这个Item即可。

    提升动画性能

    从三个纬度来对比动画性能

    • 流畅度:核心,控制每一帧动画在16ms以内完成
    • 内存:避免内存泄漏,减小内存开销
    • 耗电:减小运算量,优化算法,减小CPU占用

    优化建议

    • 尽量使用属性动画
    • 适当使用硬件加速

    使用硬件加速注意几点:

    • 在软件渲染时,可以使用重用Bitmap的方法来节省内存,但开启硬件加速后不起作用。
    • 开启硬件加速的View在前台运行时,需要耗费额外的内存,加速的UI切换到后台时,产生的额外内存有可能不释放。
    • 当UI中存在过度绘制时,硬件加速容易发生问题。

    参考书籍:《Android应用性能优化最佳实践》

    相关文章

      网友评论

          本文标题:性能优化-绘制优化

          本文链接:https://www.haomeiwen.com/subject/rhfofhtx.html