EventBus 3.0 從編譯時註解分析源碼

1、編譯時註解基礎

平常開發中可能不多會本身寫註解處理器,可是不少開源庫都用到了,如ButterKnife、EventBus、Glide等。所以咱們必需要了解其原理,才能讀懂其餘大牛寫的代碼。java

通常狀況下編寫編譯時註解的項目時會分三個模塊:android

  • 註解模塊:annotation module
  • 註解處理器模塊:processor module
  • Api模塊:api module

我的以爲若是註解不是特別多的話,仍是把註解模塊和 Api 模塊合二爲一更好,用戶引入的時候就會像下面這個樣子:git

annotationProcessor "com.test.processor"
implementation "com.test.api"
複製代碼

這樣看起來會更清爽,平白無故有多個依賴讓人感受有些麻煩。github

1. 註解模塊

寫註解必需要知道元註解,尤爲是 @Retention@Target ,這裏簡單介紹下。api

**@Retention **有三個枚舉類型:緩存

RetentionPolicy.SOURCE 表示註解只保留在源文件,當 Java 文件編譯成 class 文件的時候註解被遺棄;bash

RetentionPolicy.CLASS 表示註解被保留到 class 文件,當 JVM 加載 class 文件的時候被遺棄。數據結構

RetentionPolicy.RUNTIME 表示 JVM 加載 class 文件後依然存在,在運行過程當中能夠在任意時間被調用。app

RetentionPolicy.RUNTIME 比較容易理解,它就是用在爲反射而生的。另外兩個均可以用於編譯時註解。框架

@Target 有九種枚舉類型:

TYPE 做用於接口、類、枚舉。

FIELD 做用於字段、變量。

METHOD 做用於方法。

PARAMETER 做用於形參。

CONSTRUCTOR 做用於構造方法。

LOCAL_VARIABLE 做用於局部變量。

ANNOTATION_TYPE 做用於註解。

PACKAGE 做用於包名。

TYPE_PARAMETER 做用於類型參數(如泛型、類型轉換)。

TYPE_USE 做用於類型使用時。

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestField {
  String value();
}
複製代碼

以上代碼就經過了元註解實現了註解,TestField 能註解在變量上,而且只保留在 class 文件,運行後這個註解就會消失。使用以下:

@TestField("hello")
String value;
複製代碼

2. 註解處理器模塊

編譯時註解往簡單的說就是在 Java 代碼編譯成 class 字節碼的過程當中執行註解處理器並生成你須要的 Java 文件。

因此第一個問題就是如何在編譯時執行?答案是繼承 AbstractProcessor 就能夠了,編譯器會自動尋找繼承 AbstractProcessor 的類,並調用它的 process方法,通常來講咱們會重寫如下幾個方法:

public class TestProcessor extends AbstractProcessor {
  @Override
  public Set<String> getSupportedAnnotationTypes(){
    Set<String> annotationTypes = new LinkedHashSet<String>();
    annotationTypes.add(TestField.class.getCanonicalName());
    return annotationTypes;
  }
  @Override
  public SourceVersion getSupportedSourceVersion(){
    return SourceVersion.latestSupported();
  }
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){}
}
複製代碼

getSupportedAnnotationTypes 方法添加咱們須要解析的註解,getSupportedSourceVersion 方法返回最新的版本支持便可。主要的代碼處理在 process 方法,繼續實現上一小節的註解,咱們能夠把 TestField 註解的值賦給 value 變量,這個過程當中咱們須要先檢測有此註解的變量並保存到一個集合中,而後再拿集合內的信息去生成 Java 代碼。先來看下蒐集信息的並保存到集合的代碼:

private Map<String, ClassInfo> classInfos = new HashMap<>();

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
  classInfos.clear();
  for(TypeElement annotation: annotations){
    // 獲取一個類中全部節點,這裏能夠是域、方法、類節點等等。
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
    for(Element element : elements) {
      // 找到變量節點
      if(element instanceof VariableElement){
        // 獲取變量所在類的全限定類名
        String qualifiedClassName = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
        ClassInfo classInfo = classInfos.get(qualifiedClassName);
        if(classInfo == null){
          classInfo = new ClassInfo();
        }
        classInfo.qualifiedClassName = qualifiedClassName;
        classInfo.typeElement = (TypeElement)element.getEnclosingElement();
        classInfo.variableElements.add(element);
        // 以全限定類名做爲key可保證惟一性
        classInfos.put(qualifiedClassName, classInfo);
      }else {
        // 在本例中TestField只修飾全部域,所以若是出現不是「變量節點」的話就拋異常吧
      }
    }
  }
}

/**
* 保存一個類文件中咱們所須要的信息
*/
public static final class ClassInfo {
  String qualifiedClassName; //全限定類名
  TypeElement typeElement; // 類節點
  List<VariableElement> variableElements = new ArrayList<>(); // 一個類中全部有該註解的變量節點
  
