前言
我們經常使用的一些第三方框架,比如:butterknife,通過一行注解就可以實現View
的“自動賦值”。
那么,這其中的原理是什么呢?
為了帶大家更好的深入了解,本文將打造一個簡單的 Demo,來說明這其中的原理。
Demo 雖然簡單,但是完全按照 butterknife 實現的方式和原理打造。
實現思路
我們先看 Demo 的效果:
public class MainActivity extends AppCompatActivity { // 被注解的 View @BindView(R.id.tv) TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 為 tv 賦值 InjectHelper.inject(this); tv.setText("I am injected"); } }
代碼非常簡單,分為兩步:
- 通過
@BindView(R.id.tv)
指定被注解 View 的 Id 值; InjectHelper.inject(this);
,具體為 tv 賦值的方法。
@BindView
注解沒什么好解釋的,我們重點看下 InjectHelper.inject(this);
的實現:
public class InjectHelper { public static void inject(Activity host) { // 1、 String classFullName = host.getClass().getName() + "$$ViewInjector"; try { // 2、 Class proxy = Class.forName(classFullName); // 3、 Constructor constructor = proxy.getConstructor(host.getClass()) // 4、 constructor.newInstance(host); } catch (Exception e) { e.printStackTrace(); } } }
- 獲得 View 所在 Activity 的類路徑,然后拼接一個字符串“$$ViewInjector”。這個是編譯時動態生成的 Class 的完整路徑,也就是我們需要實現的,同時也是最關鍵的部分;
- 根據 Class 路徑,使用
Class.forName(classFullName)
生成 Class 對象; - 得到 Class 的構造函數 constructor 對象;
- 使用
constructor.newInstance(host)
new 出一個對象,這會執行對象的構造方法,方法內部是我們為 MainActivity 的 tv 賦值的地方。
我們先看一個生成好的 “XXX$$ViewInjector” 示例:
public class MainActivity$$ViewInjector { public MainActivity$$ViewInjector(MainActivity activity) { activity.tv = (TextView)activity.findViewById(2131427422); } }
到這里,我們大概知道了最關鍵的地方,就是如何生成 “XXX$$ViewInjector” 這個類了。
APT 實現方案
APT 是一種處理注解的工具,它對源代碼文件進行檢測找出其中的 Annotation,再根據注解自動生成代碼。
實現方案,分為兩種:
- android-apt,個人開發者提供,現在已經停止維護,作者推薦大家使用官方提供的解決方案。
- Android Gradle 插件:annotationProcessor,由官方提供支持。
如何由 android-apt 切換到 annotationProcessor,可以參考這里。
annotationProcessor 配置起來比較簡單,另外由於是官方支持的,所以我們選擇第二種方案。
實現步驟
第一步:定義注解 @BindView
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int value(); }
沒什么好解釋的~
第二步:實現 AbstractProcessor
1、新建一個 Java Library,引入兩個第三方庫:
dependencies {
// ... compile 'com.google.auto.service:auto-service:1.0-rc2' compile 'com.squareup:javapoet:1.7.0' }
-
auto-service:Google 公司出品,用於自動為 JAVA Processor 生成 META-INF 信息。
如果你定義了一個 Processor:
package foo.bar; import javax.annotation.processing.Processor; @AutoService(Processor.class) final class MyProcessor implements Processor { // … }
auto-service 會在編譯目錄生成一個文件,路徑是:META-INF/services/javax.annotation.processing.Processor,文件內容為:
foo.bar.MyProcessor
-
javapoet:大名鼎鼎的 squareup 公司出品,封裝了一套生成 .java 源文件的 API。
以 HelloWorld 類為例:
package com.example.helloworld; public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } }
上面的代碼就是使用javapoet用下面的代碼進行生成的:
MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out);
2、繼承 AbstractProcessor,配置相關信息:
@AutoService(Processor.class) @SupportedAnnotationTypes({"com.example.BindView"}) @SupportedSourceVersion(SourceVersion.RELEASE_7) public class ViewInjectProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //... } }
- @AutoService(Processor.class),生成 META-INF 信息;
- @SupportedAnnotationTypes({"com.example.BindView"}),聲明 Processor 處理的注解,注意這是一個數組,表示可以處理多個注解;
- @SupportedSourceVersion(SourceVersion.RELEASE_7),聲明支持的源碼版本
補充說明一下,@SupportedAnnotationTypes 和 @SupportedSourceVersion 必須聲明,否則會報錯。具體原因看大家看一下源碼就明白了,這里不做過多解釋。
除了注解方式,你也可以通過重寫下面兩個函數實現:
@AutoService(Processor.class) public class ViewInjectProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotationTypes = new HashSet<>(); annotationTypes.add("com.example.BindView"); return annotationTypes; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7; } }
3、實現 AbstractProcessor 的 process()
方法:
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // 1、 collectInfo(roundEnvironment); // 2、 writeToFile(); return true; }
process()
方法的實現,分為兩個步驟:
- 收集 Class 內的所有被
@BindView
注解的成員變量; - 根據上一步收集的內容,生成 .java 源文件。
為此,我們聲明了兩個 Map,用於保存 collectInfo()
收集的相關信息,Map 的 key 為類的全路徑:
// 存放同一個Class下的所有注解信息 Map<String, List<VariableInfo>> classMap = new HashMap<>(); // 存放Class對應的信息:TypeElement Map<String, TypeElement> classTypeElement = new HashMap<>();
VariableInfo 是一個簡單的類,用於保存被注解 View 對應的一些信息:
public class VariableInfo { // 被注解 View 的 Id 值 int viewId; // 被注解 View 的信息:變量名稱、類型 VariableElement variableElement; // ... }
4、實現 collectInfo()
方法:
void collectInfo(RoundEnvironment roundEnvironment) { classMap.clear(); classTypeElement.clear(); Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class); for (Element element : elements) { // 獲取 BindView 注解的值 int viewId = element.getAnnotation(BindView.class).value(); // 代表被注解的元素 VariableElement variableElement = (VariableElement) element; // 備注解元素所在的Class TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); // Class的完整路徑 String classFullName = typeElement.getQualifiedName().toString(); // 收集Class中所有被注解的元素 List<VariableInfo> variableList = classMap.get(classFullName); if (variableList == null) { variableList = new ArrayList<>(); classMap.put(classFullName, variableList); // 保存Class對應要素(名稱、完整路徑等) classTypeElement.put(classFullName, typeElement); } VariableInfo variableInfo = new VariableInfo(); variableInfo.setVariableElement(variableElement); variableInfo.setViewId(viewId); variableList.add(variableInfo); } }
代碼的注釋已經很完整,這里不再說明了。
這里提一下 Element 這個元素,它的子類我們用到了以下兩個:
Element
- VariableElement:代表變量
- TypeElement:代表 class
5、實現 writeToFile()
方法:
void writeToFile() {
try { for (String classFullName : classMap.keySet()) { TypeElement typeElement = classTypeElement.get(classFullName); // 使用構造函數綁定數據 MethodSpec.Builder constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build()); List<VariableInfo> variableList = classMap.get(classFullName); for (VariableInfo variableInfo : variableList) { VariableElement variableElement = variableInfo.getVariableElement(); // 變量名稱(比如:TextView tv 的 tv) String variableName = variableElement.getSimpleName().toString(); // 變量類型的完整類路徑(比如:android.widget.TextView) String variableFullName = variableElement.asType().toString(); // 在構造方法中增加賦值語句,例如:activity.tv = (android.widget.TextView)activity.findViewById(215334); constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId()); } // 構建Class TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector") .addModifiers(Modifier.PUBLIC) .addMethod(constructor.build()) .build(); // 與目標Class放在同一個包下,解決Class屬性的可訪問性 String packageFullName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec) .build(); // 生成class文件 javaFile.writeTo(filer); } } catch (Exception ex) { ex.printStackTrace(); } }
這段代碼主要就是通過 javapoet 來生成 .java 源文件,大家如果感覺陌生,建議先看一下 javapoet 的使用,參考文章: JavaPoet的基本使用。
當然,你完全可以通過拼接字符串來生成 .java 源文件的內容。
還記得文章開頭提到的 “XXX$$ViewInjector” 嗎?writeToFile()
方法,就是為了生成這個 .java 源文件的。
第三步:使用 annotationProcessor
在 app 的 build.gradle 文件中,使用 APT:
dependencies {
// ... annotationProcessor project(':lib-compiler') }
lib-compiler:為第二步新建的 Java Library。
第四步:Activity 中使用 @BindView
文章開始已經演示過相關代碼了,這里不再貼了。
一共分為以下兩步:
@BindView
注解相關 View;- 調用
InjectHelper.inject(this)
方法。
然后,你嘗試去編譯項目,會發現 APT 為我們自動生成了 XXX$$ViewInjector.class 文件。
你可以在 app/build/intermediates/classes/debug(release、其他 buildType) 下對應的包中找到。