Android App 反应卡顿,从技术上将就是UI 渲染慢。

UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因UI渲染速度缓慢而受到影响,那么系统将被迫跳过帧,用户将感觉到您的应用程序中出现卡顿。 我们把这个叫做jank

本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:

1.UI 渲染简介
2.识别Jank
3.Fix Jank
4.引起Jank 通用问题举例

1.UI 渲染简介

为了帮助您提高应用程序质量,Android会自动监视您的应用程序是否有空,并在Android生命危险仪表板中显示信息。 有关如何收集数据的信息,请参阅Play Console文档。

如果您的应用程序出现问题,本页提供诊断和解决问题的指导。

Android生命危险仪表板和Android系统会跟踪使用UI Toolkit的应用程序的渲染时间统计信息(应用程序的用户可见部分是从CanvasView hierarchy绘制的)。

如果您的应用程序不使用UI Toolkit,就像使用VulkanUnityUnrealOpenGL构建的应用程序一样,则在Android Vitals仪表板中不提供时间统计信息。

您可以通过运行
adb shell dumpsys gfxinfo <package name> 来确定您的设备是否正在记录您的应用的渲染时间指标。

2.识别Jank

在您的应用程序中定位引起jank的代码可能很困难。 本部分介绍了三种识别jank的方法:

  • 1.Visual inspection

通过视觉检查,您可以在几分钟内快速浏览应用程序中的所有用例use-cases,但不能提供与Systrace相同的详细信息。

  • 2.Systrace

Systrace提供了更多的细节,但是如果你运行Systrace来处理应用程序中的所有用例,那么就会被大量的数据淹没,难以分析。

  • 3.Custom performance monitoring

Visual inspectionSystrace都会在你的本地设备上检测到。

如果不能在本地设备上重现,则可以构建自定义性能监视器Custom performance monitoring,以测量在现场运行的设备上应用的特定部分。

1. Visual inspection

目视检查可以帮助您识别正在生产结果的使用案例。 要执行视觉检查,请打开您的应用程序并手动检查应用程序的不同部分,然后查看非常粗糙的UI。 以下是进行目视检查时的一些提示:

  • 1.运行release 版本

运行您release应用程序的版本(或至少不可调试)的版本。ART运行时为了支持调试功能而禁用了一些重要的优化,所以确保你正在寻找类似于用户将看到的东西。

  1. 开启GPU渲染

开启步骤:
Settings -->Developer options -->Profile GPU rending

开启配置文件GPU渲染,会在屏幕上显示条形图,可以快速直观地显示相对于每帧16毫秒基准测试渲染UI窗口帧所花费的时间。
每个条都有着色的组件映射到渲染管道中的一个舞台,所以你可以看到哪个部分花费的时间最长。
例如,如果框架花费大量时间处理输入,则应该查看处理用户输入的应用程序代码。

  1. 留意特殊组件

有一些组件,如RecyclerView,是Jank普遍的来源。 如果您的应用程序使用这些组件,那么运行应用程序的这些部分是一个好idea

  1. App 冷启动导致

有时候,只有当应用程序从冷启动启动(Clod start)时,才能复制jank

  • 5.低内存情况下jank 比较容易出现

一旦你发现产生jank的用例,你可能会有一个很好的想法是什么导致你的应用程序的结果。 但是,如果您需要更多信息,则可以使用Systrace进一步深入研究。

2. Systrace

Systrace是一个显示整个设备在做什么的工具,并且它可以用于识别应用程序中的JankSystrace的系统开销很小,所以在仪器使用过程中你会感受到app卡顿的存在。

Systrace记录跟踪,同时在设备上执行janky用例。 有关如何使用Systrace的说明,请参阅Systrace演练。 systrace被进程和线程分解。 在Systrace中查找应用程序的过程,应该如图所示。

android 动画 setRepeatCount android 动画卡顿_UI

上面3个标注点解释

  1. 当卡顿时,会有掉帧发生,如上图1所示