  // 獲取非限定類名
  public String getClassName(){
    if(qualifiedClassName == null){
      return null;
    }
    return qualifiedClassName.substring(qualifiedClassName.lastIndexof(".") + 1, qualifiedClassName.length());
  }
}
複製代碼

簡單介紹下 Element 的子類:

  • VariableElement:通常表明成員變量。
  • ExecutableElement :通常表明類中的方法。
  • TypeElement :通常表明表明類。
  • PackageElement :通常表明Package。

上面的代碼註釋也比較清晰了,就是每一個有指定註解的類會被遍歷到,而後把它裏面全部 VariableElement 保存起來

第二個步驟就是把已經收集的信息生成對應的 Java 文件。

private static final String SUFFIX = "$ITest";
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
  // 收集信息的代碼...
  // 開始生成Java文件
  for(ClassInfo classInfo : classInfos.values()){
    try{
      // 建立java文件對象,全限定類名+指定後綴
      JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(classInfo.qualifiedClassName + SUFFIX. classInfo.typeElement);
      // 開始寫入代碼
      Writer writer = sourceFile.openWriter();
      writer.write(generateCode(classInfo));
      writer.flush();
      writer.close();
    }catch (IOException e){
      e.printStackTrace();
    }
  }
  return true;
}

public String generateCode(ClassInfo classInfo){
  StringBuilder builder = new StringBuilder();
  // 獲取包名
  String packageName = processEnv.getElementUtils().getPackageOf(classInfo.typeElement).getQualifiedName().toString();
  builder.append("package " + packageName + ";\n")
  		 .append("import com.test.api.*;\n")
  		 .append("public class " + classInfo.getClassName() + SUFFIX + " implements ")
  		 .append("ITest<" + classInfo.qualifiedClassName + ">{\n")
  		 .append(" public void inject(" + classInfo.qualifiedClassName + " host){\n")
  		 .append(generateInject(classInfo))
  		 .append("\n }")
  		 .append("\n}");
}

public String generateInject(ClassInfo classInfo){
  StringBuilder builder = new StringBuilder();
  for(VariableElement element : classInfo.variableElements){
    TestField test = element.getAnnotation(TestField.class);
    if(test != null){
      // 獲取註解上的值
      int value = test.value();
      String variableName = element.getSimpleName().toString();
      // 給變量賦值
      builder.append(" host." + elementName + "=" + value + "\n");
    }
  }
  return builder.toString();
}
複製代碼

這段代碼建立了 Java 文件,而後在裏面使用收集到的信息拼接字符串,很是容易理解。若是複雜一些的項目能夠考慮使用 javapoet 庫。

最後在須要在 src/main/ 下新建 resources文件夾,再新建 META-INF.services文件夾,在此文件夾內新建javax.annotation.processing.Processor 文件,在文件內寫入你的註解器的全限定類名,如:

com.test.processor.TestProcessor
複製代碼

這樣註解處理器就註冊成功了,在編譯器會自動執行到這個註解。不過還有更簡單的一種方式,在build.gradle下加入如下依賴:

compile 'com.google.auto.service:auto-service:1.0-rc4'
複製代碼

而後在自定義註解處理器的類上加上以下代碼:

@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
  
}
複製代碼

這樣就會自動註冊 TestProcessor 註解類。

3. Api模塊

Api 模塊就是提供給開發者調用以前註解處理器生成的代碼。

在這個示例中咱們提供一個接口,全部生成的 Java 類都會實現這個接口,方便統一調用。就是上面生成代碼中已經出現的 ITest

public interface ITest<T> {
  void inject(T obj);
}
複製代碼

最終寫一個 app 可調用的方法:

public class TestApi {
  public static void inject(Object obj){
    Class<?> clazz = obj.getClass();
    String proxyName = clazz.getName() + "$ITest";
    // 省略 try catch
    Class<?> proxyClazz = Class.forName(proxyName);
    ITest test = (ITest) proxyClazz.newInstance();
    test.inject(obj);
  }
}
複製代碼

app 內使用以下:

public class MainActivity extends AppCompatActivity {
  @TestField(2)
  public int value;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //.....
    TestApi.inject(this);
  }
}
複製代碼

使用也很簡單,就是在變量上使用了 @TestField 註解,而後調用 TestApi.inject(this) 去調用已經生成的 Java 類。最後看下編譯時咱們生成的 Java 文件是怎樣的,其位置位於build/generated/source/apt/

package com.test.project;
import com.test.api.*;
public class MainActivity$ITest implements ITest<com.test.project.MainActivity>{
  public void inject(com.test.project.MainActivity host){
    host.value = 2;
  }
}
複製代碼

