热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android降低UI渲染速度的检测、诊断及修复

一.Slowrendering-jank为了保证UI交互的流畅,必须保证每帧的渲染时间不超过16毫秒,保证60的FPS。一旦界面有较慢的渲染,系统将强制跳帧,用户就

一. Slow rendering - jank

  • 为了保证UI交互的流畅,必须保证每帧的渲染时间不超过16毫秒,保证60的FPS。
  • 一旦界面有较慢的渲染,系统将强制跳帧,用户就会感觉到卡顿。
  • We call this jank.

二. 定位jank

1. 三种定位方法

想要准确定位发生jank的代码并不容易,以下三个办法可以帮助开发者:
- 视觉检查:可以快速直观的发现jank界面
- Systrace:能提供更多的细节信息
- FrameMetricsAggregator:在Firebase Performance Monitoring中可以查看分析数据

2. 方法一:使用视觉检查

打开app,手动切换不同的界面并查看哪些疑似jank。下面是一些检查经验:
1. 应当启动release版本的app,或不可调式版本的app。因为ART运行时为了支持调式功能,禁用了一些重要的优化。
2. 在开发者选项中,打开Profile GPU Rendering开关(GPU呈现模式)。Profile GPU Rendering可以直观地展示绘制耗时。不同的颜色代表了不同的绘制操作。
3. 有一些组件常常会导致jank,比如RecyclerView。可以重点关注包含这些组件的界面。
4. 有些jank只会发生在冷启动过程中。
5. 尽量在性能更低的测试机上做检查,原因你懂的—–让卡的变的更卡、更明显。

3. 方法二:使用systrace

Systrace能有效地发现jank,而且系统开销极小。有两种启动方法:
- 通过device monitor来启动systrace
- 现在AS默认不集成Monitor,可以使用python来启动(需要python环境):

`python systrace.py --time=10 -o mynewtrace.html sched gfx view wm`

4. 方法三:使用FrameMetricsAggregator

使用FrameMetricsAggregator来收集app帧渲染的时间,使用Firebase Performance Monitoring来记录和分析数据。

三. 修复jank

  • 你需要坚持哪些帧没有在16.7毫秒内渲染完成,然后发现问题。
  • 一般来说,将耗时任务放在异步工作线程可以有效避免jank。
  • 有个有效的办法:经常注意代码执行在哪个线程中,并且在耗时代码中检查当前线程,如果是主线程则发出警告。
  • 如果有非常重要且复杂的UI,比如Scrolling List,考虑用Automate UI performance tests
  • 下面将列举一些常见的jank原因

四. 常见jank原因

1. Scrollable lists

  • ListView,尤其是RecyclerView,你应该使用Systrace来查看它们是否导致jank。

2. RecyclerView: notifyDataSetChanged

  • 如果RecyclerView每个item正在重新绑定(将会导致重新布局和绘制),请不要使用notifyDataSetChanged(), setAdapter(Adapter), 或者 swapAdapter(Adapter, boolean) 来更新很小部分的数据。它们会标示整个列表都发生改变,在Systrace中会显示为RV FullInvalidate。
  • 应当使用SortedListDiffUtil 来进行少量更新或增加。
  • 示例代码,考虑从服务端获取一个新的信息list,使用notifyDataSetChanged:

    void onNewDataArrived(List news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }

    但这有个很严重的潜在问题:如果list变动很小,比如仅仅是新增了一个数据,RecyclerView将会清除所有item缓存,重新绑定所有的item views。

  • 示例代码,使用DiffUtil

    void onNewDataArrived(List news) {
        List oldNews = myAdapter.getItems();
        DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
        myAdapter.setNews(news);
        result.dispatchUpdatesTo(myAdapter);
    }

    而你只需要实现接口DiffUtil.Callback来告诉DiffUtil该怎样检查list对比结果。

3.RecyclerView: Nested RecyclerViews

  • 嵌套RecyclerView,特别是水平滑动list里面嵌套一个竖直RecyclerViews。
  • 当你首次滑动页面,而如果有太多的内部item,可以在内部RecyclerViews中考虑使用RecyclerView.RecycledViewPools。
  • 如果你有一打或更多RecyclerView需要显示,而且他们的itemViews类似,就应该将itemViews在各个RecyclerView中共享:

    class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
        RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
    
        ...
    
        @Override
        public void onCreateViewHolder(ViewGroup parent, int viewType) {
            // inflate inner item, find innerRecyclerView by ID…
            LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                    LinearLayoutManager.HORIZONTAL);
            innerRv.setLayoutManager(innerLLM);
            innerRv.setRecycledViewPool(mSharedPool);
            return new OuterAdapter.ViewHolder(innerRv);
    
        }
        ...

    如果想要进一步优化,对LinearLayoutManager调用setInitialPrefetchItemCount(int)

    比如每行可以显示3到5个item,调用innerLLM.setInitialItemPrefetchCount(4);来通知一个RecyclerView,当一个水平行将要显示时,如果UI线程有空,它应该预取内部的4个项目。