Systrace显示何时绘制每个框架,并对每个框架进行颜色编码以突出显示较慢的渲染时间。 这可以帮助您查找比视觉检查更准确的单个janky框架。 有关更多信息,请参阅Inspecting Frames.

  1. 掉帧提示,如上图 2所示
    Systrace检测应用程序中的问题,并在各个框架和警报面板中显示警报。 警报中的以下指示是您的最佳选择。
  2. systrace timeline 如上图3 所示

Android框架和库的一部分(如RecyclerView)包含跟踪标记。 因此,systrace时间线会显示何时在UI线程上执行这些方法,以及执行多长时间。

如果systrace没有向您显示有关长时间使用UI线程工作的详细信息,则需要使用Android CPU Profiler来记录采样或检测的方法跟踪。 一般来说,method方法痕迹不适合用于识别排队,因为由于开销太大而产生假jank,并且无法看到线程何时被阻塞。 但是,method方法跟踪可以帮助您识别应用中花费最多时间的方法。 在识别这些方法后,add Trace markers a
标记并重新运行systrace,以查看这些方法是否引起混乱。
当记录systrace时,每个跟踪标记(执行的开始 Trace.beginSection();和结束Trace.endSection();对)会增加大约10μs的开销。 为了避免假Jank结局,不要将追踪标记添加到在一帧中被称为几十次的方法中,或者短于200us左右。

如需获取更多内容,请查看Systrace详解

3. Custom performance monitoring

如果您无法在本地设备上再现突发事件,则可以在您的应用中构建自定义性能监控,以帮助识别现场设备上的突发源。

为此,请使用FrameMetricsAggregator从应用程序的特定部分收集帧渲染时间,并使用Firebase性能监控记录和分析数据。

要了解更多信息,请参阅使用Use Firebase Performance Monitoring with Android Vitals.

3.Fix Jank

为了解决这个问题,请检查哪些帧在16.7ms内没有完成,并寻找出错的地方。Record View#draw在一些帧中抽取异常长度,或者可能是Layout? 查看下面4这些问题的常见来源,以及其他问题。

为了避免乱码,长时间运行的任务应该在UI线程之外异步运行。 一定要注意你的代码正在运行在哪个线程上,并且在向主线程发布不重要的任务时要小心。

如果您的应用程序有一个复杂而重要的主UI(可能是中央滚动列表),请考虑编写可自动检测缓慢渲染时间的测试测试,并经常运行测试以防止出现回归。 有关更多信息,请参阅自动化性能测试代码实验室。

4.引起Jank 通用问题举例

以下部分解释了应用程序中常见Jank问题 的来源,以及解决这些问题的最佳方案。

滑动 List

ListView和特别是RecyclerView通常用于复杂的滚动列表,这些列表最容易被忽略。 他们都包含Systrace标记,所以你可以使用Systrace来弄清楚他们是否有助于在你的应用程序jank。 一定要传递命令行参数-a <your-package-name>来获取RecyclerView中的跟踪部分(以及添加的任何跟踪标记)以显示出来。 如果可用,请遵循systrace输出中生成的警报的指导。 在Systrace里面,你可以点击RecyclerView-traced部分查看RecyclerView正在做的工作的解释。

RecyclerView: notifyDataSetChanged

如果您看到RecyclerView中的每个项目在一个框架中被反弹(并因此重新布局和重新绘制),请确保您没有调用notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter,boolean)为小更新。 这些方法表示整个列表内容已经改变,并且将在Systrace中显示为RV FullInvalidate。 而是在内容更改或添加时使用SortedListDiffUtil生成最小更新。

例如,考虑从服务器接收新闻内容列表的新版本的应用程序。 当您将该信息发布到适配器时,可以调用notifyDataSetChanged(),如下所示:

android 动画 setRepeatCount android 动画卡顿_人工智能_02


但是这带来了一个很大的缺点 - 如果它是一个微不足道的变化(也许单个项目添加到顶部),RecyclerView不知道 - 它被告知放弃所有的缓存项目状态,因此需要重新绑定一切。最好使用DiffUtil,它将为您计算和分配最小的更新。

