14、权限注解处理:运行时权限的注解封装、自动生成权限申请代码

运行时权限,这大概是每个Android开发者都绕不开的坎儿。

我记得刚入行那会儿,每次要申请个相机权限,都得写一大坨回调代码。onRequestPermissionsResult里塞满各种判断,代码又臭又长。后来我就在想——能不能用注解把这套流程简化掉?

说白了,就是让开发者只关心「我要什么权限」和「权限拿到后做什么」,剩下的脏活累活,交给注解处理器去干。

14.1 运行时权限的痛点在哪

先看看传统写法有多烦人:

// 传统方式:又臭又长
private void requestCamera() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CAMERA}, 100);
    } else {
        openCamera();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode,
        @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 100) {
        if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            openCamera();
        } else {
            showToast("没有相机权限");
        }
    }
}

你想想看,每个页面如果有三五个权限,这代码量直接翻倍。而且requestCode还得自己维护,一不小心就冲突了。

核心痛点:

  • 模板代码重复,每个权限都要写check和request
  • 回调分散,逻辑不集中
  • requestCode容易冲突,维护成本高
  • 拒绝后再次申请的处理很麻烦

14.2 注解封装:定义我们想要的API

我理想中的权限申请,应该是这样的:

@NeedPermission(Manifest.permission.CAMERA)
void takePhoto() {
    // 权限已获取,直接拍照
    camera.open();
}

@PermissionDenied(Manifest.permission.CAMERA)
void onCameraDenied() {
    // 用户拒绝了相机权限
    showToast("需要相机权限才能拍照");
}

嗯,这才像话。开发者只需要关注业务逻辑,权限申请的过程完全透明。

要实现这个效果,我们需要定义几个核心注解:

注解名 作用 使用位置
@NeedPermission 标记需要权限的方法 方法上
@PermissionDenied 权限被拒绝时的回调 方法上
@PermissionRationale 需要解释原因时的回调 方法上
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface NeedPermission {
    String[] value();  // 权限列表
    int requestCode() default -1;  // 可选,不填则自动生成
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PermissionDenied {
    String[] value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PermissionRationale {
    String[] value();
}

小技巧:Retention用CLASS而不是SOURCE,这样注解信息会保留在class文件中,但运行时不需要反射获取。我们通过注解处理器在编译期就把代码生成好了。

14.3 注解处理器:自动生成权限申请代码

接下来是重头戏——编写注解处理器。我项目中第一次写这个的时候,踩了不少坑,尤其是处理多权限组合的情况。

核心逻辑其实不复杂:

  1. 扫描所有被@NeedPermission标记的方法
  2. 收集方法所在的类、权限列表、请求码
  3. 生成一个辅助类,里面包含权限检查、申请、回调处理
@AutoService(Processor.class)
@SupportedAnnotationTypes({
    "com.example.annotation.NeedPermission",
    "com.example.annotation.PermissionDenied",
    "com.example.annotation.PermissionRationale"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PermissionProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        // 收集所有被注解的方法
        for (Element element : roundEnv.getElementsAnnotatedWith(NeedPermission.class)) {
            ExecutableElement method = (ExecutableElement) element;
            NeedPermission annotation = method.getAnnotation(NeedPermission.class);
            String[] permissions = annotation.value();
            int requestCode = annotation.requestCode();

            // 生成代码...
            generatePermissionHelper(method, permissions, requestCode);
        }
        return true;
    }

    private void generatePermissionHelper(ExecutableElement method,
                                          String[] permissions, int requestCode) {
        // 使用JavaPoet或KotlinPoet生成代码
        // 这里省略具体生成逻辑,后面会详细展开
    }
}

注意:注解处理器不能修改已有的源文件,只能生成新文件。所以我们需要生成一个「代理类」或「辅助类」,在运行时通过这个类来完成权限申请。

14.4 运行时桥接:让生成的代码跑起来

光生成代码还不够,还得有个运行时组件来调度。我习惯把这个组件叫做PermissionManager。

public class PermissionManager {

    private static final Map<Integer, PermissionCallback> callbackMap = new HashMap<>();
    private static int requestCodeCounter = 1000;

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

        ActivityCompat.requestPermissions(activity, permissions, requestCode);
    }

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

        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }

        if (allGranted) {
            callback.onGranted();
        } else {
            callback.onDenied();
        }
    }

    public interface PermissionCallback {
        void onGranted();
        void onDenied();
    }
}

然后,注解处理器生成的代码会调用这个PermissionManager。举个例子,假设我们在MainActivity里有一个被注解的方法:

// 生成的辅助类:MainActivity_PermissionHelper
public class MainActivity_PermissionHelper {

    public static void takePhoto(MainActivity activity) {
        PermissionManager.request(activity,
            new String[]{Manifest.permission.CAMERA},
            new PermissionManager.PermissionCallback() {
                @Override
                public void onGranted() {
                    activity.takePhoto();  // 权限拿到了,调用原方法
                }

                @Override
                public void onDenied() {
                    activity.onCameraDenied();  // 权限被拒绝
                }
            });
    }
}

这样,开发者只需要在Activity的onCreate里调用一句:

MainActivity_PermissionHelper.takePhoto(this);

剩下的流程全部自动完成。是不是清爽多了?

14.5 整体架构图

下面这张图展示了整个权限注解处理的流程:

编译期 @NeedPermission 注解处理器 生成辅助类 运行时 PermissionManager Activity/Fragment onRequestPermissionsResult 编译生成 权限申请自动完成 图例: 编译期 运行时 编译生成关系 调用/回调

14.6 避坑指南与最佳实践

我在项目中实际落地这套方案时,遇到过几个比较坑的地方:

我曾经踩过的坑:

  • Fragment中申请权限:一定要用Fragment自身的requestPermissions方法,不要用Activity的。否则Fragment的onRequestPermissionsResult不会被调用。
  • 多个权限组合:如果同时申请多个权限,回调中要检查是否所有权限都被授予。我一开始只检查了第一个,结果用户拒绝了第二个权限,代码还是继续执行了。
  • 配置变更:Activity重建后,callbackMap里的回调会丢失。解决方案是用ViewModel保存回调,或者用LiveData做事件传递。

我的个人习惯:我会在注解处理器中自动为每个权限组合生成唯一的requestCode,避免手动维护。具体做法是用权限列表的hashCode作为基础,再拼接一个自增序列。

14.7 总结

权限注解处理的核心思路,就是用编译期代码生成,替代运行时的模板代码。开发者只需要写注解,剩下的检查、申请、回调分发全部自动完成。

这套方案我已经在多个项目中落地,效果很不错。代码量减少了大概60%,而且权限相关的逻辑都集中在一起,维护起来特别方便。

嗯,如果你也在为权限申请头疼,不妨试试这个思路。从简单的注解定义开始,一步步搭建自己的权限框架,你会发现——原来Android开发也可以这么优雅。


公众号:蓝海资料掘金营,微信deep3321