ANR的含义--如何防止你的安卓应用出现 "应用无响应 "的错误

302 阅读11分钟

如果有办法使我的应用程序 "应用程序无响应 "不出错,我会这样做。

无响应似乎总是在你意料之外的时候悄悄出现。而问题是,你对它们无能为力。

当你在Google Play Console中看到你的应用程序的概述时,你可能会看到它们弹出来。但那里不会有很多信息来了解ANR是如何发生的,以及你可以做什么来解决这个问题。

除此之外,如果用户碰巧遇到了ANR,你必须依靠他们愿意投入时间和精力来告诉你这个问题。我们也都曾站在另一边。如果你在使用一个应用程序时卡住了,你做的一件事就是直接去卸载它。

那么,作为开发者,我们怎样才能尽一切努力保护我们的应用程序的用户不遭遇ANR呢?

让我们来了解一下。

如何防止Android上的 "应用程序无响应 "错误?

积极主动

在我们深入研究可以帮助我们检测ANR的解决方案之前,让我们了解一下我们可以做些什么来避免一开始就出现ANR(或者尽量减少它们发生的机会)。

这些要点可能听起来很明显,但在一个足够大的应用中,可能很容易忽视一些事情。

首先,检查一下你的代码中是否有地方在UI线程上做大量工作。在UI线程上做的工作应该是简短的,并且与你的应用程序的UI有关。

如果你在那里做任何其他的逻辑,或者甚至是异步工作,请将其委托给一个后台线程或服务。

第二,如果你的线程持有锁或某些需要同步的代码块,确保你没有造成死锁或你的应用程序的某个状态达到死锁。

第三,如果你的应用程序与广播接收器打交道,你必须验证onReceive 方法的执行时间很短,并及时结束。如果那里有可能需要一些时间的工作,请把它委托给一个后台线程

另一种方法,你可以检测可能导致ANR的地方,就是使用 严格模式.你可以在开发你的应用程序时使用它,因为它可以捕捉到主线程上意外的磁盘或网络使用。

要聪明

你已经检查了你的应用程序,并且你认为它没有任何ANR的风险。所以你把它发布给公众使用。

瞧,几个月过去了,你开始看到ANRs的报告。你可以采取什么不同的做法?正如我们前面所讨论的,这些碰撞报告几乎没有提供任何关于ANR的信息。

为了帮助你的应用程序在发生ANR时给你提供最高水平的细节,我将详细介绍两个选项。

  1. 运行一个线程,该线程会轮询UI线程,看看它是否被卡住了
  2. 在API级别≥30的情况下,你可以使用 获取历史进程的退出原因

现在让我们来详细看看每一个选项。

运行一个轮询UI线程的线程

已经有一个叫做ANR-Watchdog的库,它负责检测ANR并为你提供所有的细节。如果你不想使用它或者想拥有自己的东西,这里有一个关于它所做工作的粗略轮廓。

  • 创建一个在主线程上运行的线程(它不需要做任何实际工作)
  • 看看这个线程的执行在几秒钟后是否完成了
  • 如果完成了,那么就没有发生ANR,你可以再次运行该线程
  • 如果没有,那么其他线程阻塞了主线程,导致了ANR。

下面是这样一个类的大致轮廓:

package com.tomerpacific.anrdetection

import android.os.Handler
import android.os.Looper
import java.lang.Exception

class ANRHandler: Thread() {

    val TIMEOUT: Long = 5000L
    private val handler: Handler = Handler(Looper.getMainLooper())
    private val worker : Runnable = Runnable {  }

    override fun run() {
        while (!isInterrupted) {
            handler.postAtFrontOfQueue(worker)

            try {
                sleep(TIMEOUT)
            } catch (exception: Exception) {
                exception.printStackTrace()
            }

            if (handler.hasMessages(0)) {
                //worker has not finished running so the UI thread is being held
                val stackTrace: Array<StackTraceElement> = currentThread().stackTrace
                var output: String = ""
                for (element in stackTrace) {
                    output += element.className + " " + element.methodName + " " + element.lineNumber
                }

                print(output)
            }
        }
    }
}

⚠️ runnable的执行总是在主线程上,但因为它不做任何工作,所以它不应该影响你的应用程序的性能。你也可以决定在每个需要的时间间隔内运行它。

使用getHistoricalProcessExitReasons

选项#2可以使你的生活更简单,因为它的API给你提供了很多信息。

getHistoricalProcessExitReasons在Android 11(API级别30)中引入,它的作用和你想象的一样。它返回一个记录对象的列表,说明最近的应用程序终止情况。

这个方法是在ActivityManager上调用的,接受三个参数:

  1. 包的名称--字符串类型(可以是空的)
  2. 属于该包的进程的ID--int类型
  3. 你想找回的最大数量的原因--int类型的

值得注意的是,所有这些参数都可以用默认值来代替。也就是说,你可以传递null作为包的名称,并获得调用者的UID的全部退出原因。

那么,这些记录的对象包含什么呢?嗯,这些对象是 ApplicationExitInfo 类型,它们可以为你提供很多有用的信息。

对于初学者,你可以调用getReason 方法来找出进程终止的原因。这个方法返回一个整数,标记退出原因的代码。如果返回的值是6,这意味着应用程序被终止了,因为它由于发生了ANR而没有反应。

通过GIPHY

这很整洁,但我们怎么能看到ANR发生的地方呢?为此,我们可以使用 getTraceInputStream.就像名字所暗示的那样,返回的值是一个字节的InputStream,需要像其他的InputStream一样被读取。

一个输出的例子看起来像下面这样:

I/System.out: ----- pid 2738 at 2022-04-26 17:48:12 -----
    Cmd line: com.tomerpacific.anrdetection
    Build fingerprint: 'Android/sdk_phone_x86/generic_x86:11/RSR1.210210.001.A1/7193139:userdebug/dev-keys'
    ABI: 'x86'
    Build type: optimized
I/System.out: Zygote loaded classes=15746 post zygote classes=728
    Dumping registered class loaders
    #0 dalvik.system.PathClassLoader: [], parent #1
    #1 java.lang.BootClassLoader: [], no parent
I/System.out: #2 dalvik.system.PathClassLoader: [/data/app/~~C_0mqw-g_9cjPjIR_kpRIg==/com.tomerpacific.anrdetection-3-t-I6JR9Q3QA6UY7L8iPA==/base.apk], parent #1
    Done dumping class loaders
    Classes initialized: 302 in 19.361ms
    Intern table: 31490 strong; 543 weak
    JNI: CheckJNI is on; globals=637 (plus 31 weak)
I/System.out: Libraries: libandroid.so libaudioeffect_jni.so libcompiler_rt.so libicu_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libopenjdk.so librs_jni.so libsfplugin_ccodec.so libsoundpool.so libstats_jni.so libwebviewchromium_loader.so (14)
    Heap: 91% free, 2330KB/26MB; 67022 objects
    Dumping cumulative Gc timings
I/System.out: Average major GC reclaim bytes ratio inf over 0 GC cycles
    Average major GC copied live bytes ratio 0.738176 over 4 major GCs
    Cumulative bytes moved 11482280
    Cumulative objects moved 217937
    Peak regions allocated 28 (7168KB) / 768 (192MB)
I/System.out: Start Dumping histograms for 1 iterations for young concurrent copying
    ProcessMarkStack:	Sum: 26.311ms 99% C.I. 26.311ms-26.311ms Avg: 26.311ms Max: 26.311ms
    ScanImmuneSpaces:	Sum: 5.625ms 99% C.I. 5.625ms-5.625ms Avg: 5.625ms Max: 5.625ms
    VisitConcurrentRoots:	Sum: 1.121ms 99% C.I. 1.121ms-1.121ms Avg: 1.121ms Max: 1.121ms
I/System.out: (Paused)ClearCards:	Sum: 375us 99% C.I. 7us-235us Avg: 28.846us Max: 235us
    GrayAllDirtyImmuneObjects:	Sum: 329us 99% C.I. 329us-329us Avg: 329us Max: 329us
    VisitNonThreadRoots:	Sum: 327us 99% C.I. 327us-327us Avg: 327us Max: 327us
I/System.out: InitializePhase:	Sum: 306us 99% C.I. 306us-306us Avg: 306us Max: 306us
    (Paused)GrayAllNewlyDirtyImmuneObjects:	Sum: 164us 99% C.I. 164us-164us Avg: 164us Max: 164us
    (Paused)FlipCallback:	Sum: 144us 99% C.I. 144us-144us Avg: 144us Max: 144us
    SweepSystemWeaks:	Sum: 142us 99% C.I. 142us-142us Avg: 142us Max: 142us
