在 Android 开发过程中,关于权限处理我一直存在一个误区: "既然已经使用了运行时动态申请(Runtime Permissions),是否就不需要在 AndroidManifest.xml 中进行静态注册了?"
今天终于明确了,这个答案是否定的。无论 Android 版本如何演变,清单文件注册是基础,运行时申请是补充。本文将从底层机制出发,分析为何"二者缺一不可",并重点梳理 Android 13/14 在媒体与蓝牙权限上的最新适配规则。
一、 核心机制:资格认证与钥匙索取
要理解 Android 权限的双重验证机制,可以将其类比为"大楼门禁系统"。
1. 静态注册 (AndroidManifest.xml) ------ "资格认证"
清单文件中的 <uses-permission> 声明,相当于应用向 Android 系统备案。
- 作用 :告知系统,该应用具备申请某项能力的资格。
- 机制:系统在安装应用时会读取清单建立"白名单"。如果清单中未包含某项权限,系统会认为应用根本不具备该功能,从而在后续的请求中直接拦截。
2. 动态申请 (Runtime Request) ------ "索取钥匙"
代码中的 requestPermissions() 调用,相当于在需要进入特定房间时,向用户(管理员)索要钥匙。
- 作用 :在 Android 6.0 (API 23) 及以上版本,针对危险权限(如相机、定位、存储),必须在运行时获取用户的显式同意。
- 机制:只有通过了第一步的"资格认证",系统才会弹出对话框询问用户。
二、 致命陷阱:静默失败 (Silent Failure)
如果应用仅编写了动态申请代码,而忽略了清单文件的注册,将导致以下后果:
- 应用调用
requestPermissions。 - 系统检查清单白名单,发现未注册该权限。
- 系统不弹出任何授权对话框。
- 回调方法
onRequestPermissionsResult被立即触发,返回码直接为PERMISSION_DENIED。
这种"静默失败"往往会导致开发者误以为是设备兼容性问题或逻辑错误,因为没有任何报错信息,仅仅是弹窗没有出现。因此,清单注册是动态申请生效的先决条件。
三、 实战:Android 13/14 图片与媒体权限适配
随着 Android 版本的迭代,存储权限已从最初的粗粒度控制转向精细化控制,乃至 Android 14 的"部分授权"。
1. 权限演进对照表
| Android 版本 | 清单注册 (Manifest) | 动态申请逻辑 | 特点 |
|---|---|---|---|
| Android 12 及以下 | READ_EXTERNAL_STORAGE |
申请 READ_EXTERNAL_STORAGE |
全量授权:所有文件可见。 |
| Android 13 (API 33) | READ_MEDIA_IMAGES READ_MEDIA_VIDEO READ_MEDIA_AUDIO |
按需申请对应的媒体权限 | 类型隔离:取代了通用的 Storage 权限。 |
| Android 14 (API 34) | 同上,新增: READ_MEDIA_VISUAL_USER_SELECTED |
IMAGES + USER_SELECTED 同时申请 |
部分授权:用户可仅授予部分照片的访问权。 |
2. 清单文件适配策略
为了兼容不同版本的设备,AndroidManifest.xml 需同时声明新旧权限,并利用 maxSdkVersion 进行版本隔离:
XML
ini
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
</manifest>
3. 代码适配逻辑
在运行时,应用必须根据当前设备的 Build.VERSION.SDK_INT 决定申请哪组权限:
Kotlin
scss
// 伪代码示例
val permissions = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> {
// Android 14: 图片权限 + 用户选择权限
arrayOf(Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
// Android 13: 仅申请图片权限
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
}
else -> {
// 旧版本: 申请外部存储权限
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
// 发起请求
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
四、 实战:Android 12+ 蓝牙权限与脱离定位
在 Android 12 (API 31) 之前,蓝牙扫描通常需要申请定位权限(ACCESS_FINE_LOCATION),这常引起用户的隐私担忧。Android 12 引入了独立的蓝牙权限,彻底解耦了蓝牙与定位(除非应用确实需要利用蓝牙信标进行定位)。
1. 新增权限组
BLUETOOTH_SCAN: 扫描设备。BLUETOOTH_CONNECT: 连接已配对设备。BLUETOOTH_ADVERTISE: 广播数据。
2. 关键标志:neverForLocation
如果应用使用蓝牙仅是为了连接设备(如耳机、手环),而非定位,强烈建议 在清单中添加 neverForLocation 标志。这能豁免运行时对定位权限的依赖。
XML
ini
<manifest ...>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
</manifest>
3. 代码差异
- Android 12+ : 直接申请
BLUETOOTH_SCAN和BLUETOOTH_CONNECT,无需申请 Location。 - Android 11- : 必须申请
ACCESS_FINE_LOCATION。
五、 总结
Android 权限系统正朝着"最小化授权"和"用户隐私优先"的方向发展。对于开发者而言,需牢记以下原则:
- 双重保障 :
AndroidManifest.xml注册与 Runtime Code 申请必须一一对应,缺一不可。 - 版本隔离 :利用
maxSdkVersion和代码中的版本判断,兼顾新旧设备。 - 按需索取 :仅申请应用运行所必需的最小权限集合(如蓝牙的
neverForLocation),以提升用户信任度。