(两百四十九)Android 学习性能与功耗(二)

继续学习https://developer.android.google.cn/topic/performance/vitals/crash

 

崩溃

未处理的异常或信号导致的意外退出,会使 Android 应用崩溃。使用 Java 编写的应用会在抛出未处理的异常(由 Throwable 类表示)时崩溃。使用原生代码语言编写的应用,会在执行过程中遇到未处理的信号(如 SIGSEGV)时崩溃。

当应用崩溃时,Android 会终止应用的进程并显示一个对话框,告知用户应用已停止,如图 1 所示。

(两百四十九)Android 学习性能与功耗(二)_第1张图片

图 1 Android 设备上崩溃的应用

应用并不是只有在前台运行时才会崩溃。任何应用组件,甚至是广播接收器或内容提供程序这类在后台运行的组件,都有可能导致应用崩溃。这些崩溃常常让用户感到困惑,因为他们并没有在使用应用。

 

诊断崩溃

解决崩溃问题可能很困难。但是,如果您能确定崩溃的根本原因,就有很可能能够找到解决方法。

有很多情况会导致应用崩溃。有些原因很明显,例如检查 null 值或空字符串,而其他一些原因则不太容易察觉,例如向 API 传递了无效的参数,或者是复杂的多线程互动。

读取堆栈轨迹

解决崩溃的第一步是确定它发生的位置。如果您使用的是 Play 管理中心或 logcat 工具的输出,则可使用报告详细信息中提供的堆栈轨迹。如果您没有堆栈轨迹可用,则应通过手动测试应用或联系受影响的用户在本地再现崩溃,并且要在使用 logcat 的情况下再现。

以下轨迹显示了示例应用上的一个崩溃示例:

--------- beginning of crash
    AndroidRuntime: FATAL EXCEPTION: main
    Process: com.android.developer.crashsample, PID: 3686
    java.lang.NullPointerException: crash sample
    at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
    at android.view.View.performClick(View.java:6134)
    at android.view.View$PerformClick.run(View.java:23965)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:156)
    at android.app.ActivityThread.main(ActivityThread.java:6440)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
    --------- beginning of system
    

堆栈轨迹显示对调试崩溃至关重要的两条信息:

  • 抛出的异常的类型。
  • 抛出异常的代码段。

抛出的异常的类型通常是关于问题原因的非常重要的提示。查看是否为 IOExceptionOutOfMemoryError 或其他问题,并找到有关 exception 类的文档。

抛出异常的源文件的行号显示在堆栈轨迹的第二行。对于调用的每个函数,会有另一行显示前面的调用点(称为堆栈帧)。通过遍历堆栈并检查代码,您可能会发现传递错误值的位置。如果您的代码没有出现在堆栈轨迹中,则表示您可能在某个地方将无效参数传递给了异步操作。您通常可以通过以下方式确定发生了什么问题:检查堆栈轨迹的每一行,查找所使用的任何 API 类,确认传递的参数是否正确,以及是否从允许的位置调用了参数。

 

有关再现崩溃的建议

再现问题可能无法简单地通过启动模拟器或将设备连接到计算机来实现。开发环境往往具有更多的资源,比如带宽、内存和存储。通过异常类型确定哪项资源不足,或找到 Android 版本、设备类型或应用版本之间的关联。

内存错误

如果发生了 OutOfMemoryError 异常,您可以首先创建一个内存容量较低的模拟器。图 2 显示了 AVD 管理器设置,您可以通过这些设置控制设备的内存容量。

(两百四十九)Android 学习性能与功耗(二)_第2张图片

图 2. AVD 管理器上的内存设置

网络异常

由于用户经常进出移动或 Wi-Fi 网络覆盖范围,在应用中,网络异常通常不应被视为错误,而应被视为偶尔会出现的正常运行环境。

如果您需要再现网络异常(比如 UnknownHostException),请在您的应用尝试使用网络时打开飞行模式。

此外,也可以通过选择网速模拟和/或网络延迟,降低模拟器中的网络质量。您可以使用 AVD 管理器上的 SpeedLatency 设置,也可以启动带有 -netdelay-netspeed 标记的模拟器,如以下命令行示例中所示:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm
    

本例为所有网络请求设置了 20 秒的延迟,并且将上传和下载速度设置为 14.4 Kbps。要详细了解模拟器的命令行选项,请参阅从命令行启动模拟器。

