序
现在市面上越来越多的基于注解的框架出现,它们可能或多或少的存在一些问题,但是确实是在某些方面大大简化了我们的开发周期和难度,也使得有些代码更加优雅。
为了进一步提升自己(装逼中。。。),就决定去了解一下到底什么是注解,它们有几种类型,它的实现原理,它是怎么起到简化代码的作用的。
我们以Android中权限的动态申请为示例,分别以运行时注解和编译时注解两种形式来实现。如有不足,还请指出!!!
基本功
注解仅仅是个标记,注解之所以起作用是对其解析后做了相应的处理。我们会通过编译时注解的处理和运行时注解的处理来进行说明。
元注解
1.@Target
表示该注解可以用在什么地方。可用的参数如下:
类型 | 说明 |
---|---|
CONSTRUCTOR | 构造器的声明 |
FIELD | 域声明(包括enum实例) |
LOCAL_VARIABLE | 局部变量声明 |
METHOD | 方法声明 |
PACKAGE | 包声明 |
TYPE | 类、接口(包括注解类型)或enum声明 |
PARAMETER | 参数声明 |
2.@Retention
表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:
类型 | 说明 |
---|---|
SOURCE | 注解将被编译器丢弃(注解仅保留在源码中) |
CLASS | 注解在class文件中可用,但会被JVM丢弃(编译时注解,这是默认的) |
RUNTIME | JVM将在运行期也保留注解,因此可以通过反射机制读取注解的信息(即运行时注解) |
3.Documented
将此注解包含在Javadoc中
4.Inherited
默认情况下,父类的注解子类是不能继承的,不过可以通过该注解允许子类继承父类中的注解
注解元素
注解元素为注解中定义的元素,比如下例中的int类型的value:
@Target(ElementType.METHOD)
public @interface Test{
int value();
}
注解元素可用的类型如下:
1.所有基本类型
2.String
3.Class
4.enum
5.Annotation(所以注解可以嵌套)
6.以上类型的数组
注意不允许使用任何包装类型,不过由于自动打包的存在,算不上什么限制
默认值限制
编译器对元素的默认值有着严格的限制:
1.不能有不确定的值,即元素要么具有默认值,要么在使用注解时提供元素的值
2.对于非基本类型的元素,无论是在源代码中声明,或是在注解接口中定义默认值,都不能以null作为其值
下面我们从PermissionGen的源码来分析基于运行时注解的权限处理。
运行时注解(PermissionGen)
运行时注解对应 @Retention 为 RUNTIME 的 Annotation。下面是Android中动态申请权限的示例代码:
//请求打电话的权限
public void requestPermission(){
PermissionGen
.with(MainActivity.this)
.addRequestCode(100)
.permissions(Manifest.permission.CALL_PHONE)
.request();
}
//请求失败回调
@PermissionFail(requestCode = 100)
public void callFail(){
Toast.makeText(this,"callFail",Toast.LENGTH_LONG).show();
}
//请求成功回调
@PermissionSuccess(requestCode = CALL)
public void callSuccess(){
Toast.makeText(this,"callSuccess",Toast.LENGTH_LONG).show();
}
看来这一段代码,我不禁有几个疑问:
(1)PermissionSuccess
和PermissionFail
这两个自定义的注解是怎么定义的
(2)它是怎么生效的
自定义注解
/**
* function:运行时注解。权限申请失败
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PermissionFail {
int requestCode();
}
/**
* function:运行时注解。权限申请成功
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PermissionSuccess {
int requestCode();
}
我们发现在注解类的定义中有三个地方需要注意,分别是:
(1)@Retention(RetentionPolicy.xxx)
:注解的保留策略,只有三种可选值,具体可以参考上面列出的表格
(2)@Target(ElementType.METHOD)
:当前注解作用域,我们的示例代码中表示当前注解作用于方法,其它可选值可参考上面的表格
(3)@interface
:表明当前是一个自定义注解
我们发现自定义注解定义完之后,并没有其他的多余操作,那么由他标注的方法是怎么被调用的呢,我们下面关注PermissionGen
这个类。
自定义注解是如何生效的
根据示例代码我们从PermissionGen的with函数开始入手:
private Object mObject;
public static PermissionGen with(Activity activity){
return new PermissionGen(activity);
}
public static PermissionGen with(Fragment fragment){
return new PermissionGen(fragment);
}
private PermissionGen(Object object){
mObject = object;
}
这一步的代码是很简单的,就是调用了PermissionGen的构造函数,并保存了Activity
或Fragment
的实例对象。有了PermissionGen对象,我们就可以继续下一步了,那就是addRequestCode
和permissions
:
public PermissionGen permissions(String... permissions){
mPermissions = permissions;
return this;
}
public PermissionGen addRequestCode(int requestCode){
mRequestCode = requestCode;
return this;
}
就是为两个成员变量赋值,然后返回当前的PermissionGen对象。这些都是简单的,下面重点来了:
@TargetApi(value = Build.VERSION_CODES.M)
public void request(){
requestPermissions(mObject,mRequestCode,mPermissions);
}
@TargetApi(value = Build.VERSION_CODES.M)
private static void requestPermissions(Object object, int requestCode, String[] permissions) {
//判断当前的系统版本是否大于等于23,true表示大等于,反之为false
if(!Utils.isOverMarshmallow()){
doExecuteSuccess(object,requestCode);
return ;
}
//获取没有通过的权限
List<String> deniedPermissions = Utils.findDeniedPermissions(getActivity(object),permissions);
if(deniedPermissions.size() > 0){
//请求权限
if(object instanceof Activity){
((Activity)object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);
} else if(object instanceof Fragment){
((Fragment)object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);
} else {
throw new IllegalArgumentException(object.getClass().getName() + " is not supported");
}
}else{
doExecuteSuccess(object,requestCode);
}
}
在requestPermissions
中,主要是做了以下判断:
(1)如果系统的API等级<=23或者要申请的权限没有被拒绝,则调用doExecuteSuccess
在doExecuteSuccess
中主要是通过Utils工具类来获取对应的Method,参数为Activity或Fragment实例、要寻找的注解类Class以及请求码,因为当前函数为成功的回调,所以注解类为PermissionSuccess
。具体实现逻辑如下:
public static <A extends Annotation> Method findMethodWithRequestCode(Class clazz, Class<A> annotation, int requestCode) {
//获取当前类的所有方法,不包括父类的
for(Method method : clazz.getDeclaredMethods()){
//检查传入的注解是否存在于当前元素
if(method.isAnnotationPresent(annotation)){
if(isEqualRequestCodeFromAnntation(method, annotation, requestCode)){
return method;
}
}
}
return null;
}
思路还是比较清晰的,从Activity实例或Fragment实例中获取到注解为PermissionSuccess
并且requestCode和我们传入一样的方法,然后利用返回的Method对象,通过invoke来调用,源码如下:
private static void executeMethod(Object activity,Method executeMethod){
if(executeMethod != null){
try {
//判断是否有访问权限
if (!executeMethod.isAccessible()) {
//没有访问权限时,关闭权限检查
executeMethod.setAccessible(true);
}
//对调特定函数
executeMethod.invoke(activity,null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
先是判断有没有权限,没有的话,则进行修改,然后通过invoke来调用,这样就完成了回调。
(2)要申请的权限被拒绝或部分拒绝,则将Object强转为Activity或Fragment,然后调用他们请求权限的API。我们同意或拒绝之后,下次再调用request
,还是走到上面的requestPermissions逻辑,成功的已经说了,失败的话,其实本质上是一样的。
编译时注解(MPermission)
运行时注解对应 @Retention 为 CLASS 的 Annotation(同时也是默认的,在自定义注解上,不写@Retention 也默认是CLASS
)。我们还是从自定义注解开始。
自定义注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface PermissionFail {
int value();
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface PermissionSuccess {
int value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ShowRequestPermissionRarionale {
int value();
}
我们发现除了保留策略改为RetentionPolicy.CLASS
,其它并没有什么不同,确实也没有什么不同,我们继续去看编译器。
自定义注解是如何生效的
编译时注解要生效,需要依赖于注解处理器(Annotation Processor)。一个注解的Annotation Processor,以Java代码(或者编译过的class)为输入,生成新的.java文件作为输出。这意味着我们可以生成新的Java代码!这些生成的Java代码是在新生成的.java文件中,新生成的.java文件会和普通的手动编写的Java源代码一样被javac编译。每一个注解处理器都继承自AbstractProcessor
,它有4个函数比较重要:
//对一些工具进行初始化
public synchronized void init(ProcessingEnvironment processingEnv)
//你在这里定义你的注解处理器注册到哪些注解上,必须指定
//它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
public Set<String> getSupportedAnnotationTypes()
//指定该注解处理器使用的JAVA版本,通常返回SourceVersion.latestSupported()
public SourceVersion getSupportedSourceVersion()
//真正生成java代码的地方
//annotations:请求处理的注解类型集合
//roundEnv:可以让你查询出包含特定注解的被注解元素,相当于“有关全局源码的上下文环境”
//如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
//如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
public abstract boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv)
具体我们来看看MPremission中的Processor
,从init开始:
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
mMessager = env.getMessager();
mFiler = env.getFiler();
mElementUtils = env.getElementUtils();
}
我们发现,该函数分别保存了三个成员变量,它们的作用是:打印辅助信息(错误、警告,其它提示)、对象辅助类(创建新的类)、元素相关信息的辅助类。在下面我们会用到,继续看支持的注解类型:
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationsTypes = new LinkedHashSet<>();
annotationsTypes.add(PermissionFail.class.getCanonicalName());
annotationsTypes.add(PermissionSuccess.class.getCanonicalName());
annotationsTypes.add(ShowRequestPermissionRarionale.class.getCanonicalName());
return annotationsTypes;
}
可以发现,我们支持了自定义的三个注解,版本的话依然是当前最新版。重点是process,在这里它主要是做了两件事:
(1)将注解添加到对应的缓存结合中
(2)代码动态生成代理类(重点)
代码如下:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mProxyMap.clear();
mMessager.printMessage(Diagnostic.Kind.NOTE, "process...");
//从roundEnv中获取到PermissionSuccess注解的元素,并且校验作用域是否是METHOD,满足添加添加到相应的缓存集合中
if (!processAnnotations(roundEnv, PermissionSuccess.class)) {
return false;
}
...
//遍历生成代理类
for (String key : mProxyMap.keySet()) {
ProxyInfo proxyInfo = mProxyMap.get(key);
try {
JavaFileObject jfo = mFiler.createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement()
);
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.getTypeElement(), "Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
}
至于动态生成代理类,大致的逻辑是根据原始类名(比如MainActivity为例)+特定后缀($ $PermissionProxy)代码创建代理类(即MainActivity$ $PermissionProxy),然后再代码生成特定函数,此处是对应三个注解的函数–grant、denied、rationale,大致格式如下:
// Generated code. Do not modify!
package learn.cp.inject;
import learn.cp.lib.*;
public class MainActivity$$PermissionProxy implements PermissionProxy<MainActivity> {
@Override
public void grant(MainActivity source, int requestCode) {
switch (requestCode) {
case 1:
source.success();
break;
case 2:
source.callSuccess();
break;
}
}
@Override
public void denied(MainActivity source, int requestCode) {
switch (requestCode) {
case 1:
source.fail();
break;
case 2:
source.callFail();
break;
}
}
@Override
public void rationale(MainActivity source, int requestCode) {
switch (requestCode) {
}
}
@Override
public boolean needShowRationale(int requestCode) {
switch (requestCode) {
}
return false;
}
}
现在自定义注解有了,注解处理器也有了,剩下的就是调用了,具体的调用逻辑由我们的api模块来实现,此处具体为MPermissions
类,当我们使用时会调用它的requestPermissions
函数,在其内部会调用_requestPermissions
,它的实现基本上和PermissionGen一模一样,所以就不再赘述了,我们去看一下,它授权成功的处理逻辑:
private static void doExecuteSuccess(Object object,int requestCode){
findPermissionProxy(object).grant(object,requestCode);
}
private static PermissionProxy findPermissionProxy(Object object){
try {
Class<?> clazz = object.getClass();
Class injectorClazz = Class.forName(clazz.getName()+SUFFIX);
return ((PermissionProxy) injectorClazz.newInstance());
}catch (ClassNotFoundException e){
e.printStackTrace();
}catch (InstantiationException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
}
大致逻辑如下:
(1)通过反射获取目标对象的Class对象,此处还是MainActivity
(2)再一次反射,获取MainActivity的代理对象(即MainActivity$ $PermissionProxy)的Class对象
(3)获取即MainActivity$ $PermissionProxy实例
(4)调用其grant
这样就走到上面代码生成的逻辑里了。然后回调source对应的函数,这里的source就是我们最开始传入的MainActivity,这样就完成了关联。
失败的逻辑也是类似的,大家可以自己去研究一下。
参考源码以及文章
鸿洋-Android 6.0 运行时权限处理完全解析
编译时注解-MPermissions
运行时注解-PermissionGen
注解