热门标签 | 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。

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

推荐阅读
  • 网卡工作原理及网络知识分享
    本文介绍了网卡的工作原理,包括CSMA/CD、ARP欺骗等网络知识。网卡是负责整台计算机的网络通信,没有它,计算机将成为信息孤岛。文章通过一个对话的形式,生动形象地讲述了网卡的工作原理,并介绍了集线器Hub时代的网络构成。对于想学习网络知识的读者来说,本文是一篇不错的参考资料。 ... [详细]
  • STM32 IO口模拟串口通讯
    转自:http:ziye334.blog.163.comblogstatic224306191201452833850647前阵子,调项目时需要用到低波 ... [详细]
  • Android图形架构学习笔记(待修改)
    以下简单总结来自Android官网,稍作总结:https:source.android.google.cndevicesgraphics概览Andr ... [详细]
  • x86 linux的进程调度,x86体系结构下Linux2.6.26的进程调度和切换
    进程调度相关数据结构task_structtask_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • Firefox火狐浏览器关闭到http://detectportal.firefox.com的流量问题解决办法
    本文介绍了使用Firefox火狐浏览器时出现关闭到http://detectportal.firefox.com的流量问题,并提供了解决办法。问题的本质是因为火狐默认开启了Captive portal技术,当连接需要认证的WiFi时,火狐会跳出认证界面。通过修改about:config中的network.captive-portal-service.en的值为false,可以解决该问题。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文详细介绍了如何使用MySQL来显示SQL语句的执行时间,并通过MySQL Query Profiler获取CPU和内存使用量以及系统锁和表锁的时间。同时介绍了效能分析的三种方法:瓶颈分析、工作负载分析和基于比率的分析。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 在2022年,随着信息化时代的发展,手机市场上出现了越来越多的机型选择。如何挑选一部适合自己的手机成为了许多人的困扰。本文提供了一些配置及性价比较高的手机推荐,并总结了选择手机时需要考虑的因素,如性能、屏幕素质、拍照水平、充电续航、颜值质感等。不同人的需求不同,因此在预算范围内找到适合自己的手机才是最重要的。通过本文的指南和技巧,希望能够帮助读者节省选购手机的时间。 ... [详细]
  • Imdevelopinganappwhichneedstogetmusicfilebystreamingforplayinglive.我正在开发一个应用程序,需要通过流 ... [详细]
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社区 版权所有