使用 logcat 读取

在您找到再现崩溃的方法后,可以使用 logcat 等工具获取更多信息。

logcat 输出将显示您输出的其他日志消息,以及系统输出的日志消息。请记住关闭您添加的任何额外的 Log 语句,因为在应用运行时输出这些日志会浪费 CPU 和电池电量。

 

呈现速度缓慢

界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与您的应用互动,您的应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度(为什么是 60fps?)。如果您的应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉您的应用不流畅。我们将这种情况称为卡顿

注意:Android Vitals 信息中心和 Android 系统会为使用界面工具包的应用(系统会根据 CanvasView 层次结构绘制应用的用户可见部分)记录呈现时间统计信息。如果您的应用未使用界面工具包(使用 Vulkan、Unity、Unreal 或 OpenGL 构建的应用就是这种情况),Android Vitals 信息中心将不会提供呈现时间统计信息。要确定您的设备是否为您的应用记录呈现时间指标,您可以运行 adb shell dumpsys gfxinfo

raphael:/ $ dumpsys gfxinfo com.example.flutterlayouttest
Applications Graphics Acceleration Info:
Uptime: 385526435 Realtime: 1083207520

** Graphics info for pid 10130 [com.example.flutterlayouttest] **

Stats since: 385457331272542ns
Total frames rendered: 35
Janky frames: 1 (2.86%)
50th percentile: 5ms
90th percentile: 5ms
95th percentile: 5ms
99th percentile: 109ms
Number Missed Vsync: 0
Number High input latency: 32
Number Slow UI thread: 1
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 0
Number Frame deadline missed: 1
HISTOGRAM: 5ms=34 6ms=0 7ms=0 8ms=0 9ms=0 10ms=0 11ms=0 12ms=0 13ms=0 14ms=0 15ms=0 16ms=0 17ms=0 18ms=0 19ms=0 20ms=0 21ms=0 22ms=0 23ms=0 24ms=0 25ms=0 26ms=0 27ms=0 28ms=0 29ms=0 30ms=0 31ms=0 32ms=0 34ms=0 36ms=0 38ms=0 40ms=0 42ms=0 44ms=0 46ms=0 48ms=0 53ms=0 57ms=0 61ms=0 65ms=0 69ms=0 73ms=0 77ms=0 81ms=0 85ms=0 89ms=0 93ms=0 97ms=0 101ms=0 105ms=0 109ms=1 113ms=0 117ms=0 121ms=0 125ms=0 129ms=0 133ms=0 150ms=0 200ms=0 250ms=0 300ms=0 350ms=0 400ms=0 450ms=0 500ms=0 550ms=0 600ms=0 650ms=0 700ms=0 750ms=0 800ms=0 850ms=0 900ms=0 950ms=0 1000ms=0 1050ms=0 1100ms=0 1150ms=0 1200ms=0 1250ms=0 1300ms=0 1350ms=0 1400ms=0 1450ms=0 1500ms=0 1550ms=0 1600ms=0 1650ms=0 1700ms=0 1750ms=0 1800ms=0 1850ms=0 1900ms=0 1950ms=0 2000ms=0 2050ms=0 2100ms=0 2150ms=0 2200ms=0 2250ms=0 2300ms=0 2350ms=0 2400ms=0 2450ms=0 2500ms=0 2550ms=0 2600ms=0 2650ms=0 2700ms=0 2750ms=0 2800ms=0 2850ms=0 2900ms=0 2950ms=0 3000ms=0 3050ms=0 3100ms=0 3150ms=0 3200ms=0 3250ms=0 3300ms=0 3350ms=0 3400ms=0 3450ms=0 3500ms=0 3550ms=0 3600ms=0 3650ms=0 3700ms=0 3750ms=0 3800ms=0 3850ms=0 3900ms=0 3950ms=0 4000ms=0 4050ms=0 4100ms=0 4150ms=0 4200ms=0 4250ms=0 4300ms=0 4350ms=0 4400ms=0 4450ms=0 4500ms=0 4550ms=0 4600ms=0 4650ms=0 4700ms=0 4750ms=0 4800ms=0 4850ms=0 4900ms=0 4950ms=0
Font Cache (CPU):
  Size: 0.32 kB
  Glyph Count: 1
