本部分通过一个示例跟踪记录展示了如何使用 ProfilingManager 调试应用无响应 (ANR) 问题。
设置应用以收集 ANR
首先,在应用中设置 ANR 触发器:
public void addANRTrigger() { ProfilingManager profilingManager = getApplicationContext().getSystemService( ProfilingManager.class); List<ProfilingTrigger> triggers = new ArrayList<>(); ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder( ProfilingTrigger.TRIGGER_TYPE_ANR); triggers.add(triggerBuilder.build()); Executor mainExecutor = Executors.newSingleThreadExecutor(); Consumer<ProfilingResult> resultCallback = profilingResult -> { // Handle uploading trace to your back-end }; profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback); profilingManager.addProfilingTriggers(triggers); }
捕获并上传 ANR 跟踪记录后,在 Perfetto 界面中将其打开。
分析跟踪记录
由于 ANR 触发了跟踪,因此您知道当系统检测到应用的主线程无响应时,跟踪便会结束。图 1 展示了如何导航到应用的主线程,该线程在界面中进行了相应标记。
跟踪记录的结束时间与 ANR 的时间戳一致,如图 2 所示。
跟踪记录还会显示发生 ANR 时应用正在运行的操作。
具体而言,该应用在 handleNetworkResponse 跟踪切片中运行了代码。此切片位于 MyApp:SubmitButton 切片内。它使用了 1.48 秒的 CPU 时间(图 3)。
如果您在调试时仅依赖 ANR 发生时的堆栈轨迹,则可能会错误地将 ANR 完全归因于 handleNetworkResponse 轨迹切片中运行的代码,该切片在性能分析完成记录时并未结束。不过,1.48 秒不足以自行触发 ANR,即使这是一项耗时操作。您需要进一步回溯,了解在此方法之前是什么阻塞了主线程。
为了找到一个起点来寻找 ANR 的原因,我们开始寻找界面线程生成的最后一帧,该帧对应于 Choreographer#doFrame 551275 切片,并且在开始以 ANR 结束的 MyApp:SubmitButton 切片之前没有大的延迟来源(图 4)。
如需了解阻塞情况,请缩小视图以检查完整的 MyApp:SubmitButton 切片。您会注意到线程状态中的一个关键细节,如图 4 所示:线程有 75% 的时间(6.7 秒)处于 Sleeping 状态,只有 24% 的时间处于 Running 状态。
这表明 ANR 的主要原因是等待,而不是计算。 检查个别睡眠情况,找出规律。
前三个休眠间隔(图 6-8)几乎完全相同,每个间隔大约为 2 秒。第四次休眠(图 9)是异常值,时长为 0.7 秒。 在计算环境中,时长恰好为 2 秒的情况很少是巧合。这强烈表明是程序化超时,而不是随机资源争用。最后一次休眠可能是因为线程等待的操作成功,导致线程结束等待。
该假设是:应用多次达到用户定义的 2 秒超时时间,最终成功,导致延迟时间足以触发 ANR。
如需验证这一点,请检查与 MyApp:SubmitButton 跟踪记录部分关联的代码:
private static final int NETWORK_TIMEOUT_MILLISECS = 2000; public void setupButtonCallback() { findViewById(R.id.submit).setOnClickListener(submitButtonView -> { Trace.beginSection("MyApp:SubmitButton"); onClickSubmit(); Trace.endSection(); }); } public void onClickSubmit() { prepareNetworkRequest(); boolean networkRequestSuccess = false; int maxAttempts = 10; while (!networkRequestSuccess && maxAttempts > 0) { networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS); maxAttempts--; } if (networkRequestSuccess) { handleNetworkResponse(); } } boolean performNetworkRequest(int timeoutMiliseconds) { // ... } void prepareNetworkRequest() { // ... } public void handleNetworkResponse() { Trace.beginSection("handleNetworkResponse"); // ... Trace.endSection(); }
代码证实了这一假设。onClickSubmit 方法在界面线程上执行网络请求,并使用硬编码的 NETWORK_TIMEOUT_MILLISECS(2000 毫秒)。
至关重要的是,它在 while 循环内运行,最多重试 10 次。
在此特定跟踪记录中,用户的网络连接可能较差。前三次尝试失败,导致三次 2 秒超时(总共 6 秒)。第四次尝试在 0.7 秒后成功,代码得以继续执行 handleNetworkResponse。不过,累积的等待时间已经触发了 ANR。
为避免此类 ANR,请将具有不同延迟时间的网络相关操作放入后台线程,而不是在主线程中执行。这样,即使连接不佳,界面也能保持响应,从而彻底消除此类 ANR。