1. 程式人生 > >ButterKnife原理分析(二)註解的處理

ButterKnife原理分析(二)註解的處理

本文同步發表於我的微信公眾號 hesong,掃一掃文章底部的二維碼或在微信搜尋 hesong 即可關注。

上一篇我們講解了ButterKnife的設計思想,理解了ButterKnife繫結相關原始碼的實現邏輯。但是它是怎麼通過註解的方式生成的那些邏輯程式碼,這才是最讓我們迫切想知道,因此在這篇,我將說說ButterKnife中註解處理的原理。本篇主要有以下內容:
1. 註解Annotation
2. 註解處理器AbstractProcessor
3. AutoService註冊註解處理器
4. JavaPoet生成Java程式碼
5. Element元素相關
6. 編譯時註解解析
7. 例子專案

註解(Annotation)

註解(Annotation)在Java中已經是很普遍的使用了,它其實就是一種標記資訊,然後程式在編譯或者執行的時候可以讀取這個標記資訊,去執行特定的邏輯,比如@BindView(R.id.tv_text) TextView tvText,程式在編譯時會讀取到這個@BindView註解,解析出它的值R.id.tv_text,再根據它註解的這個tvText,就可以生成類似tvText = (TextView)findViewById(R.id.tv_text);的功能程式碼。
註解按生命週期可以分為

  • RetentionPolicy.SOURCE(原始碼註解),只在原始碼中存在,在編譯時會被丟棄,通常用於檢查性的操作,如@Override。
  • RetentionPolicy.CLASS(編譯時註解),在編譯後的class檔案中依然存在,通常用於編譯時處理,如ButterKnife的@BindView。
  • RetentionPolicy.RUNTIME(執行時註解),不僅在編譯後的class檔案中存在,在被jvm虛擬機器載入之後,仍然存在,通常用於執行時處理,如Retrofit的@Get。

同時註解按使用的物件可以分為

  • ElementType.TYPE(型別註解),標記在介面、類、列舉上。
  • ElementType.FIELD(屬性註解),標記在屬性欄位上。
  • ElementType.METHOD(方法註解),標記在方法上。
  • ElementType.PARAMETER(方法引數註解),標記在方法引數上。
  • ElementType.CONSTRUCTOR(構造方法註解),標記在構造方法上。
  • ElementType.LOCAL_VARIABLE(本地變數註解),標記在本地變數上。
  • ElementType.ANNOTATION_TYPE(註解的註解),標記在註解上。
  • ElementType.PACKAGE(包註解),標記在包上。
  • ElementType.TYPE_PARAMETER(型別引數註解,Java1.8加入),標記型別引數上。
  • ElementType.TYPE_USE(型別使用註解,Java1.8加入),標記在類的使用上。

我們首先來認識ButterKnife的一個自定義屬性註解@BindView