CPU Caches:
GPU Caches:
  Other:
    Other: 0.00 bytes (1 entry)
  Scratch:
    Buffer Object: 48.00 KB (1 entry)
Other Caches:
                         Current / Maximum
  VectorDrawableAtlas    0.00 kB /   0.00 KB (entries = 0)
  Layers Total           0.00 KB (numLayers = 0)
Total GPU memory usage:
  49152 bytes, 48.00 KB (0.00 bytes is purgeable)


Pipeline=Skia (OpenGL)

Layout Cache Info:
  Usage: 1/5000 entries
  Hit ratio: 0/1 (0.000000)
Profile data in ms:

        com.example.flutterlayouttest/com.example.flutterlayouttest.MainActivity/android.view.ViewRootImpl@355459f (visibility=8)
View hierarchy:

  com.example.flutterlayouttest/com.example.flutterlayouttest.MainActivity/android.view.ViewRootImpl@355459f
  8 views, 8.50 kB of display lists


Total ViewRootImpl: 1
Total Views:        8
Total DisplayList:  8.50 kB

 

识别卡顿

在您的应用中找出导致卡顿的代码可能并非易事。本部分介绍了三种识别卡顿的方法:

  • 目视检查
  • Systrace
  • 自定义性能监控

通过目视检查,您可以在几分钟内快速查看应用中的所有用例,但通过这种方法获得的信息不如使用 Systrace 方法时获得的信息详细。Systrace 能够提供更多详细信息,但如果您针对应用中的所有用例运行 Systrace,则会被太多数据淹没,导致难以进行分析。目视检查和 Systrace 都是在您的本地设备上检测卡顿。如果无法在本地设备上重现卡顿,您可以构建自定义性能监控功能,在现场运行的设备上评测应用的特定部分。

 

目视检查方法

目视检查有助于您找出导致卡顿的用例。要进行目视检查,请打开您的应用并手动查看应用的不同部分,看看是否有卡顿的界面。以下是关于进行目视检查的一些提示:

  • 运行应用的发布版本(或至少是不可调试的版本)。为了支持调试功能,ART 运行时会停用几项重要的优化功能,因此请务必确保您看到的内容与用户将会看到的内容类似。
  • 启用 GPU 呈现模式分析功能。GPU 呈现模式分析功能会在屏幕上显示一些条形,以相对于每帧 16ms 的基准,快速直观地显示呈现界面窗口帧所花的时间。每个条形都有带颜色的区段,对应于呈现管道中的一个阶段,这样您就可以看到哪个部分用时最长。例如,如果帧花费大量时间处理输入,您应查看负责处理用户输入的应用代码。
  • 某些组件(如 RecyclerView)是卡顿的常见来源。如果您的应用使用了这些组件,您最好查看一下应用的这些部分。
  • 有时,只有当应用通过冷启动进行启动时,才能重现卡顿。
  • 您可以尝试在速度较慢的设备上运行您的应用,以突显此问题。

在发现导致卡顿的用例后,您可能已经很清楚应用中导致卡顿的原因是什么。但如果您需要更多信息,可以使用 Systrace 进一步深入分析。

 

gpu呈现模式分析功能打开方法

Enable the profiler

Before you begin, make sure you're using a device running Android 4.1 (API level16) or higher, and you enable developer options. To start profiling device GPU rendering while using your app, proceed as follows:

  1. On your device, go to Settings and tap Developer Options.
  2. In the Monitoring section, select Profile GPU Rendering.
  3. In the Profile GPU Rendering dialog, choose On screen as bars to overlay the graphs on the screen of your device.
  4. Open the app that you want to profile.

效果

(两百四十九)Android 学习性能与功耗(二)_第3张图片

 

Systrace 方法

Systrace 工具用于显示整个设备在做些什么,不过也可用于识别应用中的卡顿。Systrace 的系统开销非常小,因此您可以在插桩测试期间体验实际卡顿情况。

在设备上执行卡顿的用例时,可以使用 Systrace 记录跟踪信息。有关如何使用 Systrace 的说明,请参阅 Systrace 演示。系统跟踪信息会按进程和线程进行细分。您可以在 Systrace 中查看应用的进程,该进程应如图 1 所示。

(两百四十九)Android 学习性能与功耗(二)_第4张图片图 1:系统跟踪信息

