平常開發中可能不多會本身寫註解處理器,可是不少開源庫都用到了,如ButterKnife、EventBus、Glide
等。所以咱們必需要了解其原理,才能讀懂其餘大牛寫的代碼。java
通常狀況下編寫編譯時註解的項目時會分三個模塊:android
我的以爲若是註解不是特別多的話,仍是把註解模塊和 Api 模塊合二爲一更好,用戶引入的時候就會像下面這個樣子:git
annotationProcessor "com.test.processor"
implementation "com.test.api"
複製代碼
這樣看起來會更清爽,平白無故有多個依賴讓人感受有些麻煩。github
寫註解必需要知道元註解,尤爲是 @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;
複製代碼
編譯時註解往簡單的說就是在 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
註解類。
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 的體積,這多是編譯時註解的一個不足。
在 EventBus 3.0 以後也加入了編譯時註解,如下內容主要講解註解處理器是如何生成 Index 類,並經過使用編譯時生成的 Index 類來訂閱、分發事件的整個流程。
要想使用編譯時註解,須要在 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);
}
}
複製代碼
套路和前一節講的同樣。
先是收集信息,因爲 EventBus
的 Subscribe
註解只做用在方法上,所以只要使用一個集合,其 key
爲 全限定類名 或 TypeElement
(事實上Eventbus
是以TypeElement
爲key
),value
爲 ExecutableElement
方法節點列表。
而後是根據收集到的信息生成 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;
}
}
}
複製代碼
這裏簡單解釋下幾個類的做用。
SubscriberInfoIndex
接口,主要有兩個邏輯,一是保存以訂閱者 class
對象爲 key
,SubscriberInfo
爲 value
的集合;二是重寫 getSubscriberInfo
方法,將指定的 class
對象的 SubscriberInfo
返回出去。class
對象、訂閱者被註解修飾的方法、訂閱者父類的SubscriberInfo
。AbstractSubscriberInfo
, AbstractSubscriberInfo
則實現了SubscriberInfo
,SimpleSubscriberInfo
是 EventBus
默認惟一一個實現 SubscriberInfo
的類,可想而知它提供了讓你本身去編寫註解處理器和自定義 SubscriberInfo
的可能性。從它的實現上能看出 SimpleSubscriberInfo
內保存了訂閱者 class
對象、訂閱者方法信息等。SimpleSubscriberInfo
的一個成員變量,編譯時會把 @Subscribe
註解所修飾的方法名、形參類型、threadMode、priority、sticky
都解析出來,並保存到 SubscriberMethodInfo
中。最後須要調用提供的 api
,將 MyEventBusIndex
添加到其中:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
複製代碼
接下來看下 register
和 post
流程,主要看使用 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
事件分發就是根據參數分發到對應的方法上的,所以要保存以 eventType
爲 key
的 subscriptionsByEventType
集合,在以後的分發流程中會使用到。
分發流程
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
策略,咱們看下 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
反射調用到真實的方法,這裏就有疑問了,你這仍是用到了反射啊?其實不少框架是避免不了反射的,只是儘可能的少用反射能節省很多時間。
以上就是編譯時註解生成 MyEventBusIndex
, 而後 EventBus
訂閱分發的整個流程。下面用兩張圖總結下訂閱、分發兩個流程。
EventBus 類圖:
EventBus 時序圖(註解反射):
本文主要從編譯時註解爲核心,講述了編譯時註解的基礎以及如何編寫一個簡單的註解處理器,這對於閱讀使用到編譯時註解的開源庫源碼有很大的幫助。接着從 EventBus 3.0
的註解處理器開始分析,在瞭解了編譯時註解的基礎後能較容易的理解 MyEventBusIndex
類是如何生成的。而後繼續跟進分析了 EventBus.register
訂閱流程和 EventBus.post
事件分發的流程。
參考資料