2. 权限回调相关ANR的典型场景:系统弹窗阻塞、回调线程死锁、权限异步返回延迟

权限回调触发ANR并非单一故障模式。本章将三种高频场景提炼为可识别的故障树,帮助你在排查初期快速划定排查范围。

2.1 场景故障树与排查入口

以下故障树从现象出发,逐级分支到三种典型成因。任一场景均可能在同一应用中复合出现。

故障树检查顺序:

  • 根节点:应用无响应(ANR),日志关键行包含“PermissionsReason”或“PermissionCallback”
  • 一级分支:检查主线程堆栈是否停在 android.app.ActivityThread$ApplicationThread.scheduleTransaction 或 onRequestPermissionsResult
  • 二级分支:根据堆栈特征匹配下方三种场景

快速排查表:遇到权限ANR时,先打开 /data/anr/traces.txt,搜索“main”线程堆栈顶部的方法名。

权限回调ANR 故障树 应用无响应 (ANR) 主线程堆栈:onRequestPermissionsResult 场景1: 系统弹窗阻塞 主线程等待弹窗 Dismiss 场景2: 回调线程死锁 Binder:XXX 阻塞在锁 场景3: 异步返回延迟 未收到 onGranted/Denied 排查要点: 1. 看 window 层级 / 弹窗堆叠 2. 检查 Dialog 是否已 leak 3. 获取 WindowManager dump 排查要点: 1. 搜索线程堆栈中的锁 2. 对比主线程-Binder线程栈 3. 用 thread dump 找死锁环 排查要点: 1. 检查 PermissionController 寿命 2. 查看 binder transaction 超时 3. 复现时抓 systrace 看延迟 核心判断规则:主线程堆栈定位 → 三种场景分叉排查

2.2 场景一:系统弹窗阻塞(现象识别 → 数据采集 → 原因定位)

♦ 现象识别

  • 用户点击权限“允许”或“拒绝”后,弹窗未立即消失,应用界面卡死。
  • logcat 中出现 Input event dispatching timed out,且 focus 窗口为 PermissionController 或 GrantPermissionsActivity。
  • 主线程堆栈卡在 android.os.MessageQueue.nativePollOnce 等待弹窗回调。

♦ 数据采集

# 获取当前窗口层级
adb shell dumpsys window windows | grep -E 'Window|Grant|Permission'

# 获取弹窗 ActivityRecord
adb shell dumpsys activity activities | grep -A 20 "GrantPermissionsActivity"

# 保存 ANR traces
adb pull /data/anr/traces.txt ./traces_permblock.txt

♦ 原因定位(判断规则)

检查项正常异常(弹窗阻塞)
弹窗 Activity 状态mState=RESUMED, mFinish=truemState=STOPPED 或 PAUSED 且未 Finishing
主线程堆栈顶部进入 onRequestPermissionsResult卡在 android.app.ActivityThread$H.handleMessage
Window 层叠只有应用栈与 PermissionController出现第三层弹窗覆盖(如 Fragment Dialog)

典型根因:权限弹窗返回前,应用又弹出了一个 Dialog 或 Snackbar,导致 Window 冲突,系统弹窗无法正常 dismiss。主线程一直被阻塞在 onRequestPermissionsResult 的等待中。

2.3 场景二:回调线程死锁(现象识别 → 数据采集 → 原因定位)

♦ 现象识别

  • ANR 日志中同时存在“main”和“Binder:XX”线程卡死。
  • traces.txt 中主线程堆栈在 onRequestPermissionsResult,而 Binder 线程堆栈在 PackageManager.queryBroadcastReceivers 或 ActivityManager.checkPermission。
  • 主线程持有锁 A,等待锁 B;Binder 线程持有锁 B,等待锁 A。

♦ 数据采集

# 提取两个相关线程的完整堆栈
grep -A 30 "main" traces.txt | head -40

grep -A 30 "Binder:.*_2" traces.txt | head -40