图 1 中的系统跟踪信息包含以下用于识别卡顿的信息:

  1. 系统跟踪信息会显示每帧的绘制时间,并对每帧进行颜色编码以突出显示呈现速度缓慢的时间。与目视检查相比,这种方法有助于您更准确地找出各个卡顿的帧。如需了解详情,请参阅检查帧。
  2. 系统跟踪信息会检测您应用中的问题,并在各个帧和提醒面板中同时显示提醒。您最好遵循提醒中的指示。
  3. Android 框架和库的某些部分(如 RecyclerView)包含跟踪标记。因此,系统跟踪信息时间轴会显示在界面线程上执行这些方法的时间以及时长。

查看系统跟踪信息输出后,您可能会怀疑应用中的某些方法是导致卡顿的因素。例如,如果时间轴显示某个帧的呈现速度较慢是因为 RecyclerView 花费很长时间导致的,您可以在相关代码中添加跟踪标记,然后重新运行 systrace 以获取更多信息。在新的系统跟踪信息中,时间轴会显示应用中的方法的调用时间和执行时长。

如果系统跟踪信息未显示关于界面线程工作为何用时较长的详细信息,那么您需要使用 Android CPU Profiler 来记录采样或插桩测试的方法跟踪信息。通常情况下,方法跟踪信息不适合用于识别卡顿,因为它们会因开销过大而导致出现假正例卡顿,且无法查看线程何时运行以及何时处于阻塞状态。不过,方法跟踪信息可以帮助您找出应用中用时最多的方法。找出这些方法后,您可以添加跟踪标记并重新运行 systrace 以查看这些方法是否会导致卡顿。

注意:记录系统跟踪信息时,每个跟踪标记(执行的开始和结束对)会增加大约 10μs 的开销。为了避免出现假正例卡顿,对于在一帧中会被调用数十次或用时少于 200us 的方法,请勿为其添加跟踪标记。

如需了解详情,请参阅了解 Systrace。

 

解决卡顿问题

要解决卡顿问题,请检查哪些帧的用时超过了 16.7ms,并查看哪里出了问题。Record View#draw 在某些帧中是否用时过长,或者可能是布局问题?关于这些问题及其他问题,请参阅下面的常见的卡顿来源。

为了避免卡顿,长时间运行的任务应在界面线程之外异步运行。务必要始终清楚您的代码在什么线程上运行,并且在向主线程派发重要任务时要谨慎。

如果您的应用具有非常复杂且非常重要的主界面(可能是中央滚动列表),请考虑编写插桩测试以自动检测呈现速度缓慢的时间,并频繁运行这些测试来防止出现回归。如需了解详情,请参阅自动化性能测试 Codelab。

 

常见的卡顿来源

以下部分介绍了应用中常见的卡顿来源以及解决这些问题的最佳做法。

可滚动列表

ListViewRecyclerView (尤其是后者)常用于最易出现卡顿的复杂滚动列表。它们都包含 Systrace 标记,因此您可以使用 Systrace 来判断它们是不是导致应用出现卡顿的因素。请务必传递命令行参数 -a ,以便让 RecyclerView 中的跟踪部分(以及您添加的所有跟踪标记)显示出来。请遵循系统跟踪信息输出中生成的提醒提供的指导(如果有)。在 Systrace 中,您可以点击 RecyclerView 跟踪部分,以查看关于 RecyclerView 正在执行的工作的说明。

RecyclerView:notifyDataSetChanged

如果您在一个帧中看到 RecyclerView 中的每一项都重新绑定(并因此重新布局和重新绘制),请确保您没有调用 notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter, boolean) 来进行细微更新。这些方法会向系统表明整个列表内容已更改,并会在 Systrace 中显示为 RV FullInvalidate。应改用 SortedListDiffUtil,以便在内容发生更改或添加了内容时生成最少量的更新。

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

KotlinJava

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

但这有一个很大的缺点 – 如果是微不足道的更改(可能是单项内容添加到顶部),RecyclerView 将无法检测到这种情况 – 它被告知放弃所有缓存的内容状态,因此需要重新绑定每一项。

使用 DiffUtil 效果会好很多,它会为您计算和派发最少的更新。

KotlinJava

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

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

 

RecyclerView:嵌套的 RecyclerView

