1. 程式人生 > >ButterKnife編譯時註解探祕

ButterKnife編譯時註解探祕

安卓中很多有名的開源框架都運用了編譯時註解,如ButterKnife,EventBus , Retrofit , GreenDao等。所以作為一個合格的安卓開發者,學會運用編譯時註解是非常有必要的。

下面就仿照ButterKnife的view的注入寫一個例子來走一遍編譯時註解的流程。

第一步新建module

1、建立一個module起名為annotation作為註解類的module 用來儲存我們寫的註解類
2、建立一個java module 起名為annotation-processor (注意一定是java modeule 因為這個類中需要繼承AbstractProcessor,AbstractProcessor是javax中的類正常的android sdk中是引用不到的

最後的工程目錄:

這裡寫圖片描述

在module的build.gradle中配置一下Java的使用版本

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

這邊最好不要用1.8,因為只有android N支援java8,如果你寫1.8之後,強制要你使用buildToolsVersion為24.0.0

第二步編寫annotation中的程式碼

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public
@interface MyAnnotation { int value(); }

@Retention 註解說明,表示這個註解可以保留到哪個階段 有三種
(1)RetentionPolicy.SOURCE —— 這種型別的Annotations只在原始碼級別保留,編譯時就會被忽略
(2)RetentionPolicy.CLASS —— 這種型別的Annotations編譯時被保留,在class檔案中存在,但JVM將會忽略
(3)RetentionPolicy.RUNTIME —— 這種型別的Annotations將被JVM保留,所以他們能在執行時被JVM或其他使用反射機制的程式碼所讀取和使用.
所以編譯時註解我們就選第二種啦,編譯的時候生成程式碼,以前的一些註解框架比如xutil中的view注入的註解使用的是第三種執行時註解,可以保留到app執行時中,使用的時候通過註解和反射來實現功能。由於執行的時候使用反射的效率不是很好,所以現在就流行編譯的時候做事情啦。
@Target

註解的作用目標 表示這個註解的作用域
@Target(ElementType.TYPE) //介面、類、列舉、註解
        @Target(ElementType.FIELD) //欄位、列舉的常量
        @Target(ElementType.METHOD) //方法
        @Target(ElementType.PARAMETER) //方法引數
        @Target(ElementType.CONSTRUCTOR) //建構函式
        @Target(ElementType.LOCAL_VARIABLE)//區域性變數
        @Target(ElementType.ANNOTATION_TYPE)//註解
        @Target(ElementType.PACKAGE) ///包

第三步編寫 processor

我們都知道編譯時註解的運用一般都是在編譯的時候生成我們需要的java檔案然後去引用,
如果要生成 .java 的檔案需要用到JavaPoet (square公司開源的用來生成java檔案)
需要在processor的build.gradle檔案中加上依賴

compile 'com.squareup:javapoet:1.9.0'

JavaPoet提供了(TypeSpec)用於建立類或者介面,(FieldSpec)用來建立欄位,(MethodSpec)用來建立方法和建構函式,(ParameterSpec)用來建立引數,(AnnotationSpec)用於建立註解。

如果要生成一個java檔案,首先我們要知道我們喲啊生成的java檔案是神馬樣的,用過ButterKnife的朋友應該都見過它生成的java檔案類似下面

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

Ok 下面就通過JavaPoet 的api將上面的一個類拼接出來

 package com.example;

import com.chs.annotation.MyAnnotation;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

public class MyProcessor 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(MyAnnotation.class)) {
            //獲取最裡面的節點
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = enclosingElement.getQualifiedName().toString();
            packageName =packageName .substring(0, packageName.lastIndexOf("."));
            String className = enclosingElement.getSimpleName().toString();
            String typeMirror = element.asType().toString();

            //註解的值
            int annotationValue = element.getAnnotation(MyAnnotation.class).value();
            String name = element.getSimpleName().toString();
            TypeName type = TypeName.get(enclosingElement.asType());//此元素定義的型別
            if (type instanceof ParameterizedTypeName) {
                type = ((ParameterizedTypeName) type).rawType;
            }