/**
 * 作用於View的註解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命週期到類的編譯時期
 * @Target(ElementType.FIELD) 表示註解作用在欄位上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

它可以保留在類編譯之後,使用場景是作用在屬性上

關於註解的詳細介紹檢視Java註解基礎概念總結

註解處理器AbstractProcessor

註解只是一種標記資訊,所以需要我們自己去處理註解,註解的處理有編譯時註解處理和執行時註解處理。執行時註解,我們可以通過反射獲取註解資訊,進而進行相應處理。而編譯時註解就需要使用註解處理器(Annotation Processor)進行處理。那什麼是註解處理器?

註解處理器是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這些生成的Java程式碼是在新生成的.java檔案中,所以你不能修改已經存在的Java類,例如向已有的類中新增方法。這些生成的Java檔案,會同其他普通的手動編寫的Java原始碼一樣被javac編譯。
要實現一個註解處理器需要繼承AbstractProcessor,並重寫它的4個方法,同時必須要有一個無參的構造方法,以便註解工具能夠對它進行初始化。

public class ViewBindProcessor extends AbstractProcessor {  
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  

        //提供給註解處理器使用的工具類
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }  

    @Override  
    public Set<String> getSupportedAnnotationTypes() {  
        //新增需要處理的註解
        Set<String> annotataions = new LinkedHashSet<String>();  
        annotataions.add(MyAnnotation.class.getCanonicalName());  
        return annotataions;  
    }  

    @Override  
    public SourceVersion getSupportedSourceVersion() {
        //指定使用的Java版本
        return SourceVersion.latestSupported();  
    } 

     @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
        //這裡實現註解的處理,重點方法
        return false;  
    }  
}  
  • init,會被註解處理工具呼叫,引數ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
  • getSupportedAnnotationTypes(),指定註解處理器要處理哪些註解,返回一個字串集合,包含要處理註解的全名。
  • getSupportedSourceVersion, 指定使用的Java版本,通常這裡返回SourceVersion.latestSupported()
  • process,相當於每個處理器的main函式,在這裡可以做掃描、評估和處理註解程式碼的操作,以及生成Java檔案。

那麼init方法中ProcessingEnvironment提供的Elements,Types,Filer,Messager 等是做什麼用的呢?
- Elements:用來處理程式元素的工具類
- Types:用來處理型別資料的工具類
- Filer:用來給註解處理器建立檔案
- Messager:用來給註解處理器報告錯誤,警告,提示等訊息。

AutoService註冊註解處理器

以前要註冊註解處理器是要在module的META-INF目錄下新建services目錄,並建立一個名為javax.annotation.processing.Processor的檔案,然後在檔案中寫入要註冊的註解處理器的全名,例如在javax.annotation.processing.Processor的檔案中加上

com.pinery.compile.ViewBindProcessor

來註冊ViewBindProcessor註解處理器。

後來Google推出了通過新增AutoService註解庫來實現註解處理器的註冊,通過在你的註解處理器上加上@AutoService(Processor.class)註解,即可在編譯時生成 META-INF/services/javax.annotation.processing.Processor 檔案。
配置AutoService需要在工程的build.gradle中新增

JavaPoet生成Java程式碼

JavaPoet是Square公司出品的生成Java原始檔庫,正如其名,會寫Java程式碼的詩人,使用它的一系列API就可以很方便的生成java原始碼了。

JavaPoet中有幾個常用的類:
- MethodSpec,代表一個建構函式或方法宣告。
- TypeSpec,代表一個類,介面,或者列舉宣告。
- FieldSpec,代表一個成員變數,一個欄位宣告。
- JavaFile,包含一個頂級類的Java檔案。

這是一個計算從1到100相加的方法

public static int caculateNum() {
    int result = 0;
    for(int i = 1; i < 100; i++) {
      result = result + i;
    }
    return result;
}

我們用MethodSpec實現這個方法宣告

MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
      .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
      .addStatement("result = result $L i", "+")
      .endControlFlow()
      .addStatement("return result")
      .build();

可以發現,通過addModifiers新增方法修飾符,returns來定義方法的返回值型別,addStatement來新增方法中的一行語句,它會處理分號和換行,beginControlFlow和endControlFlow構成一個封閉的控制語段,適用於if,for,while等。 L S for Strings,代表一個字串
- TforTypes使import N for Names,代表我們自己生成的方法名或者變數名等等

例如

addStatement("$T.out.println($S)", System.class, "Hello World"))

這是定義的一個屬性

private final String name = "Pinery";

我們用MethodSpec實現這個方法宣告

FieldSpec nameField = FieldSpec.builder(String.class, "name")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S", "Pinery")
    .build();

下面是一個類的定義

public class MyClass{

    private final String name = "Pinery";

    public static int caculateNum() {
        int result = 0;
        for(int i = 1; i < 100; i++) {
          result = result + i;
        }
        return result;
    }
}

我們用TypeSpec實現這個方法宣告

TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
    .addModifiers(Modifier.PUBLIC)
    .addField(nameField)
    .addMethod(caculateMethod)
    .build();

通過TypeSpec添加了屬性實現和方法實現,其他常用的還有
- addTypeVariable,新增泛型宣告
- addSuperinterface,新增介面實現
- addJavadoc,添加註釋
- interfaceBuilder,生成一個介面

等,詳細使用可以參考JavaPoet官方文件

Element元素相關

註解處理工具掃描java原始檔,原始碼中的每一部分都是程式中的Element元素,如包,類,方法,欄位等。每一個元素代表一個靜態的,語言級別的結構。原始碼其實是一種結構化的文字(例如json文字就是一種結構化的文字),因此需要對它進行解析,解析的話,解析器會解析某些資訊代表某些結構,例如原始碼中的類宣告資訊代表TypeElement型別元素,方法宣告資訊代表代表ExecutableElement型別元素。有了這些結構,就能完整的表示整個原始碼資訊了。
Element元素分為以下型別:

  • ExecutableElement: 可執行元素,包括類或介面的方法、構造方法或初始化程式
  • PackageElement: 包元素,提供對有關包及其成員的資訊的訪問
  • TypeElement: 類或介面元素,提供對有關型別及其成員的資訊的訪問
  • TypeParameterElement: 表示一般類、介面、方法或構造方法元素的形式型別引數,型別引數宣告一個 TypeVariable
  • VariableElement: 表示一個欄位、enum常量、方法或構造方法引數、區域性變數或異常引數。

Element元素還有個asType()可以獲取元素型別TypeMirror,TypeMirror有以下具體型別:

  • ArrayType: 表示一個數組型別。多維陣列型別被表示為元件型別也是陣列型別的陣列型別。
  • DeclaredType: 表示某一宣告型別,是一個類 (class) 型別或介面 (interface) 型別。這包括引數化的型別(比如 java.util.Set)和原始型別。
  • ErrorType: 表示無法正常建模的類或介面型別。這可能是處理錯誤的結果。大多數對於派生於這種型別(比如其成員或其超型別)的資訊的查詢通常不會返回有意義的結果。
  • ExecutableType: 表示 executable 的型別。executable 是一個方法、構造方法或初始化程式。
  • NoType: 在實際型別不適合的地方使用的偽型別。NoType 的種類有:
    • VOID:對應於關鍵字 void。
    • PACKAGE:包元素的偽型別。
    • NONE:用於實際型別不適合的其他情況中;例如,java.lang.Object 的超類。
  • NullType: 表示 null 型別。此類表示式 null 的型別。
  • PrimitiveType: 表示一個基本型別。這些型別包括 boolean、byte、short、int、long、char、float 和 double。
  • ReferenceType: 表示一個引用型別。這些型別包括類和介面型別、陣列型別、型別變數和 null 型別。
  • TypeVariable: 表示一個型別變數。型別變數可由某一型別、方法或構造方法的型別引數顯式宣告。
  • WildcardType: 表示萬用字元型別引數。

編譯時註解解析

有了上面知識點的瞭解之後,下面就可以進行編譯時的註解解析了,需要以下幾個步驟:
1. 定義註解
2. 定義一個繼承自AbstractProcessor的註解處理器,重寫它4個方法中
3. 使用AutoService註冊自定義的註解處理器
4. 實現process方法,在這裡處理註解
5. 處理所有註解,得到TypeElement和註解資訊等資訊
6. 使用JavaPoet生成新的Java類

在annotations的moudle中定義一個註解

/**
 * 作用於View的註解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命週期到類的編譯時期
 * @Target(ElementType.FIELD) 表示註解作用在欄位上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

在compile的moudle中定義一個ViewBindProcessor註解處理器

/**
 * 自定義的AbstractProcessor,用於編譯時處理註解
 */
