前言
近些年,編譯期插樁技術在Android圈越來越普遍。無論是可以生成JAVA原始碼的ButterKnief、Dagger,還是操作位元組碼的VirtualAPK,甚至是新興的語言Kotlin都用到了編譯期插樁技術。學習這門技術對我們理解這些框架的原理十分有幫助。另外,我們通過這種技術可以抽離出複雜、重複的程式碼,降低程式耦合性,提高程式碼的可複用性,提高開發效率。因此,瞭解編譯期插樁技術十分必要。在介紹這項技術之前,我們先來了解一下Android程式碼的編譯過程以及插樁位置。話不多說,直接上圖。
APT
APT(Annotation Processing Tool)是一種編譯期註解處理器。它通過定義註解和處理器來實現編譯期生成程式碼的功能,並且將生成的程式碼和原始碼一起編譯成.class檔案。
代表框架:ButterKnife、Dagger、ARouter、EventBus3、DataBinding、AndroidAnnotation等。
在介紹如何應用APT技術之前,我們先來了解一些相關的知識。
一、Element
1.簡介
Element
是一種在編譯期描述.java檔案靜態結構的一種型別,它可能表示一個package、一個class、一個method或者一個field。Element
的比較應該使用equals
,因為編譯期間同一個Element
可能會用兩個物件表示。JDK提供了以下5種Element
。
2.Element的儲存結構
編譯器採用類似Html的Dom樹來儲存Element。我們用下面的Test.java
來具體說明。
//PackageElement
package me.zhangkuo.compile;
//TypeElement
public class Test {
//VariableElement
private String name;
//ExecutableElement
private Test(){
}
//ExecutableElement
public void setName(/* TypeParameterElement */ String name) {
this.name = name;
}
}
複製程式碼
Test.java
用Element樹結構描述如下:
我們可以看到 setName(String name)
的ExecutableElement
中並沒有子節點TypeParameterElement
。這是因為TypeParameterElement
沒有被納入到Element
樹中。不過我們可以通過ExecutableElement
的getTypeParameters()
方法來獲取。
此外,再給大家介紹兩個Element中十分有用的方法。
public interface Element extends AnnotatedConstruct {
//獲取父Element
Element getEnclosingElement();
//獲取子Element的集合
List<? extends Element> getEnclosedElements();
}
複製程式碼
二、TypeMirror
Element
有一個asType()
方法用來返回TypeMirror
。TypeMirror
表示 Java 程式語言中的型別。這些型別包括基本型別、宣告型別(類和介面型別)、陣列型別、型別變數和 null 型別。還可以表示萬用字元型別引數、executable 的簽名和返回型別,以及對應於包和關鍵字 void
的偽型別。我們一般用TypeMirror進行型別判斷。如下段程式碼,用來比較元素所描述的型別是否是Activity的子類。
/**
* 型別相關工具類
*/
private Types typeUtils;
/**
* 元素相關的工具類
*/
private Elements elementUtils;
private static final String ACTIVITY_TYPE = "android.app.Activity";
private boolean isSubActivity(Element element){
//獲取當前元素的TypeMirror
TypeMirror elementTypeMirror = element.asType();
//通過工具類Elements獲取Activity的Element,並轉換為TypeMirror
TypeMirror viewTypeMirror = elementUtils.getTypeElement(ACTIVITY_TYPE).asType();
//用工具類typeUtils判斷兩者間的關係
return typeUtils.isSubtype(elementTypeMirror,viewTypeMirror)
}
複製程式碼
三、一個簡單的ButterKnife
這一節我們通過編寫一個簡單的ButterKnife
來介紹一下如何編寫一個APT框架。APT應該是編譯期插樁最簡單的一種技術,通過三步就可以完成。
- 定義編譯期註解。
我們新增一個Java Library Module命名為apt_api
,編寫註解類BindView。
@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}
複製程式碼
這裡簡單介紹一下RetentionPolicy
。RetentionPolicy
是一個列舉,它的值有三種:SOURCE、CLASS、RUNTIME。
- SOURCE:不參與編譯,讓開發者使用。
- CLASS:參與編譯,執行時不可見。給編譯器使用。
- RUNTIME:參與編譯,執行時可見。給編譯器和JVM使用。
- 定義註解處理器。
同樣,我們需要新增一個Java Library Module命名為apt_processor
。
我們需要引入兩個必要的依賴:一個是我們新增的module apt_annotation
,另一個是google的com.google.auto.service:auto-service:1.0-rc3
(以下簡稱auto-service
)。
implementation project(':apt_api')
api 'com.google.auto.service:auto-service:1.0-rc3'
複製程式碼
新增一個類 ButterKnifeProcessor
,繼承 AbstractProcessor
。
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
/**
* 元素相關的工具類
*/
private Elements elementUtils;
/**
* 檔案相關的工具類
*/
private Filer filer;
/**
* 日誌相關的工具類
*/
private Messager messager;
/**
* 型別相關工具類
*/
private Types typeUtils;
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
typeUtils = processingEnv.getTypeUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
複製程式碼
auto-service
為我們簡化了定義註解處理器的流程。@AutoService
是就是由auto-service
提供的,其作用是用來告訴編譯器我們定義的ButterKnifeProcessor
是一個編譯期註解處理器。這樣在編譯時ButterKnifeProcessor
才會被呼叫。
我們還重寫了AbstractProcessor提供的四個方法:getSupportedAnnotationTypes
、getSupportedSourceVersion
、init
、process
。
-
getSupportedAnnotationTypes
表示處理器可以處理哪些註解。這裡返回的是我們之前定義的BindView。除了重寫方法之外,還可用通過註解來實現。@SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"}) 複製程式碼
-
getSupportedSourceVersion
表示處理器可以處理的Java版本。這裡我們採用最新的JDK版本就可以了。同樣,我們也可以通過註解來實現。@SupportedSourceVersion(value = SourceVersion.latestSupported()) 複製程式碼
-
init
方法主要用來做一些準備工作。我們一般在這裡初始化幾個工具類。上述程式碼我們初始了與元素相關的工具類elementUtils
、與日誌相關的工具類messager
、與檔案相關的filer
以及與型別相關工具類typeUtils
。我們接下來會看到process
主要就是通過這幾個類來生成程式碼的。 -
process
用來完成具體的程式寫程式碼功能。在具體介紹process
之前,請允許我先推薦一個庫:javapoet。javapoet
是由神奇的square
公司開源的,它提供了非常人性化的api,來幫助開發者生成.java原始檔。它的README.md
檔案為我們提供了豐富的例子,是我們學習的主要工具。private Map<TypeElement, List<Element>> elementPackage = new HashMap<>(); private static final String VIEW_TYPE = "android.view.View"; private static final String VIEW_BINDER = "me.zhangkuo.apt.ViewBinding"; @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (set == null || set.isEmpty()) { return false; } elementPackage.clear(); Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class); //收集資料放入elementPackage中 collectData(bindViewElement); //根據elementPackage中的資料生成.java程式碼 generateCode(); return true; } private void collectData(Set<? extends Element> elements){ Iterator<? extends Element> iterable = elements.iterator(); while (iterable.hasNext()) { Element element = iterable.next(); TypeMirror elementTypeMirror = element.asType(); //判斷元素的型別是否是View或者是View的子型別。 TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType(); if (typeUtils.isSubtype(elementTypeMirror, viewTypeMirror) || typeUtils.isSameType(elementTypeMirror, viewTypeMirror)) { //找到父元素,這裡認為是@BindView標記欄位所在的類。 TypeElement parent = (TypeElement) element.getEnclosingElement(); //根據parent不同儲存的List中 List<Element> parentElements = elementPackage.get(parent); if (parentElements == null) { parentElements = new ArrayList<>(); elementPackage.put(parent, parentElements); } parentElements.add(element); }else{ throw new RuntimeException("錯誤處理,BindView應該標註在型別是View的欄位上"); } } } private void generateCode(){ Set<Map.Entry<TypeElement,List<Element>>> entries = elementPackage.entrySet(); Iterator<Map.Entry<TypeElement,List<Element>>> iterator = entries.iterator(); while (iterator.hasNext()){ Map.Entry<TypeElement,List<Element>> entry = iterator.next(); //類元素 TypeElement parent = entry.getKey(); //當前類元素下,註解了BindView的元素 List<Element> elements = entry.getValue(); //通過JavaPoet生成bindView的MethodSpec MethodSpec methodSpec = generateBindViewMethod(parent,elements); String packageName = getPackage(parent).getQualifiedName().toString(); ClassName viewBinderInterface = ClassName.get(elementUtils.getTypeElement(VIEW_BINDER)); String className = parent.getQualifiedName().toString().substring( packageName.length() + 1).replace('.', '$'); ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding"); try { //生成 className_ViewBinding.java檔案 JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName) .addModifiers(PUBLIC) .addSuperinterface(viewBinderInterface) .addMethod(methodSpec) .build() ).build().writeTo(filer); } catch (IOException e) { e.printStackTrace(); } } } private MethodSpec generateBindViewMethod(TypeElement parent,List<Element> elementList) { ParameterSpec.Builder parameter = ParameterSpec.builder(TypeName.OBJECT, "target"); MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView"); bindViewMethod.addParameter(parameter.build()); bindViewMethod.addModifiers(Modifier.PUBLIC); bindViewMethod.addStatement("$T temp = ($T)target",parent,parent); for (Element element : elementList) { int id = element.getAnnotation(BindView.class).value(); bindViewMethod.addStatement("temp.$N = temp.findViewById($L)", element.getSimpleName().toString(), id); } return bindViewMethod.build(); } 複製程式碼
process的
程式碼比較長,但是它的邏輯非常簡單看,主要分為收集資料和生成程式碼兩部分。我為關鍵的地方都加了註釋,就不再詳細解釋了。到這裡我們基本上完成了註解器的編寫工作。- 使用註解
在build.gradle中引入我們定義的註解和註解處理器。
implementation project(':apt_api') annotationProcessor project(":apt_processor") 複製程式碼
應用註解
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_content) TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); tvContent.setText("這就是ButterKnife的原理"); } } 複製程式碼
到這裡,這篇檔案就結束了。什麼?你還沒說
ButterKnife
這個類呢。好吧,這個真的很簡單,直接貼程式碼吧。public class ButterKnife { static final Map<Class<?>, Constructor<? extends ViewBinding>> BINDINGS = new LinkedHashMap<>(); public static void inject(Object object) { if (object == null) { return; } try { Class<?> cls = object.getClass(); Constructor<? extends ViewBinding> constructor = findBindingConstructorForClass(cls); ViewBinding viewBinding = constructor.newInstance(); viewBinding.bindView(object); } catch (Exception e) { } } private static Constructor<? extends ViewBinding> findBindingConstructorForClass(Class<?> cls) throws Exception { Constructor<? extends ViewBinding> constructor = BINDINGS.get(cls); if (constructor == null) { String className = cls.getName(); Class<?> bindingClass = cls.getClassLoader().loadClass(className + "_ViewBinding"); constructor = (Constructor<? extends ViewBinding>) bindingClass.getConstructor(); BINDINGS.put(cls, constructor); } return constructor; } } 複製程式碼