            System.out.println("typeMirror:"+typeMirror);//被註解的物件的型別
            System.out.println("packageName:"+packageName);//包名
            System.out.println("className:"+className);//類名
            System.out.println("annotationValue:"+annotationValue);//註解傳過來的引數
            System.out.println("name:"+name);//被註解的物件的名字
            System.out.println("type:"+type);//當前被註解物件所在類的完整路徑

            ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
              //建立程式碼塊
            CodeBlock.Builder builder = CodeBlock.builder()
                    .add("target.$L = ", name);//$L是佔位符,會把後面的name引數拼接大$L所在的地方
                builder.add("($L)source.findViewById($L)", typeMirror , annotationValue);

                // 建立main方法
              MethodSpec methodSpec = MethodSpec.constructorBuilder()
                      .addModifiers(Modifier.PUBLIC)
                      .addParameter(type,"target")
                      .addParameter(ClassName.get("android.view", "View"),"source")
                      .addStatement("$L", builder.build())
                      .build();

                // 建立類
                TypeSpec helloWorld = TypeSpec.classBuilder(bindingClassName.simpleName())
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(methodSpec)
                        .build();

                try {
                    // 生成檔案
                    JavaFile javaFile = JavaFile.builder("com.chs.annotationtest", helloWorld)
                            .build();
                    // 將檔案寫出
                    javaFile.writeTo(System.out);
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return true;
    }
    //指定處理器處理哪個註解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }
    //指定使用的java的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}

AbstractProcessor是編譯時註解處理器的主要類,我們需要自定義自己的MyProcessor 整合AbstractProcessor,重寫它裡面的方法。

上面的類中的

init 方法是初始化註解處理器

process 方法是最重要的一個方法,裡面是程式碼生成的主要邏輯 ,拼接程式碼的邏輯上面註釋很清楚啦!不過這裡只實現了一個物件的註解,正常的專案中不可能只有一個物件,知道了一個物件的使用多個物件按同樣的方法迴圈拼接出來就好啦

getSupportedAnnotationTypes 方法是 指定處理器處理哪個註解

getSupportedSourceVersion 方法是 指定java的相容版本

第四步

服務註冊檔案(META-INF/services/javax.annotation.processing.Processor)

1、在module annotation-processor 中main資料夾下建立resources資料夾
2、在resources資原始檔夾下建立META-INF資料夾
3、然後在META-INF資料夾中建立services資料夾
4、然後在services資料夾下建立名為javax.annotation.processing.Processor的檔案
5、在javax.annotation.processing.Processor檔案中配置我們自己的處理器的完整引用 比如com.example.MyProcessor
這裡寫圖片描述

到這裡我們就可以在主檔案中試驗一下啦
首先把註解的module和註解器的module的依賴新增到主專案中的build.gradle中

    compile project(':annotation')
    compile project(':annotation-processor')

MainActivity中使用註解

public class MainActivity extends AppCompatActivity {
    @MyAnnotation(R.id.tv_hello)
    TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

上面註解處理器的MyProcessor 類中的process方法的最後輸出現在是輸出到控制檯System.out

javaFile.writeTo(System.out);

所以現在我們編譯一下專案就可以在控制檯中看到列印了注意不是在logCat中看是在studio 右下角的Gradle Console中看

結果如下:

typeMirror:android.widget.TextView
packageName:com.chs.annotationtest
className:MainActivity
annotationValue:2131427422
name:mTextView
type:com.chs.annotationtest.MainActivity
package com.chs.annotationtest;

import android.view.View;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

上面的log資訊包括MyProcessor 類中的process方法中我們所寫的System.out.println所打印出來的 和最終的類。可以看到最終生成了我們想要的類。

第五步

Ok然後我們就可以去試一下這個類可不可以用啦。
(1)
首先我們得把這個類輸出成一個檔案,方法就是在MyProcessor 類中的init方法中找到Filer
Filer是個介面,支援通過註解處理器建立新檔案