@AutoService(Processor.class) //新增AutoService註解,自動註冊ViewBindProcessor註解處理器
public class ViewBindProcessor extends AbstractProcessor{

    private Map<TypeElement, List<ViewBindInfo>> bindMap = new HashMap<>();

    //用來處理型別資料的工具類
    private Types typeUtils;
    //用來處理程式元素的工具類
    private Elements elementUtils;
    //用來給註解處理器建立檔案
    private Filer filer;
    //用來給註解處理器報告錯誤,警告,提示等訊息。
    private Messager messager;

    /**
     * 會被註解處理工具呼叫,引數ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    /**
     * 指定註解處理器是註冊給那一個註解的,它是一個字串的集合,意味著可以支援多個型別的註解,並且字串是合法全名。
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();

        //新增自定義的BindView註解
        annotataions.add(BindView.class.getCanonicalName());

        return annotataions;
    }

    /**
     * 指定使用的Java版本,通常這裡返回SourceVersion.latestSupported()
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 相當於每個處理器的main函式,在這裡可以做掃描、評估和處理註解程式碼的操作,以及生成Java檔案。
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        collectBindViewAnnotations(roundEnvironment);

        generateJavaFilesWithJavaPoet();

        return false;
    }

    /**
     * 收集BindView註解
     * @param roundEnvironment
     * @return
     */
    private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
        //查詢所有添加了註解BindView的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if(elements == null || elements.isEmpty()){
            return false;
        }

        for(Element element : elements){
            //註解BindView必須新增在屬性上
            if(element.getKind() != ElementKind.FIELD){
                error(element, "只有類的屬性可以新增@%s註解", BindView.class.getCanonicalName());
                return false;
            }

            //獲取註解的值
            int viewId = element.getAnnotation(BindView.class).value();
            //這個元素是屬性型別的元素
            VariableElement viewElement = (VariableElement) element;
            //獲取直接包含屬性元素的元素,即類元素
            TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();

            //將型別元素作為key,儲存到bindMap暫存
            List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
            if(viewBindInfoList == null){
                viewBindInfoList = new ArrayList<>();
                bindMap.put(typeElement, viewBindInfoList);
            }

            info("註解資訊:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());

            viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
        }

        return true;
    }

    /**
     * 生成註解處理之後的Java檔案
     */
    private void generateJavaFilesWithJavaPoet(){
        if(bindMap.isEmpty()){
            return;
        }

        //針對每個型別元素,生成一個新檔案,例如,針對MainActivity,生成MainActivity_ViewBind檔案
        for(Map.Entry<TypeElement, List<ViewBindInfo>> entry : bindMap.entrySet()){
            TypeElement typeElement = entry.getKey();
            List<ViewBindInfo> list = entry.getValue();

            //獲取當前型別元素所在的包名
            String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();

            //獲取類的全名稱
            ClassName t = ClassName.bestGuess("T");
            ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");

            //定義方法結構
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                    .addAnnotation(Override.class)//Override註解
                    .addModifiers(Modifier.PUBLIC)//public修飾符
                    .returns(void.class)//返回型別void
                    .addParameter(t, "activity")//引數型別
                    ;
            //為方法新增實現
            for(ViewBindInfo info : list){
                methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
            }

            //定義類結構
            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
                    .addModifiers(Modifier.PUBLIC)//public修飾符
                    .addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型宣告
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
                    .addMethod(methodSpecBuilder.build())//方法
                    .build();

            //定義一個Java檔案結構
            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
            try {
                //寫入到filer中
                javaFile.writeTo(filer);
            }catch (Exception ex){
                ex.printStackTrace();
            }

        }

    }

    /**
     * 錯誤提示
     * @param element
     * @param msg
     * @param args
     */
    private void error(Element element, String msg, Object... args){
        //輸出錯誤提示
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

    /**
     * 資訊提示
     * @param msg
     * @param args
     */
    private void info(String msg, Object... args) {
        messager.printMessage(
                Diagnostic.Kind.NOTE,
                String.format(msg, args));
    }

}

這裡會使用一個ViewBindInfo用於儲存view的id, 名稱,和型別的對應關係,如下:

public class ViewBindInfo {

    public int viewId;
    public String viewName;
    public TypeMirror typeMirror;

    public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
        this.viewId = viewId;
        this.viewName = viewName;
        this.typeMirror = typeMirror;
    }

}

這樣就完成了編譯時註解的處理。接下來在MainActivity中使用註解


public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_text)
    TextView tvText;
    @BindView(R.id.btn_text)
    Button btnText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BindHelper.bind(this);

        tvText.setText("Hello, BindView");
        btnText.setText("Hello, BindView");
    }
}

編譯後會生成一個MainActivity_ViewBinder.java檔案。我們在看看BindHelper的實現


public class BindHelper {

    /**
     * 繫結方法
     * @param activity
     */
    public static void bind(Activity activity) {
        try {
            Class<?> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }

}

這裡就會通過反射生成MainActivity_ViewBinder的物件,呼叫bind方法作view的繫結處理。

例子專案

CompileAnnotation

我的GitHub
微信掃一掃下方二維碼即可關注: