3. 权限未声明导致的运行时崩溃

权限崩溃,说白了就是你的 App 在运行时突然摔了个跟头。用户正用得顺手呢,界面一闪就退了。这种崩溃在 Android 6.0 以后非常常见。我自己做 SDK 集成这几年,光是跟权限相关的线上问题就排过不下几十个。

为什么会出现权限未声明的崩溃?核心原因就一个:你在调某个 API 之前,没有在 AndroidManifest 里声明对应的权限。或者说你声明了,但没在运行时动态申请。Android 系统会在你的代码试图访问受保护资源时,直接抛出一个 SecurityException。

⚠️ 特别注意: 不要以为只在 AndroidManifest.xml 里写上 <uses-permission> 就万事大吉。对于危险权限,你还需要在运行时通过 requestPermissions() 去申请。否则,系统会在 Android 6.0+ 设备上静默拒绝你的请求,或者直接崩溃。

3.1 典型的崩溃现场

我记得有一次集成一个地图 SDK,客户反馈在部分华为手机上打开地图就闪退。抓日志一看,核心堆栈是这样的:

java.lang.SecurityException: 
  Permission Denial: 
  getLocationProvider() requires android.permission.ACCESS_FINE_LOCATION 
  or android.permission.ACCESS_COARSE_LOCATION
    at android.os.Parcel.createException(Parcel.java:2071)
    at android.location.ILocationManager$Stub$Proxy.getProviders(LocationManager.java)
    at android.location.LocationManager.getProviders(LocationManager.java:1200)
    at com.map.sdk.MapEngine.init(MapEngine.java:58)
    at com.example.app.MainActivity.onCreate(MainActivity.java:35)

看到了吧?SecurityException 后面直接告诉你缺少哪个权限。很直白,不怕你读不懂。

3.2 权限分类速查表

Android 把权限分成了几类。我整理了一个表格,方便你快速对照排查:

权限类型 是否需要声明 是否需要运行时申请 常见 API 示例
Normal(普通) ✅ 需要 ❌ 不需要 INTERNET、ACCESS_NETWORK_STATE
Dangerous(危险) ✅ 需要 ✅ 必须申请 ACCESS_FINE_LOCATION、CAMERA、READ_EXTERNAL_STORAGE
Signature(签名) ✅ 需要 ❌ 不需要(仅系统应用可用) BIND_ACCESSIBILITY_SERVICE
Special(特殊) ❌ 不声明 ✅ 需要跳转设置页面申请 SYSTEM_ALERT_WINDOW、WRITE_SETTINGS
💡 一个小窍门: 我习惯在集成任何 SDK 之前,先看它的官方文档里有一张"权限依赖清单"。直接对照那张清单去检查 AndroidManifest 和动态申请的逻辑,能省掉后面大部分排查时间。

3.3 避坑指南:我踩过的三个坑

3.3.1 坑一:只写 XML 不申请运行时

几年前我刚做 SDK 集成时,认为在 AndroidManifest.xml 里写了 <uses-permission android:name="android.permission.CAMERA" /> 就够了。结果在 Android 8.0 的真机上,一调相机预览就闪退。那会儿我花了整整一个下午才意识到——危险权限必须动态申请。

正确的做法是调用 checkSelfPermission() 检查,如果没授权就调 requestPermissions()。示例:

// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
    != PackageManager.PERMISSION_GRANTED) {
    // 申请权限
    ActivityCompat.requestPermissions(this,
        new String[]{Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);
}

3.3.2 坑二:Android 11 的包可见性

这个坑在 SDK 集成里太隐蔽了。Android 11 引入了 QUERY_ALL_PACKAGES 权限,如果你的 SDK 里通过 PackageManager.getInstalledApplications() 去查询其他 App 信息,但没在 Manifest 里加 <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />,系统会直接抛 SecurityException。我曾经见过一个广告 SDK 因为这个原因在 Android 12 设备上大面积崩溃。

3.3.3 坑三:第三方 SDK 隐式调用了高危 API

有时候你自己的代码没直接调高危 API,但你集成的某个第三方 SDK 内部调了。比如一个支付 SDK 内部调用了 TelephonyManager.getDeviceId(),这就声明了 READ_PHONE_STATE 权限。如果你忘了声明这个权限,崩溃就会从 SDK 内部抛出。排查这类问题时,我建议在崩溃堆栈里找 com.xxx.sdk 的调用帧,然后去对应 SDK 的文档里确认权限依赖。

🔍 快速排查流程图: 下面这张 SVG 图总结了遇到权限崩溃时的标准排查路径,你可以把它们打印出来贴在工位旁。
权限崩溃排查流程图 捕获 SecurityException 读取异常堆栈:缺失哪个权限? 权限分类? 普通权限 确认已声明 危险权限 声明 + 动态申请? 两件事都做了? 缺少动态申请 修复后验证 说明: ① 特殊权限不声明也行,但需跳转系统设置 ② 签名权限仅系统应用可用,普通 SDK 一般不涉及

3.4 集成 SDK 时的权限配置清单

为了避免反复在同一个地方摔倒,我总结了下面这个检查清单。每次集成新的 SDK 前,我都会逐一核对:

  1. 读文档 —— SDK 官方文档里有没有"Permissions"章节?仔细看。
  2. 加声明 —— 在 AndroidManifest.xml 添加所有需要的 <uses-permission>。
  3. 测低版本 —— 在 Android 6.0 以下的设备上,危险权限不需要动态申请,但声明还是必须的。
  4. 测高版本 —— 在 Android 12 以上,额外注意 BLUETOOTH_CONNECT、QUERY_ALL_PACKAGES 等新规。
  5. 模拟用户拒绝 —— 在测试中勾选"不再询问"并拒绝权限,确认 App 不会直接崩溃。
  6. 查看运行时日志 —— 用 adb logcat -s SecurityException 过滤,可以快速发现遗漏的权限。
💡 额外提醒: 有些 SDK 提供方会在初始化时自动检测权限,如果发现缺失会输出一条警告日志。你在集成后第一时间跑一遍 adb logcat | grep -i "permission\|SecurityException",基本上能提前发现问题。

3.5 一个小而美的防御性写法

我自己在封装 SDK 调用的时候,习惯在最外层加一个 try-catch,把 SecurityException 兜住。虽然不能真正解决权限缺失的问题,但至少能让 App 不闪退,给用户一个友好的提示:

try {
    SDKManager.init(this);
    SDKManager.startScan();
} catch (SecurityException e) {
    Log.e("SDK_WRAPPER", "权限不足,请先授予必要权限", e);
    Toast.makeText(this, "请前往设置授予位置权限", Toast.LENGTH_LONG).show();
}

这种做法呢,更多的是给开发者自己留一条后路。线上环境里,有些用户可能因为各种原因拒了权限,你的 App 如果直接闪退,那体验就太糟糕了。

3.6 总结一下核心要点

  • 权限崩溃的根源:SecurityException,异常信息里会告诉你缺失哪个权限。
  • 解决方案:AndroidManifest 声明 + 运行时动态申请(危险权限)。
  • 排查路径:看堆栈 → 定位哪个 API 触发 → 去对应源码或 SDK 文档找权限依赖 → 补声明/补动态申请。
  • 别忘了 Android 11/12 新增的权限规则,有些权限(如 MANAGE_EXTERNAL_STORAGE)需要跳转到系统设置页面。

嗯,就这些。记住,权限问题其实不复杂,多数情况都是粗心漏掉了声明。按我上面那张流程图走一遍,基本半小时内能搞定。下一类要面对的崩溃问题,我会讲到另一个高频场景——资源加载异常导致的运行时崩溃。