Android 动态权限封装——新手入门使用指南

一、动态权限概述

自 Android 6.0(API 23)起,系统引入 运行时权限模型,危险权限需在运行时动态申请。本文将从封装、使用到兼容性全面解析。

二、动态权限使用场景

1. 危险权限申请

  • 摄像头、位置、存储、通讯录等敏感权限。

2 权限组处理

  • 请求同一权限组的多个权限时,系统可能合并弹窗(如 STORAGE 组的读写权限)。

3. 自定义权限提示

  • 用户拒绝后,展示解释对话框并引导至设置页。

三、动态权限封装实践

1. 基础权限工具类封装

kotlin 复制代码
object PermissionUtils {

    // 检查权限是否已授予
    fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    // 批量检查权限
    fun checkPermissions(context: Context, permissions: Array<String>): BooleanArray {
        return permissions.map { checkPermission(context, it) }.toBooleanArray()
    }

    // 请求权限(带回调)
    fun requestPermissions(
        activity: FragmentActivity,
        permissions: Array<String>,
        requestCode: Int,
        callback: (granted: BooleanArray) -> Unit
    ) {
        val launcher = activity.registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions()
        ) { grants ->
            callback(grants.values.map { it }.toBooleanArray())
        }
        launcher.launch(permissions)
    }
}

2. 链式调用封装(扩展函数)

kotlin 复制代码
class PermissionRequest(private val activity: FragmentActivity) {

    private var permissions: Array<String> = emptyArray()
    private var onGranted: (() -> Unit)? = null
    private var onDenied: ((deniedPermissions: List<String>) -> Unit)? = null

    fun permissions(vararg permissions: String): PermissionRequest {
        this.permissions = permissions.toTypedArray()
        return this
    }

    fun onGranted(block: () -> Unit): PermissionRequest {
        this.onGranted = block
        return this
    }

    fun onDenied(block: (deniedPermissions: List<String>) -> Unit): PermissionRequest {
        this.onDenied = block
        return this
    }

    fun execute() {
        if (permissions.isEmpty()) return

        val allGranted = permissions.all { PermissionUtils.checkPermission(activity, it) }
        if (allGranted) {
            onGranted?.invoke()
            return
        }

        PermissionUtils.requestPermissions(activity, permissions, System.currentTimeMillis().toInt()) { grants ->
            val deniedList = permissions.filterIndexed { index, _ -> !grants[index] }
            if (deniedList.isEmpty()) {
                onGranted?.invoke()
            } else {
                onDenied?.invoke(deniedList)
            }
        }
    }
}

// 使用示例
PermissionRequest(activity)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS)
    .onGranted { openCamera() }
    .onDenied { showDeniedDialog(it) }
    .execute()

四、注意事项

1. 权限声明与分组

  • 清单文件声明 :所有危险权限需在 AndroidManifest.xml 中声明。
  • 权限组影响:若用户已授权同一组的某个权限,其他权限可能自动授予(Android 10+ 调整此策略)。

2. 用户拒绝处理

  • 永久拒绝判断

    kotlin 复制代码
    fun shouldShowRationale(activity: FragmentActivity, permission: String): Boolean {
        return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
    }
  • 引导至设置页

    kotlin 复制代码
    fun openAppSettings(activity: Activity) {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", activity.packageName, null)
        }
        activity.startActivity(intent)
    }

3. 特殊权限处理

  • 后台定位权限(Android 10+) :需在清单中添加 <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

  • 悬浮窗权限(SYSTEM_ALERT_WINDOW):需特殊处理:

    kotlin 复制代码
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context)) {
        val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
        intent.data = Uri.parse("package:${context.packageName}")
        startActivity(intent)
    }

五、版本兼容性

1. Android 6.0 以下(API < 23)

  • 无需动态申请,安装时自动授予清单中声明的权限。

2. Android 11+(API 30)

  • 单次授权ACCESS_BACKGROUND_LOCATION 需单独申请。
  • 权限可见性 :未在清单中声明的权限无法调用 checkSelfPermission

