权限综合实战:完整权限管理框架设计

说实话,讲到这里,我觉得是时候把前面所有知识点串起来了。你想想看,我们讲了权限分类、动态申请、特殊权限、权限回调……但真正到项目里,这些知识点是散落的。你需要一个框架,把它们像拼图一样拼起来。

我在好几个大型App里都做过权限重构。最痛苦的一次,是接手一个老项目——权限申请散落在十几个Activity里,有的用RxPermissions,有的自己写回调,还有的直接在Fragment里偷偷request。嗯,那场面,简直是一场灾难。

所以今天,我带你设计一个完整的权限管理框架。它包含三个核心部分:权限状态机统一申请流程用户引导策略

一、权限状态机:让权限状态可预测

权限申请不是一次性的。用户可能拒绝、可能勾选“不再询问”、可能去设置里关闭。你需要一个状态机来管理这些流转。

我个人习惯把权限状态定义为四种:

状态 含义 触发条件
NOT_GRANTED 未申请过 / 已重置 首次进入、用户清除数据
GRANTED 已授权 用户允许
DENIED_ONCE 被拒绝一次 用户点击“拒绝”
DENIED_FOREVER 被永久拒绝 用户勾选“不再询问”并拒绝

为什么要有 DENIED_ONCE?我在项目里踩过坑——有些用户第一次拒绝只是手滑,你直接弹引导对话框反而惹人烦。所以我的做法是:第一次拒绝,只提示“这个功能需要权限”,然后再次申请。第二次还拒绝,才走引导流程。

状态机的核心逻辑,我用一个枚举加一个单例来管理:

public enum PermissionState {
    NOT_GRANTED,
    GRANTED,
    DENIED_ONCE,
    DENIED_FOREVER
}

public class PermissionStateMachine {
    private final Map<String, PermissionState> stateMap = new HashMap<>();

    public void onRequestResult(String permission, boolean granted, boolean shouldShowRationale) {
        PermissionState current = stateMap.getOrDefault(permission, PermissionState.NOT_GRANTED);
        if (granted) {
            stateMap.put(permission, PermissionState.GRANTED);
        } else if (shouldShowRationale) {
            // 用户拒绝但未勾选“不再询问”
            stateMap.put(permission, PermissionState.DENIED_ONCE);
        } else {
            // 用户勾选了“不再询问”
            stateMap.put(permission, PermissionState.DENIED_FOREVER);
        }
    }

    public boolean isDeniedForever(String permission) {
        return stateMap.get(permission) == PermissionState.DENIED_FOREVER;
    }
}

这里要注意:shouldShowRationale 是系统给你的信号。如果返回 false 且权限未授予,那就是 DENIED_FOREVER。我曾经在测试机上验证过——小米、华为对“不再询问”的处理略有差异,但 shouldShowRationale 这个 API 是可靠的。

二、统一权限申请框架:告别散落代码

我设计了一个 PermissionManager,它封装了所有申请逻辑。你只需要调用一个方法,传入权限列表和回调。

先看整体架构图:

Activity / Fragment PermissionManager (单例) 状态机模块 PermissionStateMachine 申请执行器 RequestExecutor 引导策略器 GuideStrategy SharedPreferences ActivityCompat Intent + Settings

调用方式极其简单:

PermissionManager.getInstance()
    .request(this, 
        new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
        new PermissionCallback() {
            @Override
            public void onAllGranted() {
                // 开始录制
            }

            @Override
            public void onDenied(List<String> deniedList, boolean hasDeniedForever) {
                if (hasDeniedForever) {
                    // 跳转设置页
                } else {
                    // 提示后重试
                }
            }
        });

你可能会问:为什么不用 RxPermissions?我用过,但后来发现一个问题——RxPermissions 在 Fragment 嵌套时容易内存泄漏。而且它把权限状态和 UI 逻辑耦合得太紧。我的框架把状态机独立出来,哪怕 Activity 重建了,状态还在。

三、用户引导流程:别让用户蒙圈

权限被拒绝后,怎么引导?我见过最粗暴的做法:直接弹 Toast “请在设置中开启权限”。用户看完一脸懵——设置在哪?哪个开关?

我的引导流程分三步:

  1. 轻提示:第一次拒绝,用 Snackbar 提示“需要相机权限才能扫码”,附带一个“重试”按钮。
  2. 对话框解释:第二次拒绝,弹 Dialog 说明为什么需要这个权限,并再次申请。
  3. 跳转设置:第三次拒绝(DENIED_FOREVER),引导用户去系统设置页。

跳转设置页的代码:

public static void openAppSettings(Context context) {
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    Uri uri = Uri.fromParts("package", context.getPackageName(), null);
    intent.setData(uri);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}
小技巧:跳转设置后,用户可能直接返回。你需要在 onResume 里重新检查权限状态。我习惯在 BaseActivity 的 onResume 中调用 PermissionManager.checkAndNotify(),这样用户从设置返回后,框架自动刷新状态。

四、完整框架代码骨架

下面是我在项目里实际使用的框架结构。你直接拿去改包名就能用:

// PermissionManager.java
public class PermissionManager {
    private static volatile PermissionManager instance;
    private PermissionStateMachine stateMachine;
    private Map<Integer, PermissionCallback> callbackMap;

    private PermissionManager() {
        stateMachine = new PermissionStateMachine();
        callbackMap = new HashMap<>();
    }

    public static PermissionManager getInstance() {
        if (instance == null) {
            synchronized (PermissionManager.class) {
                if (instance == null) {
                    instance = new PermissionManager();
                }
            }
        }
        return instance;
    }

    public void request(Activity activity, String[] permissions, PermissionCallback callback) {
        int requestCode = generateRequestCode();
        callbackMap.put(requestCode, callback);

        // 检查是否有 DENIED_FOREVER 的权限
        List<String> foreverDenied = new ArrayList<>();
        for (String perm : permissions) {
            if (stateMachine.isDeniedForever(perm)) {
                foreverDenied.add(perm);
            }
        }

        if (!foreverDenied.isEmpty()) {
            // 直接走引导流程
            showGuideDialog(activity, foreverDenied);
            return;
        }

        // 正常申请
        ActivityCompat.requestPermissions(activity, permissions, requestCode);
    }

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        PermissionCallback callback = callbackMap.remove(requestCode);
        if (callback == null) return;

        List<String> denied = new ArrayList<>();
        boolean hasForever = false;

        for (int i = 0; i < permissions.length; i++) {
            boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
            boolean shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(
                    currentActivity, permissions[i]);

            stateMachine.onRequestResult(permissions[i], granted, shouldShowRationale);

            if (!granted) {
                denied.add(permissions[i]);
                if (!shouldShowRationale) {
                    hasForever = true;
                }
            }
        }

        if (denied.isEmpty()) {
            callback.onAllGranted();
        } else {
            callback.onDenied(denied, hasForever);
        }
    }
}
注意:requestCode 不能超过 65535,否则系统会截断。我一般用 AtomicInteger 自增,并取模 65535。另外,不要在 Fragment 里直接调用 ActivityCompat.requestPermissions,要用 Fragment 的 requestPermissions 方法,否则回调不会走到 Fragment 的 onRequestPermissionsResult。

五、避坑指南

我曾经在一个视频会议项目里,因为权限框架设计不合理,导致上线后崩溃率飙升。总结几个血泪教训:

  • 不要缓存权限状态到内存:用户可能在设置里关闭权限,你的内存状态是过时的。每次使用权限前,用 checkSelfPermission 重新检查。
  • Android 11 的包可见性:如果你要跳转设置页,别忘了在 AndroidManifest 里声明 QUERY_ALL_PACKAGES 权限,否则某些机型跳转失败。
  • 分屏模式下的权限回调:用户分屏时,权限对话框可能被另一个 App 遮挡。你的回调要做好超时处理,我一般加一个 30 秒的计时器。
  • 华为、小米的权限白名单:有些国产 ROM 会拦截 shouldShowRationale 的返回值。我在华为 P40 上遇到过——用户拒绝后 shouldShowRationale 始终返回 true。解决方案是:结合 SharedPreferences 自己记录拒绝次数。

嗯,说到 SharedPreferences,我建议用 EncryptedSharedPreferences 来存权限状态,防止被其他 App 读取。虽然权限状态不是什么敏感信息,但安全习惯要从小处养成。

最后,我想说:权限框架没有银弹。每个项目的业务场景不同,有的需要相机权限才能扫码,有的需要位置权限才能打卡。但核心思想是一样的——把状态管理好,把引导做细致,把回调处理干净。你按照这个思路去设计,至少能覆盖 90% 的场景。

核心要点回顾

  • 权限状态机:NOT_GRANTED → GRANTED / DENIED_ONCE / DENIED_FOREVER
  • 统一框架:PermissionManager 单例 + 回调解耦
  • 引导策略:轻提示 → 解释对话框 → 跳转设置,逐级递进
  • 避坑:国产 ROM 兼容性、分屏超时、内存状态过期
公众号:蓝海资料掘金营,微信deep3321