学会自定义注解,看这就够了(1)---自定义运行时注解

前言

关于注解我相信大家都见的非常多了, 不论是Android或者Java自带的@Override等注解还是我们经常使用的一些开源库都包含注解,原因很简单,注解可以让代码更简洁,比如Retrofit、PermissionsDispatcher等库,但是自己定义注解确干的非常少。

所以这篇文章我们就来深挖以下自定义注解的使用,以及拿一个开源库来解析源码,剖析原理。

正文

先来了解一些关于注解的基本知识。

元注解

元注解是在定义自定义注解时需要用到,也就是注解的注解,比如我们自定义的注解需要用在什么地方、这个注解存活的时间等属性都是通过元注解来进行说明。

直接看下面总结

元注解.png

第一眼看时这东西未免有点多,难以记住,大可不必都记着,首先主要用到的就是@Target和@Retention这2个注解。

其中你想你的注解运用在什么地方,就使用@Target来说明;对于具体情况再区分使用运行时注解还是编译时注解,其中RUNTIME就是运行时注解,CLASS范围就是编译时注解,这2种注解区别很大,具体实现方式和侧重点也不一样,主要是下面几点:

编译时和运行时区别.png

哦,看到这里我们就大概懂了,一些开源库有时需要在添加注解后进行build才能使用一些生成的方法,这种就是编译时注解,通过生成新的文件来完成。

所以在考虑自定义什么类型注解是,也是从上面的区别来考虑,比如我需要在运行时拿到什么属性或者资源就使用运行时注解,比如我需要更快的性能,在编译器通过生成文件、使用生成文件的方法来完成需求则使用运行时注解。

接下来我们来看看这2种注解的使用,这里还是以解析库的原码来进行。

运行时注解

这里就介绍一个非常有名的运行时注解库的实现,就是EventBus,关于EventBus的使用这里就不过多解释了,具体查看项目地址:github.com/greenrobot/…

使用很方便在Android就是用于组件之间的通信,

EventBus-Publish-Subscribe.png

在一个地方调用post方法,然后注册订阅者便可以收到消息,其中的接收方法是使用注解来完成的,比如下面代码:

@Subscribe(threadMode = ThreadMode.MAIN,sticky = false,priority = 0)
public void onEventMainThread(TestFinishedEvent event) {
    Test test = event.test;
    String text = "<b>" + test.getDisplayName() + "</b><br/>" + //
            test.getPrimaryResultMicros() + " micro seconds<br/>" + //
            ((int) test.getPrimaryResultRate()) + "/s<br/>";
    if (test.getOtherTestResults() != null) {
        text += test.getOtherTestResults();
    }
    text += "<br/>----------------<br/>";
    textViewResult.append(Html.fromHtml(text));
}
复制代码

这里的onEventMainThread方法当有post这个类型的事件时便会收到,在这里我们先不讨论eventBus是如何进行消息发布和订阅的,主要来看一下这个@Subscribe这个注解。

根据前一节说的注解类型,运行时注解一般是通过反射来完成,那么这里就有了下面2个基本步骤:

运行时注解.png

声明注解

看一下EventBus中的@Subscribe注解的声明:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;

    boolean sticky() default false;

    int priority() default 0;
}
复制代码

从上面的元注解我们可以得知这个注解必须要使用在方法上,而且在运行时还保留。

同时注解的参数有3个,这里就要注意了,我们使用注解一般就是为了方便,让代码简洁,所以这里的参数一般不要太多,在使用时直接用逗号隔开即可:

@Subscribe(threadMode = ThreadMode.MAIN, sticky = false, priority = 0)
复制代码

那既然在一个类中我已经定义了订阅者,那啥时候来把这个订阅者添加到EventBus系统中呢,看一下官方使用文档:

@Override
 public void onStart() {
     super.onStart();
     EventBus.getDefault().register(this);
 }

 @Override
 public void onStop() {
     super.onStop();
     EventBus.getDefault().unregister(this);
 }

复制代码

这里直接在组件的生命周期开始和结束进行register,然后在这个组件里的订阅者便被添加到系统中,所以这里解析注解的步骤应该就是register方法中进行。

解析注解

直接看一下register()的代码:

public void register(Object subscriber) {
    Class<?> subscriberClass = subscriber.getClass();
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            subscribe(subscriber, subscriberMethod);
        }
    }
}
复制代码

我相信很多人看到这个getClass()方法获取的Class对象都有点惧怕,因为平时接触这玩意太少了,感觉这部分代码很难看懂,其实把它扒开了分析一波就不难了。

根据前面的思路,运行时注解就是找到注解,找到其参数,然后再干事,这里找到注解和参数很关键,其中就是这个Class类型,下面先说一下这个。

Class类型

直接看代码,比如这里的Activity,代码是:

//逻辑不用看,看有哪些方法
public class TestRunnerActivity extends Activity {

