Android 編譯時註解-初認識

wzgiceman發表於2017-01-13

背景

編譯時註解越來越多的出現在各大開源框架使用中,比如

JakeWharton/butterknife view

greenrobot/EventBus 事件

square/dagger 依賴注入

類似這樣的庫在開發和工作中已經越來越多,它們旨在幫助我們在效率為前提的情況下幫助開發者快速開發,節約時間成本。而它們都使用了編譯時註解的思想。

正因為如此火熱,所以有必要好好學習其中的實現原理,方便解決因為編譯時註解導致的問題,同時可將此技術運用到自己的開源庫中

思想

編譯時註解框架在編寫時有相對固定的格式,分包為例

Android 編譯時註解-初認識
這裡寫圖片描述

格式相對固定,但是也可以靈活變動,比如講apiannotations結合在一個moudel

moudel中的依賴關係也非常的固定

processors依賴包有api- annotations

app依賴包有 api -annotations-processors

其中除了appandroid moudel以外,其他全部均是java moudel

annotations註解

在講解annotations註解之前,需要對java和android註解有大致的瞭解,可以參考我之前的部落格

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)會被註解處理工具呼叫

Android 編譯時註解-初認識

process

process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)這相當於每個處理器的主函式main(),你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。

Android 編譯時註解-初認識

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並且指定valuehello,到此準備工作就算完成了,這時如果你直接編譯或者執行工程的話,是看不到任何輸出資訊的,這裡還要做的一步操作是指定註解處理器的所在,需要做如下操作:

  • 1、在 processors 庫的 main 目錄下新建 resources 資原始檔夾;

  • 2、在 resources資料夾下建立 META-INF/services 目錄資料夾;

  • 3、在 META-INF/services 目錄資料夾下建立 javax.annotation.process.Processors 檔案;

  • 4、在 javax.annotation.process.Processors 檔案寫入註解處理器的全稱,包括包路徑;

經歷了以上步驟以後方可成功執行,但是實在是太複雜了,博主為了配置這一步也是搞了好久,所以這裡推薦使用開源框架AutoService

AutoService

AutoService

直接在Processors中依賴

 compile 'com.google.auto.service:auto-service:1.0-rc2'複製程式碼

使用

@AutoService(Processor.class)
public class HelloWordProcessor extends AbstractProcessor {
xxxxxxx
}複製程式碼

到這裡執行程式便可以成功看到後臺的輸出資訊

Android 編譯時註解-初認識
這裡寫圖片描述

需要切換到右下角的Gradle Console視窗,如果變異不成功可以clean工程以後重新執行

得到需要的資料,下一步當然是將資料寫入到java class中,也就是題目所言的編譯時註解,如何才能寫入,這裡需要藉助Filer

Filer

AbstractProcessorinit方法中初始Filer

 private Filer filer;  

    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);    
        filer = processingEnv.getFiler();  
    }複製程式碼

到此我們已經有了寫入的類的幫助類,還差程式碼生成邏輯,這裡介紹使用javapoet

javapoet

JavaPoet一個是建立 .java 原始檔的輔助庫,它可以很方便地幫助我們生成需要的.java 原始檔,GitHub上面有非常詳細的用法,建議好好閱讀相關的使用

javapoet

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方法,也就是寫入程式碼的方法體,我們在javapoetExample基礎上將輸出資訊改為HelloWordAtion註解獲取的資訊,到處便完全搞定編譯時註解的整個流程,clean以後執行工程,在如下路徑下便可看到自動編譯生成的HelloWorld

Android 編譯時註解-初認識
這裡寫圖片描述

到此簡單的編譯時註解就搞定了,但是編譯時註解的自動寫入也會導致程式碼混亂,可能在多次build編譯過程中出現檔案衝突的情況,所以這裡需要引入android-apt

android-apt

android-apt能在編譯時期去依賴註解處理器並進行工作,但在生成 APK 時不會包含任何遺留無用的檔案,輔助 Android Studio專案的對應目錄中存放註解處理器在編譯期間生成的檔案

android-apt

依賴使用:

根目錄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下載原始碼先自行研究。


專欄

註解-編譯執行時註解


原始碼

下載原始碼


建議

如果你有任何的問題和建議歡迎加入QQ群告訴我!

相關文章