近些年,編譯期插樁技術在Android圈愈來愈廣泛。不管是能夠生成JAVA源碼的ButterKnief、Dagger,仍是操做字節碼的VirtualAPK,甚至是新興的語言Kotlin都用到了編譯期插樁技術。學習這門技術對咱們理解這些框架的原理十分有幫助。另外,咱們經過這種技術能夠抽離出複雜、重複的代碼,下降程序耦合性,提升代碼的可複用性,提升開發效率。所以,瞭解編譯期插樁技術十分必要。在介紹這項技術以前,咱們先來了解一下Android代碼的編譯過程以及插樁位置。話很少說,直接上圖。java
APT(Annotation Processing Tool)是一種編譯期註解處理器。它經過定義註解和處理器來實現編譯期生成代碼的功能,而且將生成的代碼和源代碼一塊兒編譯成.class文件。android
表明框架:ButterKnife、Dagger、ARouter、EventBus三、DataBinding、AndroidAnnotation等。git
在介紹如何應用APT技術以前,咱們先來了解一些相關的知識。github
Element
是一種在編譯期描述.java文件靜態結構的一種類型,它可能表示一個package、一個class、一個method或者一個field。Element
的比較應該使用equals
,由於編譯期間同一個Element
可能會用兩個對象表示。JDK提供瞭如下5種Element
。 編程
編譯器採用相似Html的Dom樹來存儲Element。咱們用下面的Test.java
來具體說明。api
//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()
方法來獲取。app
此外,再給你們介紹兩個Element中十分有用的方法。框架
public interface Element extends AnnotatedConstruct {
//獲取父Element
Element getEnclosingElement();
//獲取子Element的集合
List<? extends Element> getEnclosedElements();
}
複製代碼
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
來介紹一下如何編寫一個APT框架。APT應該是編譯期插樁最簡單的一種技術,經過三步就能夠完成。
咱們新增一個Java Library Module命名爲apt_api
,編寫註解類BindView。
@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}
複製代碼
這裏簡單介紹一下RetentionPolicy
。RetentionPolicy
是一個枚舉,它的值有三種:SOURCE、CLASS、RUNTIME。
一樣,咱們須要新增一個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;
}
}
複製代碼