I/System.out: ScanCardsForSpace:	Sum: 125us 99% C.I. 125us-125us Avg: 125us Max: 125us
    ThreadListFlip:	Sum: 96us 99% C.I. 96us-96us Avg: 96us Max: 96us
    ClearFromSpace:	Sum: 78us 99% C.I. 78us-78us Avg: 78us Max: 78us
    CopyingPhase:	Sum: 76us 99% C.I. 76us-76us Avg: 76us Max: 76us
I/System.out: FlipOtherThreads:	Sum: 58us 99% C.I. 58us-58us Avg: 58us Max: 58us
    ProcessReferences:	Sum: 54us 99% C.I. 19us-35us Avg: 27us Max: 35us
    SweepArray:	Sum: 53us 99% C.I. 53us-53us Avg: 53us Max: 53us
I/System.out: EnqueueFinalizerReferences:	Sum: 38us 99% C.I. 38us-38us Avg: 38us Max: 38us
    RecordFree:	Sum: 37us 99% C.I. 14us-23us Avg: 18.500us Max: 23us
    ForwardSoftReferences:	Sum: 25us 99% C.I. 25us-25us Avg: 25us Max: 25us
    FlipThreadRoots:	Sum: 21us 99% C.I. 21us-21us Avg: 21us Max: 21us
I/System.out: (Paused)SetFromSpace:	Sum: 19us 99% C.I. 19us-19us Avg: 19us Max: 19us
    ResumeRunnableThreads:	Sum: 12us 99% C.I. 12us-12us Avg: 12us Max: 12us
    EmptyRBMarkBitStack:	Sum: 8us 99% C.I. 8us-8us Avg: 8us Max: 8us
    SwapBitmaps:	Sum: 7us 99% C.I. 7us-7us Avg: 7us Max: 7us
    Done Dumping histograms
    young concurrent copying paused:	Sum: 750us 99% C.I. 750us-750us Avg: 750us Max: 750us
I/System.out: young concurrent copying freed-bytes: Avg: 1052KB Max: 1052KB Min: 1052KB
    Freed-bytes histogram: 960:1
    young concurrent copying total time: 35.641ms mean time: 35.641ms
    young concurrent copying freed: 8956 objects with total size 1052KB
I/System.out: young concurrent copying throughput: 255886/s / 29MB/s  per cpu-time: 179578666/s / 171MB/s
    Average minor GC reclaim bytes ratio 0.742269 over 1 GC cycles
    Average minor GC copied live bytes ratio 0.276211 over 2 minor GCs
    Cumulative bytes moved 1410368
    Cumulative objects moved 26626
I/System.out: Peak regions allocated 28 (7168KB) / 768 (192MB)
    Total time spent in GC: 35.641ms
    Mean GC size throughput: 28MB/s per cpu-time: 169MB/s
    Mean GC object throughput: 251284 objects/s
    Total number of allocations 75978
    Total bytes allocated 3382KB
    Total bytes freed 1052KB
I/System.out: Free memory 23MB
    Free memory until GC 23MB
    Free memory until OOME 189MB
    Total memory 26MB
    Max memory 192MB
    Zygote space size 3040KB
    Total mutator paused time: 750us
I/System.out: Total time waiting for GC to complete: 80.600us
    Total GC count: 1
    Total GC time: 35.641ms
    Total blocking GC count: 0
    Total blocking GC time: 0
    Histogram of GC count per 10000 ms: 0:1
    Histogram of blocking GC count per 10000 ms: 0:1
    Native bytes total: 15621964 registered: 98204
I/System.out: Total native bytes at last GC: 15537168
    /system/framework/oat/x86/android.hidl.manager-V1.0-java.odex: quicken
    /system/framework/oat/x86/android.test.base.odex: quicken
    /system/framework/oat/x86/android.hidl.base-V1.0-java.odex: quicken
I/System.out: Current JIT code cache size (used / resident): 0KB / 32KB
    Current JIT data cache size (used / resident): 4KB / 32KB
    Zygote JIT code cache size (at point of fork): 45KB / 48KB
    Zygote JIT data cache size (at point of fork): 33KB / 36KB
    Current JIT mini-debug-info size: 26KB
