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

语言: CN / TW / HK

前言

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

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

正文

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

元注解

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

直接看下面总结

元注解.png

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

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

编译时和运行时区别.png

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

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

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

运行时注解

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

使用很方便在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来剖析编译时注解,包括使用、生成文件等,不容错过,喜欢的点个赞或者关注吧。