2. 权限回调相关ANR的典型场景:系统弹窗阻塞、回调线程死锁、权限异步返回延迟
权限回调触发ANR并非单一故障模式。本章将三种高频场景提炼为可识别的故障树,帮助你在排查初期快速划定排查范围。
2.1 场景故障树与排查入口
以下故障树从现象出发,逐级分支到三种典型成因。任一场景均可能在同一应用中复合出现。
故障树检查顺序:
- 根节点:应用无响应(ANR),日志关键行包含“PermissionsReason”或“PermissionCallback”
- 一级分支:检查主线程堆栈是否停在
android.app.ActivityThread$ApplicationThread.scheduleTransaction或onRequestPermissionsResult - 二级分支:根据堆栈特征匹配下方三种场景
快速排查表:遇到权限ANR时,先打开 /data/anr/traces.txt,搜索“main”线程堆栈顶部的方法名。
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=true | mState=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 修复动作与复测(三种场景通用)
修复动作遵循“主线程不等待、回调不锁、结果不超时”三原则:
- 将权限请求入口从
onResume移动到onStart或更早生命周期,避开 Activity 焦点切换。 - 在
onRequestPermissionsResult内部绝不使用同步锁、Thread.join、CountDownLatch.await,改用异步 UI 更新或 Handler.post。 - 如果回调超时风险高,增加超时保护逻辑(使用 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 且必须有无回调时的兜底分支。