4. RecyclerView: Too much inflation / Create taking too long

  • UI线程有空时,使用预取功能使inflation Layout工作更有效率。
  • 如果您在一帧中看到 inflation Layout (而不是标记为 RV Prefetch 的部分),请确保您正在测试最近的设备( Prefetch 目前仅在 Android 5.0 API Level 21 及更高版本上支持),并使用最近版本的Support Library.
  • 当新的item显示在屏幕时,如果发现inflation 导致jank,RecyclerView中的view可能就太多了点,需要删除多余的view。
  • 如果在各个view类型中只有一个图标,颜色,或一条文本信息的差别,就有理由合并view类型,在绑定时改变这些信息。这样可以避免inflate,同时减少内存占用。

5. RecyclerView: Bind taking too long

  • 尽量减少onBindViewHolder(VH, int)的调用时间,不要在其中做多余的事情。
  • 如果只是一些简单的pojo数据,尽量不要使用Data Binding library(数据绑定库)。

6. RecyclerView or ListView: layout / draw taking too long

  • 尽可能减少布局层次复杂度。

7. ListView: Inflation

  • 确保ListView的缓存机制运作正常。缓存的View不应当再次inflate。如果每当屏幕显示item都会inflate(即使它已经显示过了),说明缓存复用机制失效了。
  • 示例代码:

    view getView(int position, View convertView, ViewGroup parent) {
    
        if (cOnvertView== null) {
            // 仅当第一次显示时flate,此处应添加到缓存
            cOnvertView= mLayoutInflater.inflate(R.layout.my_layout, parent, false)
        }
        // 这里绑定相应的数据到convertView
        return convertView;
    }

8. layout性能

如果Systrace 显示Layout的Choreographer#doFrame做了太多事情,或者太过频繁,那就表示layout性能存在问题。如果View 层次结构改变layout参数或输入,就会导致layout性能问题。

  • Layout performance: Cost

    如果这部分超过了几毫秒,很可能的原因就是RelativeLayoutsweighted-LinearLayouts的嵌套布局碰到了最坏的情况。每个layout都会触发其子View的多次measure/layout,所以这些layout的嵌套会导致layout时间开销为基于嵌套层次的O(n^2)。

    有一些方法可以可供参考:
    - 重新充足View层次结构
    - 自定义Layout,修改其layout部分
    - 使用ConstraintLayout。这个布局可以满足类似的需求,同时可以避免性能缺陷。

  • Layout performance: Frequency
    当新内容出现时,将会发生新的Layout。例如,当一个新item在RecyclerView中滚动到屏幕中。

    如果每帧都有重要的layout,而此时又有可能正在变动layout,这种情况下极有可能会掉帧。修改layout参数会导致重新layout。

    • 想要减少开销,请使用View属性动画(比如setTranslationX/Y/Z(), setRotation(), setAlpha()等等),它比改变layout属性(比如padding或margin)的性能开销小的多。

    • 通过触发invalidate()(接下来会在下一帧draw)改变View的属性,其性能也比改变layout属性要更好。这将重新记录无效的视图的绘制,并且同样也比布局性能好的多。

9.Rendering performance: UI Thread

Android UI会在两个阶段开始工作:
- 在UI线程中,Record View#draw:

在每个无效的View上绘制(Canvas),并可能调用自定义视图或代码。

- 在RenderThread中,DrawFrame

