背景
編譯時註解越來越多的出現在各大開源框架使用中,比如
square/dagger 依賴注入
類似這樣的庫在開發和工作中已經越來越多,它們旨在幫助我們在效率為前提的情況下幫助開發者快速開發,節約時間成本。而它們都使用了編譯時註解的思想。
正因為如此火熱,所以有必要好好學習其中的實現原理,方便解決因為編譯時註解導致的問題,同時可將此技術運用到自己的開源庫中
思想
編譯時註解框架在編寫時有相對固定的格式,分包為例
格式相對固定,但是也可以靈活變動,比如講api
和annotations
結合在一個moudel
裡
moudel中
的依賴關係也非常的固定
processors
依賴包有api
-annotations
app
依賴包有api
-annotations
-processors
其中除了app
是android moudel
以外,其他全部均是java moudel
annotations
註解
在講解annotations
註解之前,需要對java和android註解有大致的瞭解,可以參考我之前的部落格
先初始一個HelloWordAtion註解標註Target為ElementType.TYPE
修飾類物件
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HelloWordAtion {
String value();
}複製程式碼
一般一個註解需要對應一個註解處理器,註解處理器在processors
處理
processors
註解處理器
對應註解的處理器需要繼承AbstractProcessor
類,需要複寫以下4個方法:
init
init(ProcessingEnvironment processingEnv)
會被註解處理工具呼叫
process
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
這相當於每個處理器的主函式main(),你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。
getSupportedAnnotationTypes
etSupportedAnnotationTypes()
這裡必須指定,這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字串的集合,包含本處理器想要處理的註解型別的合法全稱
@return
註解器所支援的註解型別集合,如果沒有這樣的型別,則返回一個空集合
getSupportedSourceVersion
指定使用的Java
版本,通常這裡返回SourceVersion.latestSupported(),預設返回
SourceVersion.RELEASE_6 `
@return
使用的Java
版本
生成註解處理器
對AbstractProcessor
有了深入的瞭解,知道核心的初始編譯時編寫程式碼的方法及時process
,在process
中我們通過得到傳遞過來的資料,寫入程式碼,這裡先採用列印的方式,簡單輸出資訊,後續會詳細講解如何自己實現 butterknife
功能
public class HelloWordProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Filer是個介面,支援通過註解處理器建立新檔案
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(HelloWordAtion.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}複製程式碼
到這一步HelloWordAtion
對應的註解處理器已經編寫完成,這裡簡單的列印了HelloWordAtion
註解的class
和註解指定的value
資訊
準備工作完成以後,app
觸發呼叫
@HelloWordAtion("hello")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}複製程式碼
這裡註解註釋的類MainActivity
並且指定value
為hello
,到此準備工作就算完成了,這時如果你直接編譯或者執行工程的話,是看不到任何輸出資訊的,這裡還要做的一步操作是指定註解處理器的所在,需要做如下操作:
1、在 processors 庫的 main 目錄下新建 resources 資原始檔夾;
2、在 resources資料夾下建立 META-INF/services 目錄資料夾;
3、在 META-INF/services 目錄資料夾下建立 javax.annotation.process.Processors 檔案;
4、在 javax.annotation.process.Processors 檔案寫入註解處理器的全稱,包括包路徑;
經歷了以上步驟以後方可成功執行,但是實在是太複雜了,博主為了配置這一步也是搞了好久,所以這裡推薦使用開源框架AutoService
AutoService
直接在Processors
中依賴
compile 'com.google.auto.service:auto-service:1.0-rc2'複製程式碼
使用
@AutoService(Processor.class)
public class HelloWordProcessor extends AbstractProcessor {
xxxxxxx
}複製程式碼
到這裡執行程式便可以成功看到後臺的輸出資訊
需要切換到右下角的Gradle Console
視窗,如果變異不成功可以clean
工程以後重新執行
得到需要的資料,下一步當然是將資料寫入到java class
中,也就是題目所言的編譯時註解,如何才能寫入,這裡需要藉助Filer
類
Filer
在AbstractProcessor
的init
方法中初始Filer
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}複製程式碼
到此我們已經有了寫入的類的幫助類,還差程式碼生成邏輯,這裡介紹使用javapoet
javapoet
JavaPoet一個是建立 .java 原始檔的輔助庫,它可以很方便地幫助我們生成需要的.java 原始檔,GitHub
上面有非常詳細的用法,建議好好閱讀相關的使用
processors
依賴:
compile 'com.squareup:javapoet:1.8.0'複製程式碼
綜合上述的技術,仿照javapoet
的第一個Example
生成如下程式碼
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
// 建立main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg)
.build();
// 建立HelloWorld類
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.wzgiceman.viewinjector.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成檔案
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}複製程式碼
這裡重點講解process
方法,也就是寫入程式碼的方法體,我們在javapoet
的Example
基礎上將輸出資訊改為HelloWordAtion
註解獲取的資訊,到處便完全搞定編譯時註解的整個流程,clean
以後執行工程,在如下路徑下便可看到自動編譯生成的HelloWorld
類
到此簡單的編譯時註解就搞定了,但是編譯時註解的自動寫入也會導致程式碼混亂,可能在多次build
編譯過程中出現檔案衝突的情況,所以這裡需要引入android-apt
android-apt
android-apt
能在編譯時期去依賴註解處理器並進行工作,但在生成 APK 時不會包含任何遺留無用的檔案,輔助 Android Studio
專案的對應目錄中存放註解處理器在編譯期間生成的檔案
依賴使用:
根目錄build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'複製程式碼
app
中
apply plugin: 'com.neenbedankt.android-apt'
apt project(':processors')複製程式碼
這裡是apt
替換compile
依賴processors
總結
到此簡單的編譯時註解就搞定了,但是api
模組還沒有涉及,彆著急接下來的部落格中繼續擴充套件,運用掌握的編譯時註解和時下主流的butterknife
框架,實現一套自己的自定義注入框架中會詳細講解api
模組的使用,你會發現原來butterknife
很簡單,當然可以自由發散,擴充套件回到自己的任何開源專案中,替換掉反射提高效率。迫不及待的小夥伴可以去GitHub
下載原始碼先自行研究。