    private TestRunner testRunner;
    private EventBus controlBus;
    private TextView textViewResult;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_runtests);
        textViewResult = findViewById(R.id.textViewResult);
        controlBus = new EventBus();
        controlBus.register(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (testRunner == null) {
            TestParams testParams = (TestParams) getIntent().getSerializableExtra("params");
            testRunner = new TestRunner(getApplicationContext(), testParams, controlBus);

            if (testParams.getTestNumber() == 1) {
                textViewResult.append("Events: " + testParams.getEventCount() + "\n");
            }
            textViewResult.append("Subscribers: " + testParams.getSubscriberCount() + "\n\n");
            testRunner.start();
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN,sticky = false,priority = 0)
    public void onEventMainThread(TestFinishedEvent event) {
        Test test = event.test;
        String text = "<b>" + test.getDisplayName() + "</b><br/>" + //
                test.getPrimaryResultMicros() + " micro seconds<br/>" + //
                ((int) test.getPrimaryResultRate()) + "/s<br/>";
        if (test.getOtherTestResults() != null) {
            text += test.getOtherTestResults();
        }
        text += "<br/>----------------<br/>";
        textViewResult.append(Html.fromHtml(text));
        if (event.isLastEvent) {
            findViewById(R.id.buttonCancel).setVisibility(View.GONE);
            findViewById(R.id.textViewTestRunning).setVisibility(View.GONE);
            findViewById(R.id.buttonKillProcess).setVisibility(View.VISIBLE);
        }
    }

    public void onClickCancel(View view) {
        // Cancel asap
        if (testRunner != null) {
            testRunner.cancel();
            testRunner = null;
        }
        finish();
    }

    public void onClickKillProcess(View view) {
        Process.killProcess(Process.myPid());
    }

    public void onDestroy() {
        if (testRunner != null) {
            testRunner.cancel();
        }
        controlBus.unregister(this);
        super.onDestroy();
    }
}
复制代码

这里面的逻辑不用看,主要看这个类有哪些方法和字段、参数即可,那接下来就是获取这个类的Class类型,来看看常用的方法返回的都是些什么。

代码很简单,

//包名
String getName = subscriberClass.getName();
String getSimpleName = subscriberClass.getSimpleName();
//构造函数
Constructor<?> getCon = subscriberClass.getConstructor();
Constructor<?>[] getDeclCon = subscriberClass.getDeclaredConstructors();
//字段
Field[] getFields = subscriberClass.getFields();
Field[] getDeclFields = subscriberClass.getDeclaredFields();
//方法
Method[] getM = subscriberClass.getMethods();
Method[] getDeclM = subscriberClass.getDeclaredMethods();
//直接超类的type
Type getGenSuperClas = subscriberClass.getGenericSuperclass();
//当前类实现的接口
Class<?>[] getInter = subscriberClass.getInterfaces();
//修饰符
int getMod = subscriberClass.getModifiers();
复制代码

依次对应的方法和结果是:

Class类型.png

可以点击图片放大观看结果,通过这些方法我们就很容易得到一个类的信息,比如字段、方法等,然后再判断方法或者字段有没有添加注解啥的,思路很明确。

其实这么多方法记起来也不难,普通的getXXX方法是获取当前类和父类的public字段、方法,getDeclaredXXX方法是获取当前类声明的所有修饰符的字段、方法。

OK,按照思路,我们可以根据一个类的Class类型获取其中的方法,那再获取方法的注解以及处理即可,我们接着看。

找到注解信息

源码很多,但是我们思路很明确,根据Class找到方法,再进行处理,源码中对应的方法:

//通过反射
private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    //获取当前类声明的所有方法
    methods = findState.clazz.getDeclaredMethods();
    //遍历方法
    for (Method method : methods) {
        //获取方法的修饰符
        int modifiers = method.getModifiers();
        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
            //获取方法的参数类型
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 1) {
                //获取方法的注解
                Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                if (subscribeAnnotation != null) {
                    //获取第一个参数的类型
                    Class<?> eventType = parameterTypes[0];
                    if (findState.checkAdd(method, eventType)) {
                        //获取注解的信息
                        ThreadMode threadMode = subscribeAnnotation.threadMode();
                        //对注解的信息进行处理
                        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                    }
                }
            } 
        } 
    }
}
复制代码

其实上面代码有注释后还是非常容易读懂的,但是对于这种不经常用的代码在第一次接触时就比较头大,感觉API一点都不熟悉,其实这里每个API都可以根据其名字来判断其作用,最重要的方法就是:

Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
复制代码

通过这个获取注解信息,再获取注解里的参数和方法信息,后面进行处理。

具体处理逻辑不是本章文章核心,这里核心主要是运行时注解如何通过反射获取到注解信息。

总结

这一篇文章主要是通过EventBus库来说明运行时注解是如何生效的,其实原理非常简单,主要就是利用反射,虽然反射的API不经常用,但是看过理解一遍后还是很容易的,下一篇我们将通过Android的动态权限库PermissionsDispatcher来剖析编译时注解,包括使用、生成文件等,不容错过,喜欢的点个赞或者关注吧。

猜你喜欢

转载自juejin.im/post/7018858781736075294