banner
jzman

jzman

Coding、思考、自觉。
github

編譯時註解詳解及實現ButterKnife

PS:人是一種很樂於接受自我暗示的生物,你給了自己消極暗示,那麼你很容易變得頹廢,如果你給了自己積極暗示,那麼你也會變得積極起來。

今天看一下編譯時註解的相關知識,相信手動實踐後你會更容易理解像 Dagger、ARouter、ButterKnife 等這種使用了編譯時註解的框架,也更容易理解其內部源碼實現,內容如下:

  1. 編譯時和運行時註解
  2. 註解處理器 APT
  3. AbstractProcessor
  4. Element 和 Elements
  5. 自定義註解處理器
  6. 使用自定義註解處理器

編譯時和運行時註解#

先了解一下編譯時和運行時的區別:

  1. 編譯時:指編譯器將源代碼翻譯成機器能夠識別的代碼的過程,Java 中也就是將 Java 源代碼編譯為 JVM 識別的字節碼文件的過程。
  2. 運行時:指 JVM 分配內存、解釋執行字節碼文件的過程。

元註解 @Retention 決定註解是在編譯時還是在運行時,其可配置策略如下:

public enum RetentionPolicy {
    SOURCE,  //在編譯時會被丟棄,僅僅在源碼中存在
    CLASS,   //默認策略,運行時就會被丟棄,僅僅在 class 文件中
    RUNTIME  //編譯時會將註解信息記錄到class文件,運行時仍然保留,可以通過反射獲取註解信息
}

編譯時註解和運行時註解除了上面的區別之外,實現方式也不一樣,編譯時註解一般是通過註解處理器 (APT) 來實現的,運行時註解一般是通過反射來實現的。

關於註解和反射的可以參考下面兩篇文章:

什麼是 APT#

APT (Annotation Processing Tool) 是 javac 提供的一種可以處理註解的工具,用來在編譯時掃描和處理註解的,簡單來說就是可以通過 APT 獲取到註解及其註解所在位置的信息,可以使用這些信息在編譯器生成代碼。編譯時註解就是通過 APT 來通過註解信息生成代碼來完成某些功能,典型代表有 ButterKnife、Dagger、ARouter 等。

AbstractProcessor#

AbstractProcessor 實現 Processor 是註解處理器的抽象類,要實現註解處理器都需要繼承 AbstractProcessor 進行擴展,主要方法說明如下:

  • init:初始化 Processor,可從參數 ProcessingEnvironment 中獲取工具類 ElementsTypesFiler 以及 Messager 等;
  • getSupportedSourceVersion:返回使用的 Java 版本;
  • getSupportedAnnotationTypes:返回要處理的的所有的註解名稱;
  • process:獲取所有指定的註解進行處理;

process 方法可能會在運行過程中執行多次,直到沒有其他類產生為止。

Element 和 Elements#

