一、專案工程介紹
- lib-annotation 是一個 Java Library 模組,主要用於自定義註解;
- lib-compiler 是一個 Java Library 模組,需要依賴 lib-annotation 模組,主要用於解析自定義註解與生成原始檔。lib-compiler 還需要依賴 3 個開源庫來幫助開發;
- auto-common/auto-service:為註解處理器自動生成 metadata 檔案並將註解處理器 jar 檔案加入構建路徑,不再需要我們手動建立並更新 META-INF/services/javax.annotation.processing.Processor 檔案;
- javapoet:一款 Java 程式碼生成框架,可以令我們省去繁瑣冗雜的拼接程式碼的重複工作。
- lib-inject 是一個 Android Library 模組,需要依賴 lib-annotation 模組,主要用於提供 Api 給 app 模組呼叫;
- app 為應用模組,依賴 lib-compiler 與 lib-inject;
二、lib-annotation-自定義註解模組
建立一個自定義註解類BindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
複製程式碼
@Target(ElementType.FIELD)
表示該註解修飾的是成員變數;@Retention(RetentionPolicy.CLASS)
表示該註解只會在編譯時使用;int value()
為註解的值,這裡應該傳入的是一個控制元件 id;
三、lib-compiler-註解處理器模組
首先在build.gradle
裡新增依賴
dependencies {
api project(':lib-annotation')
implementation 'com.google.auto:auto-common:0.8'
implementation 'com.google.auto.service:auto-service:1.0-rc3'
implementation 'com.squareup:javapoet:1.9.0'
}
複製程式碼
然後建立一個類 BindViewProcessor,通過繼承 AbstractProcessor 來自定義註解處理器,繼承 AbstractProcessor 要實現一個抽象方法process()
public class BindViewProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set,
RoundEnvironment roundEnvironment) {
return false;
}
}
複製程式碼
這裡我們先不理會這個方法,先做一些準備工作
第一步,我們需要註冊 BindViewProcessor,之前我們已經新增了 auto-service 這個庫,那麼註冊就是一個註解的事,使用@AutoService(Processor.class)
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼
第二步,我們需要宣告支援的 Java 版本,這裡有兩種方式,一種是重寫getSupportedSourceVersion()
,一種是使用註解@SupportedSourceVersion()
// 重寫方法
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
// 使用註解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼
SourceVersion 是一個列舉類,可以使用SourceVersion.RELEASE_0
至SourceVersion.RELEASE_8
表示各個 Java 版本,也可以直接使用SourceVersion.latestSupported()
表示最新的版本
第三步,我們需要宣告自定義註解處理器要處理哪些註解,同樣的,這裡也有兩種方式,一種是重寫getSupportedAnnotationTypes()
,一種是使用註解@SupportedAnnotationTypes()
// 重寫方法
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(BindView.class.getCanonicalName());
return set;
}
// 使用註解-傳入註解的全類名
@SupportedAnnotationTypes({"com.fancyluo.lib_annotation.BindView"})
public class BindViewProcessor extends AbstractProcessor { }
複製程式碼
第四步,我們需要重寫init()
方法來獲取一些輔助類
// 解析 Elementm 的工具類,主要用於獲取包名
private Elements mElementUtils;
// 主要用於輸出 Java 原始檔
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
複製程式碼
第五步,這裡要重新拿起之前忽略的process()
方法,這個方法是重中之重,我們要在這裡面解析自定義註解和生成 Java 原始檔。
先來看看我們要生成什麼樣的程式碼
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
target.btnAction=(Button)target.findViewById(2131165218);
}
}
複製程式碼
當我們使用BindView
修飾程式元素的時候,我們的自定義註解處理器就可以拿到相應的程式元素的節點,通過解析節點,拿到相應的資料,然後自動的為這個程式元素所在的類生成一個輔助類,在裡面為程式元素賦值。
也可以這麼理解,我們會為使用BindView
修飾的控制元件所在的 Activity 自動的生成一個輔助類,在裡面進行控制元件的findViewById
接下來的程式碼都是在process()
方法裡,只是我將其分拆出來講解
@Override
public boolean process(Set<? extends TypeElement> set,
RoundEnvironment roundEnvironment) {
...//程式碼下面講解
return false;
}
複製程式碼
首先,我們通過 roundEnvironment 拿到所有的被BindView
修飾的節點
Set<? extends Element> elements =
roundEnvironment.getElementsAnnotatedWith(BindView.class);
複製程式碼
這裡可以理解為一個控制元件,只是被封裝轉換成了 Element
@BindView(R.id.btnAction)
Button btnAction;
轉換成 -> Element
複製程式碼
然後遍歷 elements 集合,解析資料,將我們需要的資料封裝成一個類,並按照 TypeElement 來進行分組。TypeElement 可以理解為類節點,而 Element 是成員節點,再具體來說,TypeElement 就是 MainActivity,而 Element 就是其中的 btnAction;那麼,按照 TypeElement 分組也就是將控制元件按照其所在的 Activity 進行分組。
首先建立我們需要的資料封裝類BindViewInfo
public class FieldBinding {
// 可以理解為:Button 這個型別
private TypeMirror typeMirror;
// 可以理解為:成員變數名-btnAction
private String name;
// 可以理解為:Button 的 id-R.id.btnAction
private int resId;
...
}
複製程式碼
開始遍歷集合,並且將節點資料封裝到 BindViewInfo,並將其分組儲存到 Map 集合
// Key 為型別節點,可以理解為 MainActivity
// Value 可以理解為 MainActivity 裡面所有被 BindView 註解的成員變數資訊
Map<TypeElement, List<BindViewInfo>> cacheMap = new HashMap<>();
// 遍歷所有被 BindView 註解的成員變數,按照 Activity 進行分組
for (Element element : elements) {
// 得到型別節點,可以理解為得到MainActivity
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 從快取中獲取資料,如果沒有,則新建並新增到快取
List<BindViewInfo> fieldList = cacheMap.get(enclosingElement);
if (fieldList == null) {
fieldList = new ArrayList<>();
cacheMap.put(enclosingElement, fieldList);
}
// 封裝被 BindView 註解的成員變數的資訊
// 成員變數的類型別,例如 Button
TypeMirror typeMirror = element.asType();
// 成員變數名 例如 btnAction
String fieldName = element.getSimpleName().toString();
// 控制元件資源Id 例如 R.id.btn
int resId = element.getAnnotation(BindView.class).value();
BindViewInfo bindViewInfo = new BindViewInfo(typeMirror, fieldName, resId);
fieldList.add(fieldBinding);
}
複製程式碼
將資料分好組快取後,我們就可以來構建我們需要的 Java 原始檔的程式碼了,前面說過,TypeElement 代表著一個Activity,而 List<BindViewInfo>
就代表著裡面使用 BindView 註解修飾的控制元件,我們要為 Activity 生成一個輔助類,在裡面為這些控制元件生成 findViewById
程式碼
首先,我們遍歷 cacheMap,並解析我們需要的資料
for (Map.Entry<TypeElement, List<FieldBinding>> entry : cacheMap.entrySet()) {
List<FieldBinding> bindingList = entry.getValue();
// 如果該Activity沒有被BindView註解的成員變數,則執行下一個
if (bindingList == null || bindingList.size() == 0) {
continue;
}
// 獲取型別節點 例如 MainActivity
TypeElement typeElement = entry.getKey();
// 獲取包名 例如 com.fancyluo.k_butterknife
String packageName = getPackageName(typeElement);
// 獲取類名 例如 MainActivity
String classNameStr = getClassName(packageName, typeElement);
ClassName classNamePackage = ClassName.bestGuess(classNameStr);
// 獲取ViewBinder
ClassName viewBinder = ClassName.get("com.fancyluo.lib_inject", "ViewBinder");
...//程式碼下面講解
}
複製程式碼
getPackageName(typeElement)
private String getPackageName(TypeElement enClosingElement) {
// 獲取包節點
PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
//返回的是 com.fancyluo.k_butterknife
return packageElement.getQualifiedName().toString();
}
複製程式碼
getClassName(packageName, typeElement)
// 例如 com.fancyluo.k_butterknife.MainActivity
String qualifiedName = typeElement.getQualifiedName().toString();
// 例如 com.fancyluo.k_butterknife.
int length = packageName.length() + 1;
// 如果當前的TypeElement是內部類的話,裁剪掉包名和後面的點號,並將之後的點號替換為$
return qualifiedName.substring(length).replace(".", "$");
複製程式碼
ViewBinder
是在lib_inject
模組裡定義的一個介面,我們生成的輔助類需要實現這個介面並且實現介面的bind()
方法進行控制元件的findViewById
拿到我們需要的資料以後,就可以開始使用 javapoet 提供的 api 來構建 Java 原始碼,下面,我們再來貼一下我們要生成的程式碼,然後我們會一步一步來構建這些程式碼。
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
target.btnAction=(Button)target.findViewById(2131165218);
}
}
複製程式碼
首先,我們要構建類
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(classNameStr + "$$ViewBinder")
.addModifiers(Modifier.PUBLIC)
.addTypeVariable(TypeVariableName.get("T", classNamePackage))
.addSuperinterface(ParameterizedTypeName.get(viewBinder, classNamePackage));
複製程式碼
- classBuilder 裡傳入的是類名
- addModifiers 是設定類的訪問屬性
- addTypeVariable 是設定類的泛型引數,傳入一個 TypeVariableName,TypeVariableName 第一個引數為泛型引數名,第二個引數為 ClassName,例如
T extends MainActivity
- addSuperinterface 是設定當前類實現的介面,傳入一個 ParameterizedTypeName,ParameterizedTypeName 第一個引數為父介面的 ClassName,第二個引數 ClassName,例如
ViewBinder<MainActivity>
這裡就相當於構建了
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
}
複製程式碼
第二,我們要構建方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名
.addAnnotation(Override.class)//新增註解
.addModifiers(Modifier.PUBLIC)//訪問屬性
.returns(TypeName.VOID)// 返回值
// 新增引數:1-ClassName 2-引數名 3-引數的訪問許可權
.addParameter(classNamePackage, "target", Modifier.FINAL);
複製程式碼
構建完方法的基本元素後,現在的程式碼結構為
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
...
}
}
複製程式碼
最後我們來構建方法裡面的具體程式碼,也就是相應控制元件的 findViewById
for (BindViewInfo bindViewInfo : bindingList) {
// 獲取型別名稱,例如 Button
String packageNameStr = fieldBinding.getTypeMirror().toString();
ClassName className = ClassName.bestGuess(packageNameStr);
// $L/$T代表佔位符,$L為基本型別 $T為類型別
// 這裡相當於生成了 target.btnAction=(Button)target.findViewById(2131165218);
methodBuilder.addStatement("target.$L=($T)target.findViewById($L)",
fieldBinding.getName(),
className,
fieldBinding.getResId());
}
複製程式碼
方法完全構建完成後,我們將其新增到類裡面
typeBuilder.addMethod(methodBuilder.build());
複製程式碼
最後,我們通過 Filer 類來生成 Java 原始檔
try {
//生成Java檔案,最終寫是通過filer類寫出的
JavaFile.builder(packageName,result.build())
.addFileComment("auto create make")
.build()
.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
複製程式碼
四、lib-inject-核心 Api 模組
定義一個ViewBinder
介面,之前說過,這個介面是給註解處理器自動生成的類來實現的,然後在其bind()
方法裡面實現 findViewById 程式碼
public interface ViewBinder<T> {
void bind(T target);
}
複製程式碼
接下來,定義一個核心類,其中的靜態方法bind()
會傳入要繫結的 Activity,通過這個 Activity 的類名在執行時反射獲取到註解處理器生成的對應的輔助類,然後呼叫輔助類的bind
方法完成控制元件的 findViewById
public class KButterKnife {
public static void bind(Activity activity) {
String className = activity.getClass().getName();
try {
Class<?> clazz = Class.forName(className+"$$ViewBinder");
ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
複製程式碼
五、App-應用層
最後來測試使用一下,首先, 要依賴 lib-compiler 模組與 lib-inject 模組
implementation project(':lib-inject')
// lib-compiler 為註解處理器
annotationProcessor project(':lib-compiler')
複製程式碼
然後在 Activity 裡面使用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.btnAction)
Button btnAction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
KButterKnife.bind(this);
btnAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,
"注入成功,哈哈哈", Toast.LENGTH_SHORT).show();
}
});
}
}
複製程式碼
檢視生成的原始檔