android 动画 setRepeatCount android 动画卡顿_应用程序_03

只需将您的MyCallback定义为DiffUtil.Callback实现,以通知DiffUtil如何检查您的列表。

RecyclerView: Nested RecyclerViews

嵌套RecyclerView是很常见的,特别是水平滚动列表的垂直列表(如Play Store主页上的应用程序的网格)。 这可以很好的工作,但也有很多意外四处移动。 如果在第一次向下滚动页面时看到很多内部项目膨胀,则可能需要检查是否在内部(水平)RecyclerViews之间共享RecyclerView.RecycledViewPools

默认情况下,每个RecyclerView将拥有自己的物品池。 如果在屏幕上同时显示一打itemViews,那么当itemViews不能被不同的水平列表共享的时候,如果所有的行都显示了相似类型的视图,那么这是有问题的。

android 动画 setRepeatCount android 动画卡顿_UI_04

如果要进一步优化,还可以在内部RecyclerViewLinearLayoutManager上调用setInitialPrefetchItemCount(int)
例如,如果您总是在一行中可见3.5项,请调用innerLLM.setInitialItemPrefetchCount(4);. 这将告诉RecyclerView,当一个水平行即将出现在屏幕上时,如果UI线程上有空闲时间,它应该尝试预取内部的项目

RecyclerView: Too much inflation / Create taking too long

UI线程则处于闲置状态下,RecyclerView中的预取功能应该有助于在大多数情况下通过提前完成工作来解决inflation Layout的成本问题。
如果您在一帧中看到inflation Layout(而不是标记为RV Prefetch的部分),请确保您正在测试最近的设备(Prefetch目前仅在Android 5.0 API Level 21及更高版本上支持),并使用最近版本的Support Library.

如果经常看到inflation Layout导致屏幕上出现新的Jank,验证出问题,请移除多余的ViewRecyclerView内容中的视图类型越少,当新的项目类型出现在屏幕上时,需要完成的inflation Layout就越少。

如果可能的话,将视图类型合并到合理的位置 - 如果只有图标,颜色或文本块在类型之间改变,则可以在绑定时间进行更改,并避免inflation Layout(同时减少应用程序的内存占用)。

如果您的视图类型看起来还不错,请考虑减少inflation Layout的成本。减少不必要的容器和结构视图可以帮助 - 考虑使用ConstraintLayout构建itemView,这可以很容易地减少结构视图。如果你想真正优化性能,你的项目层次结构是简单的,并且你不需要复杂的themingstyle的功能,请考虑自己调用构造函数 - 但请注意,它往往是不值得的损失的简单性和功能的权衡XML。

RecyclerView: Bind taking too long

绑定(即onBindViewHolder(VH,int))应该是非常简单的,除了最复杂的项目之外的所有项目都要花费少于一毫秒的时间。 它只需从adapter's的内部项目数据中获取POJO项目,然后在ViewHolder中的视图上调用setter。 如果RV OnBindView需要很长时间,请确认您在绑定代码中做了最少的工作。

如果您使用简单的POJO对象来保存适配器中的数据,则可以完全避免使用Data Binding l
库来将绑定代码写入onBindViewHolder

RecyclerView or ListView: layout / draw taking too long

有关绘制和布局的问题,请参阅 Layout and Rendering Performance.

ListView: Inflation

如果你不小心,ListView很容易会被意外回收。 如果每次屏幕显示项目时都看到inflation Layout,请检查Adapter.getView()的实现是否正在使用,重新绑定并返回convertView参数。 如果你的getView()实现总是inflation Layout,你的应用程序将无法从ListView中获得回收的好处。 你的getView()的结构几乎总是类似于下面的实现:

android 动画 setRepeatCount android 动画卡顿_移动开发_05

Layout performance

如果Systrace显示Choreographer#doFrame的布局部分工作太多,或者工作频繁,这意味着您遇到了布局性能问题。 您的应用的布局性能取决于View层次结构的哪个部分具有更改布局参数或输入。

