學會自定義註解,看這就夠了(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來剖析編譯時註解,包括使用、生成檔案等,不容錯過,喜歡的點個贊或者關注吧。