Element 類似於 XML 中的標籤一樣,在 Java 中Element 表示程序元素,如類、成員、方法等,每個 Element 都表示僅表示某種結構,對 Element 對象類的操作,請使用 visitor 或 getKind ( 方法進行判斷再具體處理,Element 的子類如下:

  • ExecutableElement
  • PackageElement
  • Parameterizable
  • QualifiedNameable
  • TypeElement
  • TypeParameterElement
  • VariableElement

上述元素結構分別對應下面代碼結構:

// PackageElement
package manu.com.compiler;

// TypeElement
public class ElementSample {
    // VariableElement
    private int a;
    // VariableElement
    private Object other;

    // ExecutableElement
    public ElementSample() {
    }
    // 方法參數VariableElement
    public void setA(int newA) {
    }
    // TypeParameterElement表示參數化類型,用在泛型參數中
}

Elements 是處理 Element 的工具類,只提供接口,具體有 Java 平台實現。

自定義編譯註解處理器#

下面使用 APT 實現一個註解 @Bind 來替換來模仿 ButterKnife 的註解 @BindView,案例項目結構如下:

image

在 api 模塊中定義註解 @Bind 如下:

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

compiler 模塊是 Java 模塊,引入 Google 的 auto-service 用來生成 META-INFO 下的相關文件,javapoet 用於更方便的創建 Java 文件,自定義註解處理器 BindProcessor 如下:

// 用來生成META-INF/services/javax.annotation.processing.Processor文件
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// 用於創建Java文件
implementation 'com.squareup:javapoet:1.12.1'
/**
 * BindProcessor
 */
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    private Elements mElements;
    private Filer mFiler;
    private Messager mMessager;

    // 存儲某個類下面對應的BindModel
    private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        print("init");
        // 初始化Processor

        mElements = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        print("getSupportedSourceVersion");
        // 返回使用的Java版本
        return SourceVersion.RELEASE_8;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        print("getSupportedAnnotationTypes");
        // 返回要處理的的所有的註解名稱
        Set<String> set = new HashSet<>();
        set.add(Bind.class.getCanonicalName());
        set.add(OnClick.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        print("process");
        mTypeElementMap.clear();
        // 獲取指定Class類型的Element
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
        // 遍歷將符合條件的Element存儲起來
        for (Element element : elements) {
            // 獲取Element對應類的全限定類名
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            print("process typeElement name:"+typeElement.getSimpleName());
            List<BindModel> modelList = mTypeElementMap.get(typeElement);
            if (modelList == null) {
                modelList = new ArrayList<>();
            }
            modelList.add(new BindModel(element));
            mTypeElementMap.put(typeElement, modelList);
        }

        print("process mTypeElementMap size:" + mTypeElementMap.size());

        // Java文件生成
        mTypeElementMap.forEach((typeElement, bindModels) -> {
            print("process bindModels size:" + bindModels.size());
            // 獲取包名
            String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString();
            // 生成Java文件的文件名
            String className = typeElement.getSimpleName().toString();
            String newClassName = className + "_ViewBind";

            // MethodSpec
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.bestGuess(className), "target");
            bindModels.forEach(model -> {
                constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)",
                        model.getViewFieldName(), model.getViewFieldType(), model.getResId());
            });
            // typeSpec
            TypeSpec typeSpec = TypeSpec.classBuilder(newClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(constructorBuilder.build())
                    .build();
            // JavaFile
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                    .addFileComment("AUTO Create")
                    .build();

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        return true;
    }

    private void print(String message) {
        if (mMessager == null) return;
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }
}

其中 BindModel 是對註解 @Bind 信息的簡單封裝,如下:

/**
 * BindModel
 */
public class BindModel {
    // 成員變量Element
    private VariableElement mViewFieldElement;
    // 成員變量類型
    private TypeMirror mViewFieldType;
    // View的資源Id
    private int mResId;

    public BindModel(Element element) {
        // 校驗Element是否是成員變量
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException("element is not FIELD");
        }
        // 成員變量Element
        mViewFieldElement = (VariableElement) element;
        // 成員變量類型
        mViewFieldType = element.asType();
        // 獲取註解的值
        Bind bind = mViewFieldElement.getAnnotation(Bind.class);
        mResId = bind.value();
    }

    public int getResId(){
        return mResId;
    }

    public String getViewFieldName(){
        return mViewFieldElement.getSimpleName().toString();
    }

    public TypeMirror getViewFieldType(){
        return mViewFieldType;
    }
}

在 bind 模塊創建要生成的文件

/**
 * 初始化
 */
public class BindKnife {
    public static void bind(Activity activity) {
        // 獲取activity的全限定類名
        String name = activity.getClass().getName();
        try {
            // 反射創建並注入Activity
            Class<?> clazz = Class.forName(name + "_ViewBind");
            clazz.getConstructor(activity.getClass()).newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

當然 ButterKnife 應該是把創建的對象緩存起來,不會每次都會去創建新的對象,雖然也是用了反射,但是相較運行時註解來說反射的測試減少了很多,所以性能也較運行時註解更好,這也算是編譯時註解和運行時註解的區別。

使用自定義註解處理器#

使用方式和 ButterKnife 類似,如下:

public class MainActivity extends AppCompatActivity{
   
    @Bind(R.id.tvData)
    TextView tvData;

    @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindKnife.bind(this);
        tvData.setText("data");
    }
}

了解編譯時註解,可能並並不會去立馬造輪子,但是在看其他框架的時候非常有幫助,在解決問題的時候也就多了一個途徑。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。