在本地RenderThread上运行,但是将根据`Record View#draw`阶段生成的工作进行操作。
  1. Rendering performance: UI Thread

    如果Record View#draw花费了大量时间,经常可能的原因就是Bitmap正在UI线程中被绘制。绘制Bitmap会用到CPU渲染,所以一般要尽量避免。可以使用Android CPU Profiler来检测这个问题。

    • 绘制位图通常是在应用程序想要在显示位图之前修饰位图的时候完成的。比如绘制圆角图片:

      Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
      Paint paint = new Paint();
      paint.setAntiAlias(true);
      // draw a round rect to define shape:
      bitmapCanvas.drawRoundRect(0, 0,
              roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 
              20, 20, paint);
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
      // multiply content on top, to make it rounded
      bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
      bitmapCanvas.setBitmap(null);
      // now roundedOutputBitmap has sourceBitmap inside, but as a circle

      如果这是UI线程中的工作,你可以将这些工作放在后台启动的解码线程中,甚至可以放在draw的时候。

    • 示例代码,性能差的的代码:

      void setBitmap(Bitmap bitmap) {
          mBitmap = bitmap;
          invalidate();
      }
      
      void onDraw(Canvas canvas) {
          canvas.drawBitmap(mBitmap, null);
      }
    • 示例代码,提升性能的改变:

      void setBitmap(Bitmap bitmap) {
          mShaderPaint.setShader(
                  new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
          invalidate();
      }
      
      void onDraw(Canvas canvas) {
          canvas.drawRoundRect(0, 0, mWidth, mHeight, 20, 20, mShaderPaint);
      }
    • 这通常也可以用于后台保护(在位图顶部绘制渐变)和图像过滤(使用ColorMatrixColorFilter)。

    • 如果由于其他原因(可能将其用作缓存)绘制到位图,则尝试绘制直接传递到View或Drawable的硬件加速硬件,如有必要,可考虑使用LAYER_TYPE_HARDWARE调用setLayerType()来缓存复杂的渲染 输出,并仍然利用GPU渲染。

  2. Rendering performance: RenderThread

    一些Canvas操作记录开销很小,但会在RenderThread中触发昂贵的计算。Systrace会对此作出警告的:

    • Canvas.saveLayer(): 尽力避免。
      它将触发每帧昂贵的、无缓存的离屏渲染,应当尽量避免,或者至少确保传递CLIP_TO_LAYER_SAVE_FLAG(或者调用一个不带flag的变量).

    • Animating large Paths:

      当硬件加速Canvas传递给Views时,Canvas.drawPath()被调用,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果路径较大,请避免逐帧编辑,以便高速缓存和绘制。

      drawPoints(), drawLines(), drawRect/Circle/Oval/RoundRect() 效率更高,即使最终调用了更多的draw方法。

    • Canvas.clipPath:
      会触发昂贵的裁剪操行为,应尽量避免。

      如果可能,应选择绘制形状,而不是裁剪到非矩形形状。绘制的性能更好,而且抗锯齿。

      • 示例代码,性能差的:

        canvas.save();
        canvas.clipPath(mCirclePath);
        canvas.drawBitmap(mBitmap);
        canvas.restore();
      • 示例代码,优化过的:

        // one time init:
        mPaint.setShader(new BitmapShader(mBitmap, TileMode.CLAMP, TileMode.CLAMP));
        // at draw time:
        canvas.drawPath(mCirclePath, mPaint);
  3. Bitmap uploads

    Android将位图显示为OpenGL纹理,并且首次在一帧中显示位图时,将其上传到GPU。可以在Systrace中将此视为上传宽度x高度纹理。这可能需要几个毫秒(见下图),但是有必要用GPU显示图像。

    image

    如果这些花费很长时间,请首先检查trace中的宽度和高度。如果正在显示的位图比屏幕大很多,就是浪费时间和空间。一般bitmap加载库会提供简单的方法来请求大小合适的位图。

    在Android 7.0中,一些图片库可能会在需要图片之前调用预加载方法prepareToDraw()来触发更早的GPU上传,此时RenderThread 是空闲状态。

    这个操作可以在解码后做,也可以在将图片绑定到View时来做。

    一般来说,图片库会帮你做这个。除此之外,如果想自己管理图片,或想确定不会在更新的设备上传,也可以在合适的地方手动调用prepareToDraw()

10.线程调度延迟(Thread scheduling delays)

  • Systrace 会用不同的颜色来指示线程状态:

    • 灰色:Sleeping ,睡眠
    • 蓝色:Runnable,可以运行,但调度器还没有选择它运行
    • 绿色:Actively running ,正在运行
    • 红色或橙色:Uninterruptible sleep
      这在调试因线程调度延迟导致的jank问题时,非常有用。

    image
    图中可以看到,UI线程在RenderThread的syncFrameState 正在运行时和bitmap上传时会被阻塞,

  • 另一种情况:RenderThread在使用IPC时会被阻塞:在帧的开始处获取缓冲区,从中查询信息,或者通过eglSwapBuffers将缓冲区传回给合成器。

    在最近版本的Android中,导致UI线程停止的原因常常就是IPC。
    而修复措施如下:

    • 尽量避免远程调用
    • 如果必须要用远程调用,请缓存数据以便返回,或在后台线程中调用远程方法。
  • 可以通过adb命令来捕获binder transactions的方法调用栈:

    $ adb shell am trace-ipc start
    … use the app - scroll/animate ...
    $ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
    $ adb pull /data/local/tmp/ipc-trace.txt
  • 有时像getRefreshRate()这样的貌似无害的调用可能会触发binder transactions,并在频繁调用时导致严重的问题。定期跟踪可以快速找到并解决这些问题:

    image

    上图显示,在RV fling中的binder transactions导致UI线程睡眠。请保持简洁的bind逻辑,使用trace-ipc 来追踪并删除远程调用。

  • 如果你没有看到绑定Activity,但仍然没有看到你的UI线程运行,确保没有等待另一个线程的锁或其他操作。通常,UI线程不应该等待来自其他线程的结果。

11. 对象分配和GC

Systrace会告诉你GC是否频繁运行,Android Memory Profiler可以显示对象分配发生在哪儿。

image
HeapTaskDaemon thread中,花费94ms的GC。

  • 请尽量避免在密集的循环中分配对象。

推荐阅读
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 本文详细介绍了使用 Python 进行 MySQL 和 Redis 数据库操作的实战技巧。首先,针对 MySQL 数据库,通过 `pymysql` 模块展示了如何连接和操作数据库,包括建立连接、执行查询和更新等常见操作。接着,文章深入探讨了 Redis 的基本命令和高级功能,如键值存储、列表操作和事务处理。此外,还提供了多个实际案例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文介绍了如何利用 `matplotlib` 库中的 `FuncAnimation` 类将 Python 中的动态图像保存为视频文件。通过详细解释 `FuncAnimation` 类的参数和方法,文章提供了多种实用技巧,帮助用户高效地生成高质量的动态图像视频。此外,还探讨了不同视频编码器的选择及其对输出文件质量的影响,为读者提供了全面的技术指导。 ... [详细]
  • 开发日志:高效图片压缩与上传技术解析 ... [详细]
  • 2.2 组件间父子通信机制详解
    2.2 组件间父子通信机制详解 ... [详细]
  • 体积小巧的vsftpd与pureftpd Docker镜像在Unraid系统中的详细配置指南:支持TLS加密及IPv6协议
    本文详细介绍了如何在Unraid系统中配置体积小巧的vsftpd和Pure-FTPd Docker镜像,以支持TLS加密和IPv6协议。通过这些配置,用户可以实现安全、高效的文件传输服务,适用于各种网络环境。配置过程包括镜像的选择、环境变量的设置以及必要的安全措施,确保了系统的稳定性和数据的安全性。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • SQLite数据库CRUD操作实例分析与应用
    本文通过分析和实例演示了SQLite数据库中的CRUD(创建、读取、更新和删除)操作,详细介绍了如何在Java环境中使用Person实体类进行数据库操作。文章首先阐述了SQLite数据库的基本概念及其在移动应用开发中的重要性,然后通过具体的代码示例,逐步展示了如何实现对Person实体类的增删改查功能。此外,还讨论了常见错误及其解决方法,为开发者提供了实用的参考和指导。 ... [详细]
  • 在过去,我曾使用过自建MySQL服务器中的MyISAM和InnoDB存储引擎(也曾尝试过Memory引擎)。今年初,我开始转向阿里云的关系型数据库服务,并深入研究了其高效的压缩存储引擎TokuDB。TokuDB在数据压缩和处理大规模数据集方面表现出色,显著提升了存储效率和查询性能。通过实际应用,我发现TokuDB不仅能够有效减少存储成本,还能显著提高数据处理速度,特别适用于高并发和大数据量的场景。 ... [详细]
  • 本文介绍了一种利用PHP cURL库高效提取Sohu邮箱联系人列表的方法。通过设置错误报告级别、定义Cookie文件路径等关键步骤,确保了代码的稳定性和可靠性。经过实际测试,该方法在2012年3月24日被验证为有效,能够快速准确地获取联系人信息。此外,文章还提供了详细的代码示例和注意事项,帮助开发者更好地理解和应用这一技术。 ... [详细]
  • 过去查询Mysql的时候,都见3306对所有端口开放着,感觉不安全。netstat&nbsp;-anlp&nbsp;|&nbsp;grep&nbsp;mysqltcp&nbsp;0&am ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 本文详细介绍了在CentOS 6.5 64位系统上使用阿里云ECS服务器搭建LAMP环境的具体步骤。首先,通过PuTTY工具实现远程连接至服务器。接着,检查当前系统的磁盘空间使用情况,确保有足够的空间进行后续操作,可使用 `df` 命令进行查看。此外,文章还涵盖了安装和配置Apache、MySQL和PHP的相关步骤,以及常见问题的解决方法,帮助用户顺利完成LAMP环境的搭建。 ... [详细]
  • Node.js 中的椭圆曲线 Diffie-Hellman 密钥交换方法 `crypto.createECDH()` 使用详解 ... [详细]
author-avatar
克阳光沫沫的幸福
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有