嵌套 RecyclerView 很常见,对于由水平滚动列表组成的纵向列表(例如 Play 商店主页面上的应用网格),尤其如此。这种方法效果很好,但它也会导致大量来回移动的视图。在首次向下滚动页面时,如果您看到大量内部内容出现扩充,则可能需要检查内部(水平)RecyclerView 之间是否正在共享 RecyclerView.RecycledViewPool。默认情况下,每个 RecyclerView 都将有自己的内容池。然而,在屏幕上同时显示十几个 itemViews 的情况下,如果所有行都显示类型相似的视图,那么当不同的水平列表无法共享 itemViews 时,就会出现问题。

KotlinJava

    class OuterAdapter extends RecyclerView.Adapter {
        RecyclerView.RecycledViewPool sharedPool = 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(sharedPool);
            return new OuterAdapter.ViewHolder(innerRv);

        }
        ...
    

如果您希望进一步优化,还可以对内部 RecyclerView 的 LinearLayoutManager 调用setInitialPrefetchItemCount(int)。例如,如果您始终在某行中显示 3.5 项内容,请调用 innerLLM.setInitialItemPrefetchCount(4);。这将向 RecyclerView 表明,当某个水平行即将显示在屏幕上时,如果界面线程中有空余时间,RecyclerView 应尝试预取该行中的内容。

 

RecyclerView:扩充过多/创建过程用时太长

RecyclerView 中的预取功能会在界面线程处于空闲状态时提前执行工作,因此在大多数情况下应该有助于解决扩充造成的开销问题。如果您在帧中(而不是标记为 RV 预取的部分中)看到了扩充,请确保您是在版本较新的设备上进行测试(预取功能当前仅在 Android 5.0 API 级别 21 及更高版本上受支持),并且使用的是较新版本的支持库。

如果您经常在屏幕上出现新内容时看到导致卡顿的扩充问题,请确认您的视图类型数量没有超出所需要的数量。RecyclerView 内容中的视图类型越少,屏幕上出现新的内容类型时需要进行的扩充就越少。如果可能的话,可以在适当情况下合并视图类型 – 如果不同类型之间只有图标、颜色或文本片段不同,您可以在绑定时进行这些更改,从而避免扩充(同时减少应用占用的内存)。

如果视图类型看起来合适,请考虑降低扩充导致的开销。减少不必要的容器和结构视图会有所帮助 – 请考虑使用 ConstraintLayout 构建 itemViews,以便轻松减少结构视图。如果您希望真正进行优化以提升性能,内容的层次结构非常简单,并且您不需要复杂的主题和样式功能,可以考虑自己调用构造函数,但是请注意,通常不值得为此牺牲 XML 的简易性和功能。

RecyclerView:绑定用时过长

绑定(即 onBindViewHolder(VH, int))应该非常简单,并且所有内容(最复杂的内容除外)所需的绑定时间都应远远少于 1 毫秒。它应该只从适配器的内部内容数据获取 POJO 内容,并对 ViewHolder 中的视图调用 setter。如果 RV OnBindView 用时很长,请确认在绑定代码中只执行少量的工作。

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

RecyclerView 或 ListView:布局 / 绘制用时过长

关于绘制和布局方面的问题,请参阅有关布局和呈现性能的部分。

ListView:扩充

如果不够谨慎,很容易在 ListView 中意外停用回收功能。如果每次有新内容显示到屏幕上时您都会看到扩充,请检查您的 Adapter.getView() 实现是否正在使用、重新绑定并返回 convertView 参数。如果您的 getView() 实现始终会扩充,您的应用将无法在 ListView 中享受到回收的好处。getView() 的结构应该几乎总是与下面的实现类似:

KotlinJava

    View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {
            // only inflate if no convertView passed
            convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
        }
        // … bind content from position to convertView …
        return convertView;
    }
    

 

布局性能

如果 Systrace 表明 Choreographer#doFrame 的布局部分执行的工作过多或者执行工作的频率太高,则意味着您遇到了布局性能问题。应用的布局性能取决于视图层次结构的哪个部分包含会发生改变的布局参数或输入。

布局性能:开销