Layout performance: Cost

如果段长度超过几毫秒,则可能是针对RelativeLayoutsweighted-LinearLayouts.
的最差嵌套性能。

这些布局中的每一个都可以触发其子项的多个measure/layout传递,因此嵌套它们会导致嵌套深度上的O(n ^ 2)行为。 请尝试避免使用RelativeLayoutLinearLayoutweight特征,除了层次结构的最低叶节点之外的所有特征。 有几种方法可以做到这一点:

  • 优化View结构
  • 使用自定义View
  • 尝试转换到ConstraintLayout,它提供了类似的功能,并且没有性能上的缺陷。
Layout performance: Frequency

当新内容出现在屏幕上时,将会发生新的Layout,例如,当一个新项目在RecyclerView中滚动查看时。 如果在每个框架上都发生重要的布局,则可能是在布局上进行动画处理,这很可能导致丢帧。 通常,动画应该在View的绘图属性(例如setTranslationX / Y / Z()setRotation(),setAlpha()等)上运行。 这些都可以比Layout属性(如填充或边距)更好地更改。 通常通过调用触发invalidate()setter,然后在下一帧中绘制(Canvas),来更改视图的绘制属性。 这将重新记录无效的视图的绘图操作,并且通常也比布局好得多。

Rendering performance渲染性能

Android UI在两个阶段工作 - 在UI线程Record View#draw,在RenderThread上绘制DrawFrame。 第一次运行在每个无效的View上绘制(Canvas),并可能调用自定义视图或代码。 第二个在本地RenderThread上运行,但是将根据Record View#draw阶段生成的工作进行操作。

Rendering performance: UI Thread

如果Record View#draw需要很长时间,则通常是在UI线程上绘制位图的情况。 绘制位图需要使用CPU渲染,一般应该避免在主线程中绘制。 您可以使用Android CPU分析器的方法跟踪来查看这是否是问题。

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

android 动画 setRepeatCount android 动画卡顿_UI_06


如果这是您在UI线程上所做的工作,则可以在后台的解码线程上执行此操作。 在这样的一些情况下,你甚至可以在绘制时做这个工作,所以如果你的Drawable或View代码看起来像这样:

android 动画 setRepeatCount android 动画卡顿_ui_07

可以将上面代码优化为如下:

android 动画 setRepeatCount android 动画卡顿_应用程序_08

请注意,这通常也可以用于后台保护(在位图顶部绘制渐变)和图像过滤(使用ColorMatrixColorFilter),以及修改位图的其他两种常见操作。

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

Rendering performance: RenderThread

一些canvas操作是便小的消耗,但触发RenderThread昂贵的计算。 Systrace通常会通知这些。

Canvas.saveLayer()

避免Canvas.saveLayer() - 它可以触发昂贵的,未缓存的,离屏渲染每一帧。 尽管Android 6.0的性能得到了提高(当进行优化以避免GPU上的渲染目标切换时),但是如果可能的话,避免使用这个昂贵的API仍然是好事,或者至少确保您通过CLIP_TO_LAYER_SAVE_FLAG(或者调用一个变体 不带标志)。

Animating large Paths

当硬件加速Canvas传递给Views时,Canvas.drawPath()被调用,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果路径较大,请避免逐帧编辑,以便高速缓存和绘制。 drawPoints(),drawLines()和drawRect / Circle / Oval / RoundRect()更有效率 - 即使最终使用更多的绘制调用,最好使用它们。

Canvas.clipPath

clipPath(Path)触发了昂贵的裁剪行为,通常应该避免。 如果可能,选择绘制形状,而不是剪裁到非矩形。 它性能更好,支持抗锯齿。 例如,下面的clipPath调用:

android 动画 setRepeatCount android 动画卡顿_UI_09

Bitmap uploads

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

android 动画 setRepeatCount android 动画卡顿_UI_10