3. Android 13+(API 33)

  • 细化媒体权限READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 替代 READ_EXTERNAL_STORAGE
  • 附近 Wi-Fi 设备权限NEARBY_WIFI_DEVICES 替代 ACCESS_FINE_LOCATION 用于 Wi-Fi 扫描。

六、最佳实践

  1. 按需申请:仅在需要时请求权限,避免一次性申请所有权限。
  2. 最小权限原则 :优先使用 READ_EXTERNAL_STORAGE 替代 WRITE_EXTERNAL_STORAGE
  3. 测试用例覆盖
    • 用户首次拒绝。
    • 用户勾选"不再询问"。
    • 权限被系统自动撤销(如通过 ADB 命令)。
  4. 使用 Jetpack 安全组件 :如 Security Identity Credential API 替代直接存储敏感数据。

七、完整示例代码

kotlin 复制代码
// MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val PERMISSIONS = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.READ_CONTACTS
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_open_camera.setOnClickListener {
            requestCameraPermission()
        }
    }

    private fun requestCameraPermission() {
        PermissionRequest(this)
            .permissions(*PERMISSIONS)
            .onGranted { openCamera() }
            .onDenied { deniedPermissions ->
                if (deniedPermissions.any { !shouldShowRationale(it) }) {
                    showGoToSettingsDialog()
                } else {
                    showRationaleDialog()
                }
            }
            .execute()
    }

    private fun showRationaleDialog() {
        AlertDialog.Builder(this)
            .setTitle("权限需要")
            .setMessage("请授予相机和通讯录权限以正常使用功能")
            .setPositiveButton("确定") { _, _ -> requestCameraPermission() }
            .setNegativeButton("取消", null)
            .show()
    }

    private fun showGoToSettingsDialog() {
        AlertDialog.Builder(this)
            .setTitle("权限被永久拒绝")
            .setMessage("请前往设置中手动开启权限")
            .setPositiveButton("去设置") { _, _ -> openAppSettings() }
            .setNegativeButton("取消", null)
            .show()
    }

    private fun openCamera() {
        // 打开相机逻辑
    }

    private fun shouldShowRationale(permission: String): Boolean {
        return ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
    }

    private fun openAppSettings() {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", packageName, null)
        }
        startActivity(intent)
    }
}

八、总结

  • 封装核心:简化权限检查与请求流程,统一处理回调。
  • 兼容性关键:适配 Android 6.0+ 动态模型,处理 Android 11+ 细化权限。
  • 用户体验:友好引导用户授予权限,避免粗暴拒绝导致流失。

更多分享

  1. 一文吃透Kotlin中冷流(Clod Flow)和热流(Hot Flow)
  2. 一文带你吃透Kotlin协程的launch()和async()的区别
  3. 一文带你吃透接口(Interface)结合 @AutoService 与 ServiceLoader 详解
  4. 一文带你吃透Android中显示Intent与隐式Intent的区别
  5. 一文带你吃透Android View绘制流程与原理详解
相关推荐
casual_clover1 小时前
Android 设备实现 adb connect 连接的步骤
android·adb
恋猫de小郭1 小时前
Android 确定废弃「屏幕方向锁定」等 API ,如何让 App 适配大屏和 PC/XR 等场景
android·前端·flutter
神仙别闹2 小时前
基于Java(SSM)+Mysql实现移动大厅业务办理(增删改查)
android·java·mysql
木子庆五4 小时前
Android设计模式之模板方法模式
android·设计模式·模板方法模式
孤舟簔笠翁6 小时前
【Audio开发一】android音频问题排查指南
android·音视频
CYRUS_STUDIO8 小时前
Unidbg Trace 反 OLLVM 控制流平坦化(fla)
android·逆向·汇编语言
木子庆五10 小时前
Android设计模式之工厂方法模式
android·设计模式·工厂方法模式
_丿丨丨_11 小时前
PHP回调后门小总结
android·开发语言·php
程序员江同学11 小时前
Kotlin 技术月报 | 2025 年 3 月
android·kotlin
热爱编程的小曾12 小时前
sqli-labs靶场 less 7
android·adb·less