# 检查锁等待链
grep -E "waiting to lock|locked" traces.txt

♦ 原因定位(死锁环识别)

// 典型死锁堆栈特征
"main" prio=5 tid=1 Blocked
  at com.example.MyActivity.onRequestPermissionsResult(MyActivity.java:85)
  - waiting to lock <0x01a2b3c4> (a java.lang.Object)
  ...
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:...)

"Binder:25607_5" prio=5 tid=12 Blocked
  at android.content.pm.PackageManager.queryBroadcastReceivers(PackageManager.java:...)
  - waiting to lock <0x05d6e7f8> (a java.lang.Object)
  ...
  - locked <0x01a2b3c4> (a java.lang.Object)  // 持有主线程等待的锁

死锁判定规则:主线程“waiting to lock”的锁地址,恰好出现在另一个线程的“locked”行中。两个线程形成循环等待即构成死锁。修复方向:禁止在权限回调中使用任何跨进程同步操作(如 wait/notify、同步 query、Lock.lock)。

2.4 场景三:权限异步返回延迟(现象识别 → 数据采集 → 原因定位)

♦ 现象识别

  • logcat 中权限弹窗已出现,但 onRequestPermissionsResult 长达数秒未被回调。
  • Activity 可见但无法操作,触摸事件全部超时。
  • 系统服务端(system_server)无异常,PermissionController 无崩溃日志。

♦ 数据采集

# 增加 systrace 抓取权限回调延迟
adb shell atrace --async_start -b 16000 am wm input

# 复现后停止
adb shell atrace --async_stop -o /tmp/perm_trace.txt

# dump PermissionController 状态
adb shell dumpsys activity service com.android.permissioncontroller/.PermissionControllerService

♦ 原因定位(延迟计算与判断)

延迟位置预期时长异常阈值
用户点击 → system_server 收到< 200ms> 1s(输入管道拥塞)
system_server → PermissionController onGranted< 500ms> 2s(binder 队列拥堵)
PermissionController → 应用 onRequestPermissionsResult< 300ms> 1.5s(主线程繁忙)

典型根因:应用主线程在权限结果返回前正在执行耗时操作(如批量数据库写入、网络同步请求),导致 MessageQueue 无法及时处理来自 ActivityThread 的 EXECUTE_TRANSACTION 消息。修复动作:将权限请求时机提前到 onResume 之前,或在 onResumeFragments 中触发请求。

2.5 修复动作与复测(三种场景通用)

修复动作遵循“主线程不等待、回调不锁、结果不超时”三原则:

  1. 将权限请求入口从 onResume 移动到 onStart 或更早生命周期,避开 Activity 焦点切换。
  2. 在 onRequestPermissionsResult 内部绝不使用同步锁、Thread.join、CountDownLatch.await,改用异步 UI 更新或 Handler.post。
  3. 如果回调超时风险高,增加超时保护逻辑(使用 Handler.postDelayed + 标志位,超时后模拟拒绝结果)。

♦ 复测方案

  • 使用 Monkey 随机权限操作 + 同时发起 UI 跳转,验证无 ANR。
  • 执行 adb shell am start -a android.intent.action.REQUEST_PERMISSIONS 并反复快速按返回键。
  • 通过 sts-tradefed run commandAndExit template/local --template:seqtest 'Permission' 跑完 100 次权限授予/撤销循环。

2.6 预防(编码与 review 清单)

检查点正例反例
回调内部耗时仅更新 UI 标志位或发事件发起网络请求、读写 DB、等待锁
权限请求时机在 Activity.onCreate/onStart 中请求在 onResume 中等待弹窗返回后再执行
弹窗重叠请求前 dismiss 所有 Dialog不关弹窗直接 requestPermissions
回调超时添加 fallback 超时逻辑无超时,无限等待系统广播

Review 杀手问题:“如果 PermissionController 自己 ANR 了,你的 onRequestPermissionsResult 等多久?” 回答不应超过 500ms 且必须有无回调时的兜底分支。