SecurityException是Android开发中最常见的运行时异常之一,当应用尝试执行一个需要特定权限的操作,而系统判定应用并未获得该权限时,就会抛出这个异常并直接导致应用崩溃。很多开发者会遇到一个奇怪的现象:明明已经调用了requestPermissions方法发起动态权限申请,甚至回调里返回的结果也是授权成功,但真正执行打电话、读写文件、定位等敏感操作时依然抛出SecurityException。这种情况说明权限申请的某个环节出了问题,可能是清单文件声明缺失,可能是权限类型判断错误,也可能是申请流程本身存在逻辑漏洞。本文将系统分析这个问题的成因与解决办法。

一、SecurityException产生的根本原因与权限机制原理
要理解为什么动态申请会失败,首先需要明白Android的权限体系。Android 6.0(API 23)引入了运行时权限机制,将权限分为普通权限和危险权限两大类。普通权限如网络访问权限android.permission.INTERNET,只需在AndroidManifest.xml中声明,系统会在安装时自动授予;危险权限如读写存储、摄像头、定位等,除了清单声明外,还必须在运行时通过代码动态申请并获得用户同意。
SecurityException抛出的本质是权限检查失败。系统在执行敏感操作前会调用checkPermission系列方法校验当前进程是否持有对应权限,一旦校验不通过且该操作不允许静默失败,就会直接抛出异常终止执行。需要特别注意的一点是:清单文件中没有声明的权限,运行时申请是一定会失败的。动态申请的前提是先在Manifest中声明,这是最容易踩的第一个坑。申请了A权限但执行的是B权限对应的操作,同样会导致异常。
另外,Android将危险权限按权限组划分,例如READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE同属STORAGE组。理论上同组权限授权一个,其他也会自动授予,但从Android 11开始系统已经弱化了这一行为,应用不应依赖权限组的隐式授权,而应将用到的每一个权限都显式声明并申请。
二、动态权限申请失败的常见原因分析
1. Manifest中未声明对应权限
这是最高频的错误。动态申请接口requestPermissions只是触发系统授权弹窗,它不能凭空创建权限。如果AndroidManifest.xml中没有对应的<uses-permission>标签,申请结果会直接返回拒绝,后续操作必然抛出SecurityException。正确的声明方式如下:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
2. targetSdkVersion低于23导致申请流程形同虚设
当应用的targetSdkVersion设置为23以下时,系统会采用兼容模式,危险权限在安装时全部自动授予,动态申请接口不会弹出真实的授权对话框。这种情况下如果用户在系统设置中手动关闭了权限,应用调用敏感API时依然会崩溃。检查build.gradle中的targetSdkVersion配置,确保其不低于23,是排查问题的基本步骤。
3. 特殊权限无法通过普通动态申请获得
有一类权限被称为特殊权限(Special Permissions),例如SYSTEM_ALERT_WINDOW(悬浮窗)和WRITE_SETTINGS(修改系统设置),它们不能通过requestPermissions申请,必须跳转到系统设置页面让用户手动开启。如果开发者错误地用普通方式申请这类权限,申请会静默失败,随后执行相关操作时就会抛出SecurityException。悬浮窗权限的正确申请方式是:
// 悬浮窗权限需要跳转设置页面申请
if (!Settings.canDrawOverlays(context)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
}4. Fragment中申请权限的上下文错误
在Fragment中发起权限申请时,如果使用了getActivity().requestPermissions(),授权回调会先走Activity的onRequestPermissionsResult,Fragment中可能收不到回调,导致逻辑判断出错。正确做法是直接调用Fragment自身的requestPermissions()方法,并重写Fragment中的回调方法。
5. 用户永久拒绝后不再弹窗
当用户勾选了“不再询问”或第二次拒绝了权限申请后,系统不会再弹出授权对话框,requestPermissions会立即返回拒绝结果。如果代码中没有处理这种状态,用户反复点击功能按钮却没有响应,或者开发者误以为已授权而直接执行操作,最终引发崩溃。
三、完整正确的动态权限申请实现方案
下面给出一个健壮的权限申请封装,覆盖了权限检查、申请、回调处理和永久拒绝引导的完整流程,推荐使用AndroidX的Activity Result API实现:
// 使用Activity Result API申请权限
private final ActivityResultLauncher<String> permissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
// 权限已授权,执行敏感操作
doSensitiveOperation();
} else {
if (!shouldShowRequestPermissionRationale(
Manifest.permission.CAMERA)) {
// 用户已永久拒绝,引导去设置页开启
showGoSettingsDialog();
} else {
Toast.makeText(this, "权限被拒绝,功能无法使用",
Toast.LENGTH_SHORT).show();
}
}
});
private void requestCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
doSensitiveOperation();
} else {
permissionLauncher.launch(Manifest.permission.CAMERA);
}
}在执行敏感操作前进行最后一次权限校验是防御性编程的好习惯。即使申请流程一切正常,用户也可能在授权后又从系统设置中撤销了权限。例如执行拨号操作前先检查CALL_PHONE权限,未授权时降级为跳转拨号界面而不直接调用startCall,可以有效避免崩溃。同时建议将权限申请封装成独立的权限管理类,统一管理申请入口和结果分发,避免在多个Activity中重复编写零散逻辑。
对于批量申请多个权限的场景,需要遍历回调结果数组逐一判断,只要有一个关键权限被拒绝就应该阻断流程并给出明确提示,而不是默认全部成功。此外,权限申请的时机也有讲究,最理想的时机是在用户触发对应功能时按需申请,让用户清楚知道权限的用途,这样既能提高授权率,也符合各大应用商店的权限合规要求。
四、排查权限问题的实用技巧
当线上或测试环境出现SecurityException时,可以借助adb命令快速定位当前应用的权限状态。执行adb shell dumpsys package 包名 | grep permission可以查看应用已申请和已授予的全部权限列表,判断权限是否真的授予成功。如果列表中没有目标权限,说明Manifest声明可能存在问题,或构建产物没有合并进最新的清单文件。
另一个常见陷阱是第三方SDK的权限合并。Android构建工具会自动合并所有依赖库的Manifest,某些SDK会引入不必要的危险权限,在用户授权时引起困惑。可以通过在自身Manifest中添加tools:node="remove"来移除多余权限,同时定期审计合并后的清单文件。此外,使用shouldShowRequestPermissionRationale方法判断用户是否永久拒绝时要注意,该方法在授权前、拒绝一次后的返回值各不相同,建议结合SharedPreferences记录申请次数来综合判断,才能准确识别永久拒绝状态并给出正确的引导。
SecurityExceptionAndroid运行时权限动态权限申请修改时间:2026-09-02 02:08:34