尝试跳出枯燥的 API 列表,用**“演进逻辑”和“核心场景”**来串联从 Android 10 到 Android 16的权限变化
Android 权限:从 Android 10 到 Android 16 的演进逻辑
前言:为什么 Android 权限让人“头秃”?
作为 Android 开发者一定经历过代码在 Android 9 跑得飞起,一上 Android 14 直接崩溃的绝望
Android 权限系统之所以难以记忆,是因为 Google 的核心策略一直在变:
- 早期 (Android 6.0前):一揽子授权,安装即拥有
- 中期 (Android 6.0-9.0):运行时权限 (Runtime Permissions) 引入,但也只是简单的
check和request - 近期 (Android 10-16):“去权限化” (Permissionless) 和 “最小特权原则”。Google 希望你通过系统组件(如 Photo Picker)来获取数据,而不是申请一个大权限把用户隐私看个精光
本文将按功能模块对 Android 10 到 16 的核心权限变革进行归纳,并附带最新的代码最佳实践
第一章:存储权限 (Storage) —— 变得面目全非
这是 Android 历史上改动最大、最让开发者痛苦的部分 核心逻辑是:“你的地盘你做主,公共区域按需拿,别人的地盘别乱碰”
1. 演进时间轴
- Android 10 (Q): 引入 Scoped Storage (分区存储)
- App 只能无限制访问自己的私有目录 (
Android/data/包名/) - 访问公共目录(相册、下载)需要通过 MediaStore
- 特权:
requestLegacyExternalStorage=true可以暂时逃避。
- App 只能无限制访问自己的私有目录 (
- Android 11 (R): 强制执行分区存储。
WRITE_EXTERNAL_STORAGE基本失效,只能用于写入旧版 API 兼容。- 引入
MANAGE_EXTERNAL_STORAGE:允许管理所有文件 但 Google Play 审核极其严格(仅限文件管理器、杀毒软件等)
- Android 13 (T): 细分媒体权限。
READ_EXTERNAL_STORAGE被拆解为:READ_MEDIA_IMAGES(图片)READ_MEDIA_VIDEO(视频)READ_MEDIA_AUDIO(音频)
- Android 14 (U): 部分访问权限。
- 引入
READ_MEDIA_VISUAL_USER_SELECTED。用户可以只授权给你“3张照片”,而不是整个相册。
- 引入
2. 生存法则(记忆口诀)
- 私有文件 (Context.getExternalFilesDir):不需要任何权限,随便读写。
- 媒体文件 (相册):Android 13+ 申请细分权限,Android 10-12 申请 Read Storage。
- 文档/下载:不要申请权限。使用
Storage Access Framework (SAF),即弹出一个系统文件选择器让用户选,选完系统会给你一个临时 URI
第二章:前台服务与通知 (Foreground Service & Notification) —— 此时无声胜有声
Google 发现很多 App 滥用前台服务保活,或者乱发通知,于是开始下狠手
1. 演进时间轴
- Android 12 (S): 限制后台启动。
- 除少数例外,App 在后台时禁止启动前台服务。
- Android 13 (T): 通知权限动态化。
POST_NOTIFICATIONS登场。不申请不给发通知,用户可以一键关闭你的所有通知。
- Android 14 (U): 前台服务必须分类。
- 必须在 Manifest 中声明
<service android:foregroundServiceType="xxx">。 - 类型包括:
camera,location,microphone,mediaPlayback,phoneCall,dataSync等。 - 大坑:
dataSync和mediaProcessing类型在 Android 15 中有严格的运行时长限制(例如 6 小时),超时直接 ANR 或 Crash。
- 必须在 Manifest 中声明
2. 生存法则
- 如果你的 App 不需要常驻通知栏,尽量用 WorkManager 替代前台服务。
- 适配 Android 14 必须检查
startForeground代码,补全ServiceType。
第三章:特殊/高敏感权限 (Accessibility & System) —— 权力越大,责任越大
这部分权限不走寻常路,通常需要跳转到系统设置页,甚至需要专门的配置文件。
1. 无障碍服务 (Accessibility Service)
很多自动化工具、抢票软件、防撤回插件依赖此功能。它不是简单的 <uses-permission>。
-
流程:
- Manifest 声明 Service:
<service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <!-- 只有系统能绑定我 --> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> <!-- 必须有配置文件 --> </service> - 创建 XML 配置 (
res/xml/accessibility_service_config.xml): 定义能监控哪些包、响应速度、反馈类型等。 - 申请方式:不能弹窗申请,必须 Intent 跳转到
Settings.ACTION_ACCESSIBILITY_SETTINGS让用户手动开启。
- Manifest 声明 Service:
-
Android 13 限制:限制旁加载 (Sideload)。如果你是通过 APK 直接安装(非商店),系统会默认禁用无障碍开启入口,用户需要去“应用详情”页手动“允许受限制的设置”。
2. 悬浮窗 (System Alert Window)
- 权限:
SYSTEM_ALERT_WINDOW。 - 演进:从 Android 10 开始,
TYPE_APPLICATION_OVERLAY是唯一合法的窗口类型。Android 12+ 可能会在状态栏显示“正在覆盖其他应用”的提示,防止覆盖攻击。
第四章:代码实战 —— "Launcher" 范式革命
以前我们用 startActivityForResult 和 onRequestPermissionsResult,代码逻辑是割裂的,容易造成内存泄漏或逻辑混乱。
Android 官方现在强烈推荐 Activity Result API (Jetpack Component)。
1. 为什么叫 Launcher?
因为它将“启动一个请求”和“处理结果”封装成了一个 Launcher 对象。你不需要在 Activity 级别去 Override 方法,而是注册一个回调。
2. 实战:申请单个权限 (Kotlin)
// 1. 在 onCreate 或 onAttach 中注册 (必须在生命周期开始前)
// RequestPermission() 是系统预置的契约 (Contract)
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// 权限已给,执行操作
Log.d("TAG", "Permission Granted")
} else {
// 权限被拒,提示用户或降级处理
Log.d("TAG", "Permission Denied")
}
}
// 2. 在需要的时候发射 (Launch)
fun onCameraButtonClick() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
3. 实战:Photo Picker (去权限化最佳实践)
强烈推荐:不需要申请 READ_EXTERNAL_STORAGE,直接拉起系统选图界面。
// PickVisualMedia 是 Android 13 引入的契约 (旧版本通过 GMS 支持)
private val pickMedia = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri: Uri? ->
// 回调返回用户选中的那个图片的 URI,App 拥有该 URI 的临时读权限
if (uri != null) {
imageView.setImageURI(uri)
} else {
Log.d("PhotoPicker", "No media selected")
}
}
// 使用
fun onSelectPhotoButtonClick() {
// 限制只能选图片
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
第五章:Android 15 & 16 展望 —— 隐私沙盒与封闭
目前 Android 15 处于 Beta/稳定 阶段,Android 16 处于预览/规划阶段
-
Android 15 (Vanilla Ice Cream):
- 私密空间 (Private Space):类似双开,但系统级隔离。App 在私密空间内无法访问主空间的任何数据,权限完全独立。
- 屏幕录制检测:App 可以注册回调,当被录屏时获得通知(用于金融/版权 App)。
- 16KB Page Size:底层内存分页大小变化,NDK 开发需要重新编译,否则 App 无法运行。
-
Android 16 (预计):
- Photo Picker 强制化:可能会进一步限制直接读取媒体库,强制要求 App 使用 Photo Picker。
- 精确闹钟限制:
SCHEDULE_EXACT_ALARM权限可能会更加严格,推动使用 WorkManager。 - 隐私沙盒 (Privacy Sandbox):彻底废除广告 ID (GAID),使用 Topics API 替代,广告追踪权限将不复存在。
总结:开发者的“权限思维导图”
认识权限场景:
- 我要存文件 -> 是私有数据吗?是 -> 直接写
filesDir。不是 -> 媒体文件用 MediaStore,文档用 SAF。别再想WRITE_EXTERNAL_STORAGE了! - 我要后台干活 -> 是必须即时响应的吗?是 -> 前台服务 (记得声明类型)。不是 ->
WorkManager。 - 我要用相机/位置/麦克风 -> 必须动态申请,且要有心理准备用户只给你“仅一次”授权。
- 我要自动点屏幕 -> 这是无障碍服务,去写 XML 配置。
- 代码怎么写 -> 别用
onRequestPermissionsResult,用registerForActivityResult。
希望这篇博文能帮你理清 Android 权限那团乱麻。收藏起来,下次被 Crash 搞疯的时候翻出来看看!
Android 的权限体系庞大如迷宫,特别是涉及到电话核心功能、网络发现、后台精确调度、应用间可见性等领域,每一块都有深坑
Android 10-16 权限全景:从通讯到底层服务的深度解析
如果说存储和媒体权限是“明枪”,那么通讯、网络和系统能力的权限变更就是“暗箭”。它们往往导致应用无法搜到蓝牙设备、无法接听电话、或者闹钟不响
本文将通过功能领域进行划分,深度剖析 Android 10 到 Android 16 (及预览) 的权限变革
第一篇章:电话与通讯 (Telephony & SMS) —— Google Play 的雷区
这部分权限受 Google Play 政策(SMS/Call Log Policy)管控最严。一旦申请不当,应用会被直接下架
1. 默认电话/短信应用 (Default Dialer / SMS Handler)
如果你的 App 想完全接管电话功能(如拨号器),你需要的不仅仅是权限,而是成为**“角色 (Role)”**
-
Android 10+ 变革:
- 权限失效:仅仅申请
READ_CALL_LOG可能被系统忽略,除非是默认拨号器。 - RoleManager 登场:取代了旧的
ACTION_CHANGE_DEFAULT_DIALER。你不再是请求一个权限,而是请求成为“电话角色”
// 请求成为默认电话应用 (Android 10+) val roleManager = getSystemService(RoleManager::class.java) val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER) startActivityForResult(intent, REQUEST_CODE) - 权限失效:仅仅申请
2. 读取设备标识 (IMEI / MEID)
- Android 10 (Q) 核弹级变更:
READ_PHONE_STATE降权:普通 App(非系统预装、非设备所有者)彻底无法获取 IMEI、序列号。- 后果:
getDeviceId()返回 null 或抛异常。 - 替代方案:
- 广告追踪 -> 使用 OAID (国内) 或 GAID (海外)。
- 唯一用户标识 -> 使用 UUID 生成并存在 SP 中,或使用 SSAID (
Settings.Secure.ANDROID_ID)。
3. VoIP 自管通话
- 权限:
MANAGE_OWN_CALLS。 - 用途:让微信、Zoom 的语音通话像系统电话一样,显示在锁屏的全屏接听界面,并能通过蓝牙耳机接听。
- ConnectionService:必须实现这个服务,系统会将你的通话视为“电信级”通话,不仅能获得音频焦点,还能在原生电话打进来时自动处理(如挂起 VoIP)。
第二篇章:连接与网络 (Connectivity) —— 那个“位置”权限的幽灵
长久以来,扫描 Wi-Fi 和蓝牙意味着你能定位用户(通过 BSSID 数据库)。因此,Android 曾强制要求扫描蓝牙必须给定位权限。这个逻辑在 Android 12 被修正。
1. 蓝牙权限的大分裂 (Android 12+)
在 Android 12 之前,连个蓝牙耳机都要申请 ACCESS_FINE_LOCATION,用户非常反感。
- Android 12 (S) 变革:旧的
BLUETOOTH和BLUETOOTH_ADMIN被废弃/拆分。BLUETOOTH_SCAN:搜索设备。BLUETOOTH_CONNECT:连接已配对设备。BLUETOOTH_ADVERTISE:广播自己(作为外设)。- 关键 Flag:如果你真的不需要通过蓝牙推算位置,必须断言:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <!-- 加上这句,就不用申请定位权限了! -->
2. Wi-Fi 与本地网络
- 本地网络发现:Android 对获取本地 IP 和 MAC 地址限制越来越严。
NEARBY_WIFI_DEVICES(Android 13+):- 用于通过 Wi-Fi 感知 (Wi-Fi Aware) 发现附近设备,而无需位置权限。
- 如果要获取 Wi-Fi SSID (名字),你依然需要
ACCESS_FINE_LOCATION(精密定位) 并且用户必须开启 GPS。
第三篇章:应用可见性 (Package Visibility) —— 孤岛时代
在 Android 11 之前,任何 App 都可以扫描手机里装了什么软件(getInstalledPackages),这造成了严重的隐私泄露(比如检测用户是否安装了竞品、是否有金融软件)。
1. <queries> 标签 (Android 11/R)
- 默认不可见:现在调用
pm.getInstalledPackages()只能返回自己和系统核心应用。 - 白名单机制:如果你需要判断用户是否安装了“微信”以便分享,必须在 Manifest 声明:
<queries> <!-- 指定包名 --> <package android:name="com.tencent.mm" /> <!-- 或者指定 Intent 动作 --> <intent> <action android:name="android.intent.action.SEND" /> <data android:mimeType="image/jpeg" /> </intent> </queries>
2. 只有 Launcher 可以全看
QUERY_ALL_PACKAGES权限:允许看到所有 App。- 警告:这也是 Google Play 的极高风险权限。除非你是启动器 (Launcher)、杀毒软件或文件管理器,否则申请这个权限会被直接拒审。
第四篇章:后台任务与闹钟 (Background & Alarms) —— 电池的守门人
1. 精确闹钟 (Exact Alarm)
- 场景:闹钟 App、日历提醒。
- Android 12 (S) 变更:
- 引入
SCHEDULE_EXACT_ALARM。 - 注意:这在 Android 13+ 依然是默认授予的,但 Google Play 政策收紧,普通 App 如果滥用精确闹钟(而不是用
WorkManager),会被审核警告。
- 引入
- Android 14 (U) 变更:
- 权限不再默认授予!App 安装后,需要检查
canScheduleExactAlarms(),如果没有,需跳转设置页让用户开启。 - 替代:
USE_EXACT_ALARM(仅限闹钟、计时器类应用申请,享有豁免权)。
- 权限不再默认授予!App 安装后,需要检查
2. 忽略电池优化
- 权限:
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS。 - 效果:让 App 在 Doze 模式(打盹模式)下也能联网和保活。
- 现状:Google Play 极难通过。如果你只是为了后台推消息,请用 FCM (Firebase) 或国内厂商推送,不要申请这个白名单。
第五篇章:安装与安全 (Install & Security)
1. 安装未知来源应用
- 权限:
REQUEST_INSTALL_PACKAGES。 - 特性:这不是运行时弹窗,而是跳转到“允许来自此来源的应用”设置页。
- Android 10+:Scoped Storage 导致安装 APK 变得复杂,必须通过
FileProvider共享 URI 给系统安装器。
2. 生物识别 (Biometric)
- 弃用:
USE_FINGERPRINT已过时。 - 新标准:
USE_BIOMETRIC。 - UI 统一:必须使用
BiometricPromptAPI。系统统一提供指纹/面容/虹膜的验证弹窗,App 不再直接处理指纹硬件的信号,只能拿到“验证成功/失败”的回调。
第六篇章:Android 14/15/16 新特性前瞻与总结
Android 14 (Upside Down Cake)
- 屏幕截图检测:
DETECT_SCREEN_CAPTURE。用于金融类 App,当用户截屏时收到回调(而非直接阻止,直接阻止是用FLAG_SECURE)。 - 全屏通知限制:
USE_FULL_SCREEN_INTENT只有通话和闹钟应用能默认获取,其他 App 需要用户手动授权。
Android 15 (Vanilla Ice Cream)
- 私密空间 (Private Space):这是一个系统级的“沙盒”。如果你在私密空间外,甚至无法通过
LauncherApps检索到私密空间内的 App。 - 应用归档 (App Archiving):系统支持卸载 App 但保留数据。
Android 16 (Baklava - 预计)
- Privacy Sandbox 强制化:传统的广告 ID 可能彻底退出历史舞台,取而代之的是系统根据用户习惯计算出的“Topics”(兴趣标签),App 只能请求 Topics,不能追踪用户个体。
第七篇章:现代化的代码申请方式 (Launcher 详解)
不要再用 startActivityForResult 甚至 Intent 跳转去申请特殊权限了,Android Jetpack 的 Activity Result API 提供了极其优雅的封装。
1. 申请成为默认助理/电话/短信 (RoleManager)
// 定义 Launcher
val roleLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// 成功成为默认应用
}
}
// 调用
fun requestRole() {
val roleManager = getSystemService(RoleManager::class.java)
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)
roleLauncher.launch(intent)
}
2. 请求开启蓝牙 (不仅仅是权限,是开关)
// 这是一个预定义的 Contract,系统会自动弹窗问用户“是否允许开启蓝牙”
val enableBtLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// 蓝牙已开启
}
}
// 调用
fun enableBluetooth() {
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBtLauncher.launch(intent)
}
3. 打开文档 (SAF - 替代存储权限)
val openDocLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
// 获得了该文件的临时读写权限
uri?.let { handleFile(it) }
}
// 调用:只显示 PDF
openDocLauncher.launch(arrayOf("application/pdf"))
总结:权限治理“三板斧”
面对复杂的权限版本差异,建议遵循以下原则:
- 能不申就不申:
- 要文件?用 SAF (OpenDocument) 或 Photo Picker。
- 要扫描周边?用 Companion Device Manager (配套设备管理器) 或 Nearby API,这甚至不需要位置权限
- 版本隔离:
- 在
AndroidManifest.xml中善用android:maxSdkVersion。例如READ_EXTERNAL_STORAGE在 Android 13+ 已经没用了,就加上maxSdkVersion="32",避免误导系统
- 在
- 拥抱 Launcher:
- 把所有的权限请求、Intent 跳转回调都重构为
registerForActivityResult,这能极大减少代码耦合,且自动处理生命周期问题
- 把所有的权限请求、Intent 跳转回调都重构为