  @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Filer是個介面,支援通過註解處理器建立新檔案
        filer = processingEnv.getFiler();
    }

(2)
然後在MyProcessor 類中的process方法中最後通過Filer輸出

                     // 將檔案寫出
                    javaFile.writeTo(filer);

然後重新編譯工程,就可以看到生成的檔案了。位置:
這裡寫圖片描述
開啟檔案可以看到內容:

package com.chs.annotationtest;

import android.view.View;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

然後就是把主程式中的MainActivity跟我們生成的MainActivity_ViewBinding類關聯起來讓MainActivity_ViewBinding初始化MainActivity中的註解了的物件。

根據我們建立類的時候的規則可以找到我們建立的類的名字,然後通過反射執行裡面的方法。

public class MyAnnotationUtil {
    public static void bind(Activity activity) {
        //獲取activity的decorView(根view)
        View view = activity.getWindow().getDecorView();
        String qualifiedName = activity.getClass().getName();

        //找到該activity對應的Bind類的名字
        String generateClass = qualifiedName + "_ViewBinding";
            //然後呼叫Bind類的構造方法,從而完成activity裡view的初始化
        try {
            Class.forName(generateClass).getConstructor(activity.getClass(), View.class).newInstance(activity, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在MainActivity中呼叫


public class MainActivity extends AppCompatActivity {
    @MyAnnotation(R.id.tv_hello)
    TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAnnotationUtil.bind(this);
        mTextView.setText("哈哈哈哈");
    }
}

執行專案可以看到手機上

這裡寫圖片描述

成功啦!ButterKnife的工作原理是不是很清晰啦!

用過ButterKnife的朋友都知道我們引入依賴的時候是有兩句話的:下面是最新版的ButterKnife

dependencies {
  compile 'com.jakewharton:butterknife:8.8.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

老版的ButterKnife 第二句話是

apt 'com.jakewharton:butterknife-compiler:8.4.0'

annotationProcessor 和 apt是神馬鬼。它們是Android Studio中處理註解處理的外掛 處理註解的工具
其實它倆的作用是一樣的作用如下:

可以自動的幫你為生成的程式碼建立目錄,使註解處理器生成的程式碼能被Android Studio正確的引用,讓生成的程式碼編譯到APK裡面去
重點:只在編譯的時候執行依賴的庫,但是庫最終不打包到apk中, 因為我們的annotation-processor module的作用就是生成java檔案,我們的App中用到的是它生成的java檔案,而對於它自身的程式碼當打包完的時候是沒什麼用的,所以不應該打包到apk中。

所以我們的主專案中的依賴就可以改為:

    compile project(':annotation')
    annotationProcessor project(':annotation-processor')

把控制器annotation-processor改為annotationProcessor 依賴,執行專案效果跟上面的一樣,而這次我們的apk中會少了annotation-processor中程式碼,這樣做無疑可以使apk得到了優化。

apt跟annotationProcessor 的作用是一樣的,只不過apt是個人自由開發,比annotationProcessor 出來的早,後來谷歌開發了annotationProcessor, Android Gradle 外掛 2.2 版本內建annotationProcessor比apt的使用比apt簡單。與時俱進嗎,所以apt怎麼用就不說啦。

註解處理器的簡化:

上面在寫完註解處理器MyProcessor 之後進行了註解處理器的註冊
服務註冊檔案(resources/META-INF/services/javax.annotation.processing.Processor)
這樣建立很多個資料夾是不是很麻煩呀,沒關係有個簡單的方法。

現在把我們建立的resources/META-INF/services/javax.annotation.processing.Processor全部刪掉,重新編譯專案可以看到我們想要的java檔案沒有生成

然後給module annotation-processor 新增依賴

compile 'com.google.auto.service:auto-service:1.0-rc3'

然後在MyProcessor 類上添加註解

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor{}

然後在編譯專案可以看到我們想要的 MainActivity_ViewBing.java檔案又出來了,執行專案效果跟上面一樣。

好啦,到此為止是不是發現ButterKnife的實現原理你已經明白了呢 ?哈哈哈哈哈 !