如果这些部分的用时超过几毫秒,您可能遇到了对 RelativeLayouts 或 weighted-LinearLayouts 来说最糟糕的嵌套性能。这些布局中的每一个都可以触发其子级的多次评测/布局传递,因此嵌套这些布局可能会导致嵌套深度方面出现 O(n^2) 行为。请尝试在层次结构的所有叶节点(最低叶节点除外)中避免使用 RelativeLayout,或避免使用 LinearLayout 的权重功能。您可以采用以下几种方法:

  • 您可以调整结构视图的组织方式。
  • 您可以定义自定义布局逻辑。请参阅优化您的布局指南,查看具体示例。
  • 您可以尝试转换为 ConstraintLayout,该布局提供类似的功能,但不存在性能缺陷。

https://developer.android.google.cn/training/improving-layouts

改善布局性能

布局是 Android 应用中直接影响用户体验的关键部分。如果实现不当,您的布局可能会导致应用界面缓慢且需要占用大量内存。Android SDK 包含一些工具,有助于您识别布局性能方面的问题,将这些工具与这里的课程相结合,您将能够以尽可能少的内存占用量实现流畅的滚动界面。

课程

优化布局层次结构

同样,复杂的网页会延长加载时间,如果布局层次结构过于复杂,也会导致性能问题。本课介绍了如何使用 SDK 工具检查布局并发现性能瓶颈。

通过 重复使用布局

如果您的应用界面在多个位置重复某些布局结构,则可以通过本课了解如何创建高效、可重复使用的布局结构,然后将它们添加到相应的界面布局中。

按需加载视图

除了将一个布局组件简单地添加到另一个布局之外,您可能希望仅在需要时(有时是在 Activity 运行后)显示所包含的布局。本课介绍了如何通过按需加载部分布局来改善布局的初始化性能。

布局性能:频率

屏幕上出现新内容时,例如当新内容滚动到到 RecyclerView 中的视图上时,应该会进行布局。如果每帧都进行明显布局,则可能是在为布局呈现动画效果,这很可能会导致丢帧。一般来说,动画应以 View 的绘制属性(例如 setTranslationX/Y/Z()setRotation()setAlpha() 等等)运行。与布局属性(例如,内边距或外边距)相比,这些属性的更改开销要低得多。更改视图的绘制属性的开销也低得多,通常是调用会触发 invalidate() 的 setter,后跟下一帧中的 draw(Canvas)。这会重新记录已失效视图的绘制操作,并且开销通常也比布局低得多。

呈现性能

Android 界面工作分为两个阶段:界面线程上的 Record View#draw 和 RenderThread 上的 DrawFrame。第一阶段对每个失效的 View 运行draw(Canvas),并可调用自定义视图或代码。第二阶段在原生 RenderThread 上运行,但将根据 Record View#draw 阶段生成的工作运行。

呈现性能:界面线程

如果 Record View#draw 需要很长时间,通常情况下会在界面线程上绘制位图。绘制到位图时使用的是 CPU 呈现,因此通常应尽量避免此操作。结合使用方法跟踪功能和 Android CPU Profiler,看看这是否会带来问题。

当应用希望在显示位图之前对其进行装饰时,通常会执行绘制到位图这一操作。装饰有时候是指像添加圆角这样的操作:

 

 

KotlinJava

    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
    

如果您正在界面线程上执行此类工作,则可以转到后台的解码线程上执行。在某些类似的情况下,您甚至可以在绘制时执行该工作,因此,如果您的 DrawableView 代码如下所示:

KotlinJava

    void setBitmap(Bitmap bitmap) {
        mBitmap = bitmap;
        invalidate();
    }

    void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, null, paint);
    }
    

您可以将其替换为以下代码:

KotlinJava

    void setBitmap(Bitmap bitmap) {
        shaderPaint.setShader(
                new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
        invalidate();
    }

    void onDraw(Canvas canvas) {
        canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
    }
    

请注意,这种操作通常也可以用于背景保护(在位图前绘制渐变)和图片过滤(使用 ColorMatrixColorFilter),这是用于修改位图的另外两种常见操作。

如果要出于其他原因而绘制到位图(可能是将其用作缓存),请尝试直接绘制到传递至视图或 Drawable 的硬件加速画布;如果需要,请考虑调用带有 LAYER_TYPE_HARDWAREsetLayerType() 来缓存复杂的呈现输出,并仍然充分利用 GPU 呈现功能。

 

呈现性能:RenderThread

有些画布操作虽然记录开销很低,但会在 RenderThread 上触发开销非常大的计算。Systrace 通常会通过提醒来调用这些操作。

Canvas.saveLayer()

避免 Canvas.saveLayer() – 它可能会触发以开销非常大且未缓存的屏幕外方式呈现每帧。虽然 Android 6.0 中的性能得到了提升(进行了优化以避免 GPU 上的呈现目标切换),但仍然最好尽可能避免使用这个开销非常大的 API,或者至少确保传递 Canvas.CLIP_TO_LAYER_SAVE_FLAG(或调用不带标记的变体)。

为大型路径添加动画效果

对传递至视图的硬件加速画布调用 Canvas.drawPath() 时,Android 会首先在 CPU 上绘制这些路径,然后将它们上传到 GPU。如果路径较大,请避免逐帧修改,以便高效地对其进行缓存和绘制。drawPoints()drawLines()drawRect/Circle/Oval/RoundRect() 的效率更高 – 即使您最终使用了更多绘制调用,也最好使用它们。

Canvas.clipPath

clipPath(Path) 会触发开销非常大的裁剪行为,因此通常应避免使用它。如果可能,请选择使用绘制形状,而不是裁剪为非矩形。它的效果更好,并支持抗锯齿功能。例如,以下 clipPath 调用:

KotlinJava

  canvas.save();
    canvas.clipPath(circlePath);
    canvas.drawBitmap(bitmap, 0f, 0f, paint);
    canvas.restore();
    

可改为表示为:

KotlinJava

    // one time init:
    paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    // at draw time:
    canvas.drawPath(circlePath, mPaint);
    

位图上传

Android 会将位图显示为 OpenGL 纹理,并且当位图第一次显示在帧中时,它会上传到 GPU。您可以在 Systrace 中看到此操作显示为上传“宽 x 高”纹理。这可能需要几毫秒的时间(见图 2),但必须使用 GPU 显示图片。

如果这些操作用时较长,请首先检查跟踪信息中的宽度和高度数据。请确保显示的位图不会明显大于其在屏幕上的显示区域,否则会浪费上传时间和内存。通常,位图加载库会提供一些简易的方法来请求大小适当的位图。

在 Android 7.0 中,位图加载代码(通常由库完成)可以调用 prepareToDraw(),以便在需要用到它之前便触发上传。这样,上传操作会在 RenderThread 处于空闲状态时提前进行。只要您知道位图,就可以在解码之后或将位图绑定到视图时执行此操作。理想情况下,您的位图加载库会为您执行此操作,但如果您要自行管理,或者想要确保在更高版本的设备上不会触发上传,则可以在自己的代码中调用 prepareToDraw()

(两百四十九)Android 学习性能与功耗(二)_第5张图片

图 2:应用在某帧中花费超过 10ms 的时间上传一张 180 万像素的位图。可以缩减其大小,也可以在使用 prepareToDraw() 进行解码时提前触发上传。

 

线程调度延迟

线程调度程序在 Android 操作系统中负责确定系统中的哪些线程应该运行、何时运行以及运行多长时间。有时,出现卡顿是因为应用的界面线程处于阻塞或未运行状态。Systrace 使用不同的颜色(见图 3)来指明线程何时处于休眠状态(灰色)、可运行(蓝色:可以运行,但调度程序尚未选择让它运行)、正在运行(绿色)或处于不可中断休眠状态(红色或橙色)。这对于调试由线程调度延迟引起的卡顿问题非常有用。

注意:更低版本的 Android 会更频繁地遇到不是应用错误导致的调度问题。目前这一方面得到了持续改进,因此请考虑更多地在较新的操作系统版本上调试线程调度问题,因为在这些版本上,未调度的线程更有可能是应用错误导致的。

(两百四十九)Android 学习性能与功耗(二)_第6张图片

图 3:突出显示界面线程处于休眠状态的时间段。

注意:对于帧的某些部分,界面线程或 RenderThread 预计不会运行。例如,在 RenderThread 的 syncFrameState 正在运行并且位图已上传时,界面线程会处于阻塞状态,这是为了 RenderThread 可以安全地复制界面线程使用的数据。另一个例子是,RenderThread 在使用 IPC 执行下述操作时可能会处于阻塞状态:在帧的开头获取缓冲区,从中查询信息,或者通过 eglSwapBuffers 将缓冲区信息传回给合成器。

应用执行过程中的长时间停顿通常是由 binder 调用(Android 上的进程间通信 (IPC) 机制)引起的。在较新的 Android 版本中,这是导致界面线程停止运行的最常见原因之一。一般来说,解决方法是避免调用进行 binder 调用的函数;如果不可避免,则应该缓存相应值,或将工作转移到后台线程。随着代码库变得越来越大,当您调用一些低级别方法时,很容易会因为不小心而意外添加 binder 调用,但同样很容易通过跟踪找到并修复它们。

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

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 事务,如果频繁调用这些事务,还会引发严重问题。定期进行跟踪有助于您在这些问题出现时快速发现并解决它们。

(两百四十九)Android 学习性能与功耗(二)_第7张图片

图 4:显示由于 RV 投掷中的 binder 事务而导致的界面线程休眠。让绑定逻辑保持简单,并使用 trace-ipc 跟踪和移除 binder 调用。

如果您没有看到 binder 活动,但也未看到界面线程运行,请确保您未在等待来自其他线程的某项锁定或其他操作。通常,界面线程应该不需要等待来自其他线程的结果 – 其他线程应向其发布信息。

 

对象分配和垃圾收集

自从 ART 在 Android 5.0 中作为默认运行时引入后,对象分配和垃圾回收 (GC) 问题已显著缓解,但这项额外的工作仍有可能加重线程的负担。您可以针对每秒不会发生多次的罕见事件(例如用户点按一个按钮)进行分配,但请记住,每次分配都会产生开销。如果它处于被频繁调用的紧密循环中,请考虑避免分配以减轻 GC 上的负载。

Systrace 会显示 GC 是否频繁运行,而 Android Memory Profiler 可显示分配来源。如果尽可能避免分配(尤其是在紧密循环中),则应该不会遇到问题。

(两百四十九)Android 学习性能与功耗(二)_第8张图片

图 5:显示 HeapTaskDaemon 线程上的 94ms GC

在较新版本的 Android 中,GC 通常在名为 HeapTaskDaemon 的后台线程上运行。请注意,大量的分配可能意味着在 GC 上耗费更多的 CPU 资源,如图 5 所示。

 

冻结的帧

冻结的帧是呈现时间超过 700ms 的界面帧。这是一个问题,因为您的应用在帧的呈现过程中几乎有一秒钟的时间卡住,对用户输入无响应。我们通常建议应用在 16ms 内呈现帧,以确保界面流畅。但是,当您的应用启动或转换到其他屏幕时,初始帧的绘制时间通常会超过 16ms,这是因为您的应用必须扩充视图,对屏幕进行布局并从头开始执行初始绘制。因此,Android 将冻结的帧与呈现速度缓慢分开跟踪。您应用中的任何帧的呈现时间都不应超过 700ms。

为了帮助您提高应用质量,Android 会自动监控您的应用是否存在冻结的帧,并在 Android Vitals 信息中心显示相关信息。如需了解如何收集数据,请参阅 Play 管理中心文档。

冻结的帧是呈现速度缓慢的一种极端形式,因此诊断和解决问题的过程是相同的。如需了解如何诊断和修复呈现速度缓慢问题,请参阅呈现速度缓慢。

 

总结

主要学习了两部分知识,crash 和 卡顿

1.crash其实定位相对简单,adb logcat -b crash 或者log中找异常,然后根据堆栈找对于代码就好了

2.卡顿挺复杂的,需要对布局,系统绘制等有深入的了解。官方推荐了3个方法

  • 一个是目测,结合现场和代码进行问题的定位;
  • 另外是通过systrace进行定位,待进一步详细学习
  • 最后一种方法类似于systrace和leakcanary,叫做Firebase,应用需要添加依赖和代码

卡顿的解决依赖于卡顿原因的定位和相相对应的解决,主要原因有

  • 列表(listview和recycleView)
  • 布局
  • 显现性能(涉及到源码,需要研究)
  • 线程调度延迟
  • 对象分配和垃圾收集

 

待续:

弄懂下面这段话

Android 界面工作分为两个阶段:界面线程上的 Record View#draw 和 RenderThread 上的 DrawFrame。第一阶段对每个失效的 View 运行draw(Canvas),并可调用自定义视图或代码。第二阶段在原生 RenderThread 上运行,但将根据 Record View#draw 阶段生成的工作运行。

你可能感兴趣的:(Performance)