Android編譯期插樁,讓程序本身寫代碼(一)

前言

近些年,編譯期插樁技術在Android圈愈來愈廣泛。不管是能夠生成JAVA源碼的ButterKnief、Dagger,仍是操做字節碼的VirtualAPK,甚至是新興的語言Kotlin都用到了編譯期插樁技術。學習這門技術對咱們理解這些框架的原理十分有幫助。另外,咱們經過這種技術能夠抽離出複雜、重複的代碼,下降程序耦合性,提升代碼的可複用性,提升開發效率。所以,瞭解編譯期插樁技術十分必要。在介紹這項技術以前,咱們先來了解一下Android代碼的編譯過程以及插樁位置。話很少說,直接上圖。java

APT

APT(Annotation Processing Tool)是一種編譯期註解處理器。它經過定義註解和處理器來實現編譯期生成代碼的功能,而且將生成的代碼和源代碼一塊兒編譯成.class文件。android

表明框架:ButterKnife、Dagger、ARouter、EventBus三、DataBinding、AndroidAnnotation等。git

在介紹如何應用APT技術以前,咱們先來了解一些相關的知識。github

1、Element

1.簡介

Element是一種在編譯期描述.java文件靜態結構的一種類型,它可能表示一個package、一個class、一個method或者一個field。Element的比較應該使用equals,由於編譯期間同一個Element可能會用兩個對象表示。JDK提供瞭如下5種Element編程

2.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樹中。不過咱們能夠經過ExecutableElementgetTypeParameters()方法來獲取。app

此外,再給你們介紹兩個Element中十分有用的方法。框架

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

2、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)
}
複製代碼

3、一個簡單的ButterKnife

這一節咱們經過編寫一個簡單的ButterKnife來介紹一下如何編寫一個APT框架。APT應該是編譯期插樁最簡單的一種技術,經過三步就能夠完成。

  1. 定義編譯期註解。

咱們新增一個Java Library Module命名爲apt_api,編寫註解類BindView。

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

這裏簡單介紹一下RetentionPolicyRetentionPolicy是一個枚舉,它的值有三種:SOURCE、CLASS、RUNTIME。

  • 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;
        }
    }
    複製代碼
相關文章
相關標籤/搜索