因此在調用 inject 方法以後,MainActivity 中的 value 就被賦值爲 2 了。

看起來編譯時註解能作不少事情,並且把操做放在編譯期就不會拖慢程序運行時的速度,因此不少框架採起這種方式代替註解反射。不過一樣的,註解處理器生成的類也會增大 app 的體積,這多是編譯時註解的一個不足。

2、EventBus源碼分析

在 EventBus 3.0 以後也加入了編譯時註解,如下內容主要講解註解處理器是如何生成 Index 類,並經過使用編譯時生成的 Index 類來訂閱、分發事件的整個流程。

1. MyEventBusIndex

要想使用編譯時註解,須要在 build.gradle 內添加以下腳本:

android {
  defaultConfig {
    javaCompileOptions {
       annotationProcessorOptions {
         arguments = [ eventBusIndex : 'com.example.myapplication.MyEventBusIndex' ]
       }
    }
  }
}
dependencies {
  implementation 'org.greenrobot:eventbus:3.1.1'
  annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1'
}
複製代碼

annotationProcessor 依賴註解處理器沒什麼問題,那麼 arguments 這個參數又是什麼用處呢?咱們帶着問題去看下註解處理器的代碼:

public class EventBusAnnotationProcessor extends AbstractProcessor {
    public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex";
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
       // ...... 
       // 這裏拿到了gradle內的配置,也就是 com.example.myapplication.MyEventBusIndex
       String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
       // 收集節點信息並保存
       collectSubscribers(annotations, env, messager);
       // 生成 Java 文件
       createInfoIndexFile(index);
    }
}
複製代碼

套路和前一節講的同樣。

先是收集信息,因爲 EventBusSubscribe 註解只做用在方法上,所以只要使用一個集合,其 key 爲 全限定類名 或 TypeElement(事實上Eventbus是以TypeElementkey),valueExecutableElement 方法節點列表。

而後是根據收集到的信息生成 Java 文件,這個文件的路徑是 com.example.myapplication.MyEventBusIndex。這裏就再也不詳細展開,想詳細瞭解能夠去官方文檔裏看下源碼,理解了編譯時註解基礎後這些代碼是比較容易理解的。假設咱們在 MainActivity 中某個方法上作了以下註解:

@Subscribe
public void testMethod(TestEvent event){
}
複製代碼

那麼在build/generated/source/apt/生成的 MyEventBusIndex 類就是以下:

public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
        putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("testMethod", TestEvent.class),
        }));

    }
    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }
    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}
複製代碼

這裏簡單解釋下幾個類的做用。

  • MyEventBusIndex:實現了 SubscriberInfoIndex 接口,主要有兩個邏輯,一是保存以訂閱者 class 對象爲 keySubscriberInfovalue 的集合;二是重寫 getSubscriberInfo 方法,將指定的 class 對象的 SubscriberInfo 返回出去。
  • SubscriberInfo:一個接口,能提供訂閱者 class 對象、訂閱者被註解修飾的方法、訂閱者父類的SubscriberInfo
  • SimpleSubscriberInfo :繼承了 AbstractSubscriberInfoAbstractSubscriberInfo 則實現了SubscriberInfoSimpleSubscriberInfoEventBus 默認惟一一個實現 SubscriberInfo 的類,可想而知它提供了讓你本身去編寫註解處理器和自定義 SubscriberInfo 的可能性。從它的實現上能看出 SimpleSubscriberInfo 內保存了訂閱者 class 對象、訂閱者方法信息等。
  • SubscriberMethodInfo:是 SimpleSubscriberInfo 的一個成員變量,編譯時會把 @Subscribe 註解所修飾的方法名、形參類型、threadMode、priority、sticky 都解析出來,並保存到 SubscriberMethodInfo 中。

最後須要調用提供的 api ,將 MyEventBusIndex 添加到其中:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
複製代碼

2. 訂閱分發流程

接下來看下 registerpost 流程,主要看使用 MyEventBusIndex 類的邏輯,本文不涉及註解反射的邏輯。

訂閱流程

public void register(Object subscriber) {
    // 獲取訂閱者的 class 對象
    Class<?> subscriberClass = subscriber.getClass();
    // 經過subscriberMethodFinder解析出這個訂閱者被訂閱的全部方法
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            // 把訂閱信息保存到集合中
            subscribe(subscriber, subscriberMethod);
        }
    }
}
複製代碼

接下來走到 subscriberMethodFinder 裏是如何解析出被 @Subscribe 註解的方法:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
	// 先從緩存中取,所以即便是註解反射也不會耗費多少時間
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    if (ignoreGeneratedIndex) {
    	// 經過註解反射獲取被訂閱的方法
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
    	// 經過編譯時註解生成的 MyEventBusIndex 獲取被訂閱的方法
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    // ......
    METHOD_CACHE.put(subscriberClass, subscriberMethods);
    return subscriberMethods;
}
複製代碼