如果这些花费很长时间,请首先检查轨迹中的宽度和高度数字。确保正在显示的位图不比显示的屏幕区域大得多。如果是,则浪费上传时间和内存。通常位图加载库提供了简单的方法来请求适当大小的位图。

Android 7.0中,位图加载代码(通常由库完成)可以在需要之前调用prepareToDraw()来及早触发上传。这样上传发生的早,而RenderThread空闲。这可以在解码之后完成,也可以在将位图绑定到View时进行,只要知道位图即可。理想情况下,你的位图加载库会为你做这个,但是如果你正在管理你自己的,或者想确保你没有在新设备上点击上传,你可以在你自己的代码中调用prepareToDraw()

Thread scheduling delays

线程调度程序是Android操作系统的一部分,负责决定系统中哪些线程应该运行,何时运行以及运行多长时间。 有时候,因为你的应用程序的UI线程被阻塞或者没有运行,就会发生JankSystrace使用不同的颜色来指示线程正在Sleep(灰色)Runnable(蓝色:可以运行,但调度程序还没有选择它运行)正在运行(绿色)中断(红色或橙色)。 这对于调试线程调度延迟导致的Jank`问题非常有用。

注意:
旧版本的Android更频繁地遇到不是应用程序故障的调度问题。 在这方面进行了不断的改进,所以考虑在最近的操作系统版本上更多的调试线程调度问题,在这些版本中,被调度的线程更可能是应用程序的错误。

android 动画 setRepeatCount android 动画卡顿_人工智能_11

UI线程RenderThread预计不会运行时,有框架的一部分。 例如,UI线程RenderThreadsyncFrameState正在运行并且上传位图时被阻塞 - 这是因为RenderThread可以安全地复制UI线程所使用的数据。 另一个例子是,RenderThread在使用IPC时可以被阻塞:在帧的开始处获取缓冲区,从中查询信息,或者通过eglSwapBuffers将缓冲区传回给合成器。

在您的应用程序的执行中经常会有很长时间的暂停,这些都是由Android上的进程间通信(IPC)机制进行的。 在最近的Android版本中,这是UI线程停止运行的最常见原因之一。 一般来说,修正是为了避免调用函数来调用binder; 如果这是不可避免的,那么应该缓存该值,或将工作移动到后台线程。 随着代码库变得越来越大,如果不小心的话,通过调用一些低级别的方法,很容易意外地添加了一个binder调用,但是使用跟踪来发现和修复它们也是很容易的。

如果您有绑定事务,则可以使用以下adb命令来捕获其调用堆栈:

android 动画 setRepeatCount android 动画卡顿_ui_12

有时像getRefreshRate()这样的无害的表面调用可能会触发绑定事务,并在频繁调用时导致严重的问题。 定期跟踪可以帮助您快速找到并解决这些问题。

android 动画 setRepeatCount android 动画卡顿_人工智能_13

如果你没有看到绑定Activity,但仍然没有看到你的UI线程运行,请确保你没有等待来自另一个线程的锁定或其他操作。 通常,UI线程不应该等待来自其他线程的结果 - 其他线程应该向其发布信息post message.

Object allocation and garbage collection

对象分配和垃圾回收(GC)已经成为一个问题,因为ARTAndroid 5.0中默认运行时引入的,但是仍然有可能通过这些额外的工作来减轻你的线程负担。 对于每秒钟不会发生多次的罕见事件(如用户单击按钮)进行分配是很好的做法,但要记住,每次分配都需要付出一定的代价。 如果它处于一个频繁调用的紧密循环中,请考虑避免分配来减轻GC上的负载。

Systrace会告诉你GC是否频繁运行,Android Memory Profiler可以显示你的分配来自哪里。 如果你可以避免分配,特别是在紧密的循环中,你应该没有问题。

android 动画 setRepeatCount android 动画卡顿_ui_14

在最新版本的Android上,GC通常在名为HeapTaskDaemon的后台线程上运行。 请注意,大量的分配可能意味着更多的CPU资源花费在GC上.

至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!