Android10+权限治理:

14 阅读13分钟

尝试跳出枯燥的 API 列表,用**“演进逻辑”“核心场景”**来串联从 Android 10 到 Android 16的权限变化


Android 权限:从 Android 10 到 Android 16 的演进逻辑

前言:为什么 Android 权限让人“头秃”?

作为 Android 开发者一定经历过代码在 Android 9 跑得飞起,一上 Android 14 直接崩溃的绝望

Android 权限系统之所以难以记忆,是因为 Google 的核心策略一直在变:

  1. 早期 (Android 6.0前):一揽子授权,安装即拥有
  2. 中期 (Android 6.0-9.0):运行时权限 (Runtime Permissions) 引入,但也只是简单的 checkrequest
  3. 近期 (Android 10-16)“去权限化” (Permissionless) 和 “最小特权原则”。Google 希望你通过系统组件(如 Photo Picker)来获取数据,而不是申请一个大权限把用户隐私看个精光

本文将按功能模块对 Android 10 到 16 的核心权限变革进行归纳,并附带最新的代码最佳实践


第一章:存储权限 (Storage) —— 变得面目全非

这是 Android 历史上改动最大、最让开发者痛苦的部分 核心逻辑是:“你的地盘你做主,公共区域按需拿,别人的地盘别乱碰”

1. 演进时间轴

  • Android 10 (Q): 引入 Scoped Storage (分区存储)
    • App 只能无限制访问自己的私有目录 (Android/data/包名/)
    • 访问公共目录(相册、下载)需要通过 MediaStore
    • 特权requestLegacyExternalStorage=true 可以暂时逃避。
  • 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 等。
    • 大坑dataSyncmediaProcessing 类型在 Android 15 中有严格的运行时长限制(例如 6 小时),超时直接 ANR 或 Crash。

2. 生存法则

  • 如果你的 App 不需要常驻通知栏,尽量用 WorkManager 替代前台服务。
  • 适配 Android 14 必须检查 startForeground 代码,补全 ServiceType

第三章:特殊/高敏感权限 (Accessibility & System) —— 权力越大,责任越大

这部分权限不走寻常路,通常需要跳转到系统设置页,甚至需要专门的配置文件。

1. 无障碍服务 (Accessibility Service)

很多自动化工具、抢票软件、防撤回插件依赖此功能。它不是简单的 <uses-permission>

  • 流程

    1. 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>
      
    2. 创建 XML 配置 (res/xml/accessibility_service_config.xml): 定义能监控哪些包、响应速度、反馈类型等。
    3. 申请方式:不能弹窗申请,必须 Intent 跳转到 Settings.ACTION_ACCESSIBILITY_SETTINGS 让用户手动开启。
  • Android 13 限制限制旁加载 (Sideload)。如果你是通过 APK 直接安装(非商店),系统会默认禁用无障碍开启入口,用户需要去“应用详情”页手动“允许受限制的设置”。

2. 悬浮窗 (System Alert Window)

  • 权限:SYSTEM_ALERT_WINDOW
  • 演进:从 Android 10 开始,TYPE_APPLICATION_OVERLAY 是唯一合法的窗口类型。Android 12+ 可能会在状态栏显示“正在覆盖其他应用”的提示,防止覆盖攻击。

第四章:代码实战 —— "Launcher" 范式革命

以前我们用 startActivityForResultonRequestPermissionsResult,代码逻辑是割裂的,容易造成内存泄漏或逻辑混乱。

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 处于预览/规划阶段

  1. Android 15 (Vanilla Ice Cream):

    • 私密空间 (Private Space):类似双开,但系统级隔离。App 在私密空间内无法访问主空间的任何数据,权限完全独立。
    • 屏幕录制检测:App 可以注册回调,当被录屏时获得通知(用于金融/版权 App)。
    • 16KB Page Size:底层内存分页大小变化,NDK 开发需要重新编译,否则 App 无法运行。
  2. Android 16 (预计):

    • Photo Picker 强制化:可能会进一步限制直接读取媒体库,强制要求 App 使用 Photo Picker。
    • 精确闹钟限制SCHEDULE_EXACT_ALARM 权限可能会更加严格,推动使用 WorkManager。
    • 隐私沙盒 (Privacy Sandbox):彻底废除广告 ID (GAID),使用 Topics API 替代,广告追踪权限将不复存在。

总结:开发者的“权限思维导图”

认识权限场景

  1. 我要存文件 -> 是私有数据吗?是 -> 直接写 filesDir。不是 -> 媒体文件用 MediaStore,文档用 SAF。别再想 WRITE_EXTERNAL_STORAGE 了!
  2. 我要后台干活 -> 是必须即时响应的吗?是 -> 前台服务 (记得声明类型)。不是 -> WorkManager
  3. 我要用相机/位置/麦克风 -> 必须动态申请,且要有心理准备用户只给你“仅一次”授权。
  4. 我要自动点屏幕 -> 这是无障碍服务,去写 XML 配置。
  5. 代码怎么写 -> 别用 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) 变革:旧的 BLUETOOTHBLUETOOTH_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 (仅限闹钟、计时器类应用申请,享有豁免权)。

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 统一:必须使用 BiometricPrompt API。系统统一提供指纹/面容/虹膜的验证弹窗,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"))

总结:权限治理“三板斧”

面对复杂的权限版本差异,建议遵循以下原则:

  1. 能不申就不申
    • 要文件?用 SAF (OpenDocument) 或 Photo Picker。
    • 要扫描周边?用 Companion Device Manager (配套设备管理器) 或 Nearby API,这甚至不需要位置权限
  2. 版本隔离
    • AndroidManifest.xml 中善用 android:maxSdkVersion。例如 READ_EXTERNAL_STORAGE 在 Android 13+ 已经没用了,就加上 maxSdkVersion="32",避免误导系统
  3. 拥抱 Launcher
    • 把所有的权限请求、Intent 跳转回调都重构为 registerForActivityResult,这能极大减少代码耦合,且自动处理生命周期问题