其實即便使用反射也並不會耗費多少時間,由於 EventBus 會只會在第一次使用時反射,以後都使用緩存。跳過反射部分,直接看 findUsingInfo 方法:

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        findState.subscriberInfo = getSubscriberInfo(findState);
        // ......
    }
    // ......
}
複製代碼

FindState 類能夠理解爲保存了 SubscriberInfo 、SubscriberMethod、class對象等信息,在以後會使用到。關鍵在於 getSubscriberInfo 方法。

private SubscriberInfo getSubscriberInfo(FindState findState) {
    // ......
    if (subscriberInfoIndexes != null) {
        for (SubscriberInfoIndex index : subscriberInfoIndexes) {
            SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
            if (info != null) {
                return info;
            }
        }
    }
    return null;
}
複製代碼

subscriberInfoIndexes 是一個 List 數據結構,咱們以前調用 EventBus.builder().addIndex(new MyEventBusIndex()) 其實就是將 MyEventBusIndex 添加到 subscriberInfoIndexes 中,這個時候咱們就能夠取出訂閱者class對象對應的 SubscriberInfo,還記得它保存了訂閱者被註解 @Subscribe 所修飾的方法。最終會經過 SubscriberInfo 返回對應的方法列表,咱們再回到 register 方法,在拿到訂閱方法列表後,調用 subscribe 方法:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    Class<?> eventType = subscriberMethod.eventType;
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    } 
    // ...
}
複製代碼

eventType 是被訂閱方法的參數的 class 對象,EventBus 事件分發就是根據參數分發到對應的方法上的,所以要保存以 eventTypekeysubscriptionsByEventType 集合,在以後的分發流程中會使用到。

分發流程

public void post(Object event) {
	// 某個線程都會有本身的PostingThreadState
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    // 保證消息能按添加順序分發
    eventQueue.add(event);
    // isPosting標誌位防止屢次分發
    if (!postingState.isPosting) {
        postingState.isMainThread = isMainThread();
        postingState.isPosting = true;
        try {
            while (!eventQueue.isEmpty()) {
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}
複製代碼

這段方法核心就是從消息隊列中取 event 消息而後調用 postSingleEvent 方法,postSingleEvent 方法內部主要是對父類對象 eventType 的檢查,默認是開啓父類檢查的,若是想要加快事件分發的速度並且不須要分發給父類,能夠考慮把標誌位改成不檢查父類,接着會調用 postToSubscription 方法,

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, event);
            break;
        case MAIN:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case MAIN_ORDERED:
            if (mainThreadPoster != null) {
                mainThreadPoster.enqueue(subscription, event);
            } else {
                // temporary: technically not correct as poster not decoupled from subscriber
                invokeSubscriber(subscription, event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, event);
            break;
    }
}
複製代碼

這裏有 5 種 threadMode

  • POSTING:分發事件在哪一個線程,訂閱者的方法就會在哪一個線程直接被調用。
  • MAIN:不管分發事件在主線程仍是子線程,方法會在主線程中被調用。
  • MAIN_ORDERED:方法會在主線程中被調用,並且方法被調用的順序和事件分發的順序一致。
  • BACKGROUND:若是事件分發在主線程,方法調用則會在子線程,若是事件分發已經在子線程了,那麼直接在這個線程內調用方法。
  • ASYNC:方法調用必定會在另外一個線程中。

默認是 POSTING 策略,咱們看下 invokeSubscriber 方法作了什麼:

void invokeSubscriber(Subscription subscription, Object event) {
    try {
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    }
}
複製代碼

這裏再熟悉不過了,經過 method.invoke 反射調用到真實的方法,這裏就有疑問了,你這仍是用到了反射啊?其實不少框架是避免不了反射的,只是儘可能的少用反射能節省很多時間。

3. 小結

以上就是編譯時註解生成 MyEventBusIndex, 而後 EventBus 訂閱分發的整個流程。下面用兩張圖總結下訂閱、分發兩個流程。

EventBus 類圖:

eventbus_class

EventBus 時序圖(註解反射):

eventbus_timeline

3、總結

本文主要從編譯時註解爲核心,講述了編譯時註解的基礎以及如何編寫一個簡單的註解處理器,這對於閱讀使用到編譯時註解的開源庫源碼有很大的幫助。接着從 EventBus 3.0 的註解處理器開始分析,在瞭解了編譯時註解的基礎後能較容易的理解 MyEventBusIndex 類是如何生成的。而後繼續跟進分析了 EventBus.register 訂閱流程和 EventBus.post 事件分發的流程。

參考資料

Android 如何編寫基於編譯時註解的項目

EventBus官方文檔

相關文章
相關標籤/搜索