如果有办法使我的应用程序 "应用程序无响应 "不出错,我会这样做。
无响应似乎总是在你意料之外的时候悄悄出现。而问题是,你对它们无能为力。
当你在Google Play Console中看到你的应用程序的概述时,你可能会看到它们弹出来。但那里不会有很多信息来了解ANR是如何发生的,以及你可以做什么来解决这个问题。
除此之外,如果用户碰巧遇到了ANR,你必须依靠他们愿意投入时间和精力来告诉你这个问题。我们也都曾站在另一边。如果你在使用一个应用程序时卡住了,你做的一件事就是直接去卸载它。
那么,作为开发者,我们怎样才能尽一切努力保护我们的应用程序的用户不遭遇ANR呢?
让我们来了解一下。
如何防止Android上的 "应用程序无响应 "错误?
积极主动
在我们深入研究可以帮助我们检测ANR的解决方案之前,让我们了解一下我们可以做些什么来避免一开始就出现ANR(或者尽量减少它们发生的机会)。
这些要点可能听起来很明显,但在一个足够大的应用中,可能很容易忽视一些事情。
首先,检查一下你的代码中是否有地方在UI线程上做大量工作。在UI线程上做的工作应该是简短的,并且与你的应用程序的UI有关。
如果你在那里做任何其他的逻辑,或者甚至是异步工作,请将其委托给一个后台线程或服务。
第二,如果你的线程持有锁或某些需要同步的代码块,确保你没有造成死锁或你的应用程序的某个状态达到死锁。
第三,如果你的应用程序与广播接收器打交道,你必须验证onReceive 方法的执行时间很短,并及时结束。如果那里有可能需要一些时间的工作,请把它委托给一个后台线程
另一种方法,你可以检测可能导致ANR的地方,就是使用 严格模式.你可以在开发你的应用程序时使用它,因为它可以捕捉到主线程上意外的磁盘或网络使用。
要聪明
你已经检查了你的应用程序,并且你认为它没有任何ANR的风险。所以你把它发布给公众使用。
瞧,几个月过去了,你开始看到ANRs的报告。你可以采取什么不同的做法?正如我们前面所讨论的,这些碰撞报告几乎没有提供任何关于ANR的信息。
为了帮助你的应用程序在发生ANR时给你提供最高水平的细节,我将详细介绍两个选项。
- 运行一个线程,该线程会轮询UI线程,看看它是否被卡住了
- 在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上调用的,接受三个参数:
- 包的名称--字符串类型(可以是空的)
- 属于该包的进程的ID--int类型
- 你想找回的最大数量的原因--int类型的
值得注意的是,所有这些参数都可以用默认值来代替。也就是说,你可以传递null作为包的名称,并获得调用者的UID的全部退出原因。
那么,这些记录的对象包含什么呢?嗯,这些对象是 ApplicationExitInfo 类型,它们可以为你提供很多有用的信息。
对于初学者,你可以调用getReason 方法来找出进程终止的原因。这个方法返回一个整数,标记退出原因的代码。如果返回的值是6,这意味着应用程序被终止了,因为它由于发生了ANR而没有反应。
这很整洁,但我们怎么能看到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中,进入视图→工具窗口→分析器。

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

你要关注线程时间线,看看那里是否有什么不正常的情况。每个线程的活动可以通过三种颜色来识别:
- 绿色 - 表示该线程正在运行或处于可运行状态
- 黄色--表示该线程正在等待执行某些I/O操作
- 灰色--表示该线程正在睡觉
总结
希望你已经获得了一些信心,使你的应用程序尽可能地防止ANR。使用上面列出的工具和技术可能有助于防止你的应用程序的下一次ANR。
欢迎你查看我在GitHub上的其他一些文章。