I/System.out: Current JIT capacity: 64KB
    Current number of JIT JNI stub entries: 0
    Current number of JIT code cache entries: 48
    Total number of JIT compilations: 6
    Total number of JIT compilations for on stack replacement: 1
I/System.out: Total number of JIT code cache collections: 0
    Memory used for stack maps: Avg: 35B Max: 52B Min: 28B
    Memory used for compiled code: Avg: 125B Max: 257B Min: 69B
    Memory used for profiling info: Avg: 70B Max: 188B Min: 20B
    Start Dumping histograms for 48 iterations for JIT timings
    Compiling:	Sum: 385.780ms 99% C.I. 0.556ms-25.610ms Avg: 8.037ms Max: 25.610ms
I/System.out: TrimMaps:	Sum: 44.431ms 99% C.I. 2.400us-5148us Avg: 925.645us Max: 5643us
    Done Dumping histograms
    Memory used for compilation: Avg: 83KB Max: 322KB Min: 8560B
    ProfileSaver total_bytes_written=0
    ProfileSaver total_number_of_writes=0
    ProfileSaver total_number_of_code_cache_queries=0
I/System.out: ProfileSaver total_number_of_skipped_writes=0
    ProfileSaver total_number_of_failed_writes=0
    ProfileSaver total_ms_of_sleep=5000
    ProfileSaver total_ms_of_work=0
I/System.out: ProfileSaver total_number_of_hot_spikes=5
    ProfileSaver total_number_of_wake_ups=0
I/System.out: suspend all histogram:	Sum: 11.468ms 99% C.I. 0.018ms-10.658ms Avg: 1.042ms Max: 11.094ms
    DALVIK THREADS (15):
    "main" prio=5 tid=1 Runnable
      | group="main" sCount=0 dsCount=0 flags=0 obj=0x72107008 self=0xe7d05410
      | sysTid=2738 nice=-10 cgrp=top-app sched=0/0 handle=0xf6267478
I/System.out:   | state=R schedstat=( 5812106631 1041760011 2536 ) utm=535 stm=45 core=0 HZ=100
      | stack=0xff7cb000-0xff7cd000 stackSize=8192KB
      | held mutexes= "mutator lock"(shared held)
        at com.tomerpacific.anrdetection.MainActivity$onCreate$1$1.onClick(MainActivity.kt:18)
        at android.view.View.performClick(View.java:7448)
        at android.view.View.performClickInternal(View.java:7425)
I/System.out:     at android.view.View.access$3600(View.java:810)
        at android.view.View$PerformClick.run(View.java:28305)
I/System.out:     at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
I/System.out:     at java.lang.reflect.Method.invoke(Native method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

这只是所有输出的部分片段,但你可以看到它提供了大量的信息,包括:

  • 可用内存/总内存/最大内存
  • 堆诊断(空闲百分比,分配对象的大小和数量)。
  • 主线程的堆栈跟踪

其他有用的方法包括:

  • getTimestamp - 进程终止时的时间戳
  • getDescription - 关于进程终止原因的系统描述

善于利用资源

如果你的应用程序确实受到ANR的困扰,解决它们可能是相当棘手的。这可能是因为你没有得到完整的堆栈跟踪(或者根本就没有),你无法重现它,或者它发生在一些复杂的设备上。那么,你能做什么呢?

在Android Studio≥3.2版本中,你有一个叫做CPU Profiler的工具。这个工具可以让你在应用程序的运行期间检查你的线程活动。通过它,你可以发现哪些线程在运行,运行了多长时间,以及它们在哪里运行。

要使用它,在Android Studio中,进入视图→工具窗口→分析器。

1_RklpDywn-s_a7seW8_wcaw

一个窗口将在屏幕的底部打开。一旦你把一个进程附加到它上面,你会看到三个时间线:

  1. 事件时间线
  2. CPU时间线
  3. 线程时间线

1__aG-Tlaaa7ZWpLbuQnLlqQ

你要关注线程时间线,看看那里是否有什么不正常的情况。每个线程的活动可以通过三种颜色来识别:

  • 绿色 - 表示该线程正在运行或处于可运行状态
  • 黄色--表示该线程正在等待执行某些I/O操作
  • 灰色--表示该线程正在睡觉

总结

希望你已经获得了一些信心,使你的应用程序尽可能地防止ANR。使用上面列出的工具和技术可能有助于防止你的应用程序的下一次ANR。

欢迎你查看我在GitHub上的其他一些文章。