性能调试实践示例:ANR

本部分将通过一个示例轨迹,展示如何使用 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 显示了如何在界面中导航到已相应标记的应用主线程。

Perfetto 界面导航到应用主线程。
图 1. 导航到应用的主线程。

轨迹的末尾与 ANR 的时间戳相匹配,如图 2 所示。

Perfetto 界面显示了轨迹的结尾,并突出显示了 ANR 触发位置。
图 2. ANR 触发位置。

该轨迹还显示了应用在发生 ANR 时正在运行的操作。具体来说,应用在 handleNetworkResponse 轨迹切片中运行了代码。此切片位于 MyApp:SubmitButton 切片内。它使用了 1.48 秒的 CPU 时间(图 3)。

Perfetto 界面显示了 ANR 发生时 handleNetworkResponse 执行所消耗的 CPU 时间。
图 3. 发生 ANR 时的执行情况。

如果您目前仅依靠 ANR 发生时的堆栈轨迹进行调试,则可能会错误地将 ANR 完全归因于在配置文件完成记录时尚未结束的 handleNetworkResponse 轨迹切片中运行的代码。不过,1.48 秒不足以单独触发 ANR,即使它是一项开销很大的操作。您需要进一步回溯时间,了解此方法之前是什么阻塞了主线程。

为了找到 ANR 的原因,我们从界面线程生成的最后一个帧(对应于 Choreographer#doFrame 551275 slice)之后开始查找,并且在开始导致 ANR 的 MyApp:SubmitButton slice 之前没有明显的延迟源(图 4)。

Perfetto 界面,显示了 ANR 之前界面线程呈现的最后一帧。
图 4. ANR 之前生成的最后一个应用帧。

如需了解阻塞情况,请缩小视图以检查整个 MyApp:SubmitButton 切片。如图 4 所示,您会注意到线程状态中的一个关键细节:线程有 75% 的时间(6.7 秒)处于 Sleeping 状态,只有 24% 的时间处于 Running 状态

Perfetto 界面显示了操作期间的线程状态,突出显示了 75% 的休眠时间和 24% 的运行时间。
图 5.`MyApp:SubmitButton` 操作期间的线程状态。

这表明 ANR 的主要原因是等待,而不是计算。检查各个睡眠时段,找出规律。

Perfetto 界面,显示了 MyAppSubmitButton 轨迹切片中的第一个休眠间隔。
图 6. `MyAppSubmitButton` 中的首次休眠时间。
Perfetto 界面,显示了 MyAppSubmitButton 轨迹切片中的第二个睡眠间隔。
图 7. `MyAppSubmitButton` 中的第二次休眠时间。
Perfetto 界面,显示了 MyAppSubmitButton 轨迹切片中的第三个休眠间隔。
图 8. `MyAppSubmitButton` 中的第三个睡眠时间。
Perfetto 界面,显示了 MyAppSubmitButton 轨迹切片中的第四个休眠间隔。
图 9.`MyAppSubmitButton` 中的第四个睡眠时间。

前三个睡眠间隔(图 6-8)几乎完全相同,每个间隔大约为 2 秒。异常的第四次睡眠(图 9)时长为 0.7 秒。 在计算环境中,时长正好为 2 秒的情况很少是巧合。这强烈表明是程序化超时,而不是随机资源争用。最后一次休眠可能是因为线程等待的操作成功完成,因此线程结束了等待。

此假设是,应用多次达到用户定义的 2 秒超时时间,最终成功,但导致了足够的延迟来触发 ANR。

Perfetto 界面显示了 MyApp:SubmitButton 轨迹切片期间的延迟摘要,表明存在多个 2 秒的休眠间隔。
图 10. `MyApp:SubmitButton` slice 期间的延迟时间摘要。

如需验证这一点,请检查与 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) {
  // ...
}

  // ...
}

public void handleNetworkResponse() {
  Trace.beginSection("handleNetworkResponse");
  // ...
  Trace.endSection();
}

此代码证实了这一假设。onClickSubmit 方法在界面线程上执行网络请求,并使用硬编码的 NETWORK_TIMEOUT_MILLISECS(2000 毫秒)。关键在于,它在 while 循环中运行,最多重试 10 次。

在此特定轨迹中,用户可能网络连接状况不佳。前三次尝试均失败,导致三次 2 秒超时(总共 6 秒)。第四次尝试在 0.7 秒后成功,使代码能够继续执行 handleNetworkResponse。不过,累计等待时间已触发 ANR。

为避免此类 ANR,请将延迟时间不定的网络相关操作放入后台线程,而不是在主线程中执行这些操作。 这样一来,即使连接不良,界面也能保持响应,从而完全消除此类 ANR。