Android編譯期插樁,讓程式自己寫程式碼(一)

青黴素發表於2019-04-02

前言

近些年,編譯期插樁技術在Android圈越來越普遍。無論是可以生成JAVA原始碼的ButterKnief、Dagger,還是操作位元組碼的VIrtualAPK,甚至是新興的語言Kotlin都用到了編譯期插樁技術。學習這門技術對我們理解這些框架的原理十分有幫助。另外,我們通過這種技術可以抽離出複雜、重複的程式碼,降低程式耦合性,提高程式碼的可複用性,提高開發效率。因此,瞭解編譯期插樁技術十分必要。在介紹這項技術之前,我們先來了解一下Android程式碼的編譯過程以及插樁位置。話不多說,直接上圖。

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

Android編譯期插樁,讓程式自己寫程式碼(一)

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樹結構描述如下:

Android編譯期插樁,讓程式自己寫程式碼(一)

我們可以看到 setName(String name)ExecutableElement中並沒有子節點TypeParameterElement。這是因為TypeParameterElement沒有被納入到Element樹中。不過我們可以通過ExecutableElementgetTypeParameters()方法來獲取。

此外,再給大家介紹兩個Element中十分有用的方法。

public interface Element extends AnnotatedConstruct {
    //獲取父Element
    Element getEnclosingElement();
    //獲取子Element的集合
    List<? extends Element> getEnclosedElements();
}
複製程式碼

二、TypeMirror

Element有一個asType()方法用來返回TypeMirrorTypeMirror表示 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應該是編譯期插樁最簡單的一種技術,通過三步就可以完成。

  1. 定義編譯期註解。

我們新增一個Java Library Module命名為apt_api

編寫註解類。

@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}
複製程式碼

這裡簡單介紹一下RetentionPolicy各型別的區別。

  • SOURCE:不參與編譯,讓開發者看的。
  • CLASS:參與編譯,執行時不可見。給編譯器看的。
  • RUNTIME:參與編譯,執行時可見。給JVM看的。
  1. 定義註解處理器。

同樣,我們需要新增一個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提供的四個方法:getSupportedAnnotationTypesgetSupportedSourceVersioninitprocess

  • getSupportedAnnotationTypes表示處理器可以處理哪些註解。這裡返回的是我們之前定義的BindView。除了重寫方法之外,還可用通過註解來實現。

    @SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"})
    複製程式碼
  • getSupportedSourceVersion表示處理器可以處理的Java版本。 這裡我們採用最新的JDK版本就可以了。同樣,我們也可以通過註解來實現。

    @SupportedSourceVersion(value = SourceVersion.latestSupported())
    複製程式碼
  • init方法主要用來做一些準備工作。我們一般在這裡初始化幾個工具類。上述程式碼我們初始了與元素相關的工具類elementUtils、與日誌相關的工具類messager、與檔案相關的filer以及與型別相關工具類typeUtils。我們接下來會看到process主要就是通過這幾個類來生成程式碼的。

  • process用來完成具體的程式寫程式碼功能。在具體介紹process之前,請允許我先推薦一個庫:javapoetjavapoet是由神奇的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的程式碼比較長,但是它的邏輯非常簡單看,主要分為收集資料和生成程式碼兩部分。我為關鍵的地方都加了註釋,就不再詳細解釋了。到這裡我們基本上完成了註解器的編寫工作。

    1. 使用註解

    在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;
        }
    }
    複製程式碼

相關文章