1. 程式人生 > >通過編譯時註解生成程式碼實現自己的ButterKnife

通過編譯時註解生成程式碼實現自己的ButterKnife

背景概述

註解的處理除了可以在執行時通過反射機制處理外,還可以在編譯期進行處理。

Java5中提供了apt工具來進行編譯期的註解處理。apt是命令列工具,與之配套的是一套描述“程式在編譯時刻的靜態結構”的API:Mirror API(com.sun.mirror.*)。通過Mirror API可以獲取到被註解的Java型別元素的資訊,從而提供自定義的處理邏輯。具體的處理工具交給apt來處理。編寫註解處理器的核心是兩個類:註解處理器(com.sun.mirror.apt.AnnotationProcessor)、註解處理器工廠(com.sun.mirror.apt.AnnotationProcessorFactory)。apt工具在完成註解處理後,會自動呼叫javac來編譯處理完成後的原始碼。然而,apt工具是oracle提供的私有實現(在JDK開發包的類庫中是不存在的)。

在JDK6中,將註解處理器這一功能進行了規範化,形成了java.annotation.processing的API包,Mirror API則進行封裝,形成javax.lang.model包。

當前註解在Android的開發中的使用越來越普遍,例如EventBus、ButterKnife、Dagger2等。這些都通過註解根據反射機制動態編譯生成程式碼的方式來解決在執行時使用發射機制帶來的效率問題,我們今天利用Android studio的來實現一下自己的ButterKnife UI註解框架。

實戰專案

首先看下專案結構:
這裡寫圖片描述
整個專案包含四個modules,其中app主要是用於測試的Android模組,其他三個分別是:

annotations:主要用於申明app需要用到的註解

api:用於申明註解框架的api

complier:用於在編譯期間通過反射機制自動生成程式碼

annotations模組

定義註解@BindView

這裡我聲明瞭一個BindView註解,宣告週期為CLASS,作用域為成員變數

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

回顧一下RetentionPolicy的三個值含義:

  • SOURCE. 註解保留在原始碼中,但是編譯的時候會被編譯器所丟棄。比如@Override, @SuppressWarnings

  • CLASS. 這是預設的policy。註解會被保留在class檔案中,但是在執行時期間就不會識別這個註解。

  • RUNTIME. 註解會被保留在class檔案中,同時執行時期間也會被識別。所以可以使用反射機制獲取註解資訊。比如@Deprecated

api模組

api宣告框架中使用的api,比如繫結解綁,查詢View控制元件等,這是一個android library。

向用戶提供繫結方法

public class MyViewBinder {
    private static final ActivityViewFinder activityFinder = new ActivityViewFinder();//預設宣告一個Activity View查詢器
    private static final Map<String, ViewBinder> binderMap = new LinkedHashMap<>();//管理保持ViewBinder的Map集合

    /**
     * Activity註解繫結 ActivityViewFinder
     *
     * @param activity
     */
    public static void bind(Activity activity) {
        bind(activity, activity, activityFinder);
    }


    /**
     * '註解繫結
     *
     * @param host   表示註解 View 變數所在的類,也就是註解類
     * @param object 表示查詢 View 的地方,Activity & View 自身就可以查詢,Fragment 需要在自己的 itemView 中查詢
     * @param finder ui繫結提供者介面
     */
    private static void bind(Object host, Object object, ViewFinder finder) {
        String className = host.getClass().getName();
        try {
            ViewBinder binder = binderMap.get(className);
            if (binder == null) {
                Class<?> aClass = Class.forName(className + "$$ViewBinder");
                binder = (ViewBinder) aClass.newInstance();
                binderMap.put(className, binder);
            }
            //呼叫xxx$$ViewBinder類中的bindView方法,對View進行相應的賦值繫結
            if (binder != null) {
                binder.bindView(host, object, finder);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解除註解繫結 ActivityViewFinder
     *
     * @param host
     */
    public static void unBind(Object host) {
        String className = host.getClass().getName();
        ViewBinder binder = binderMap.get(className);
        if (binder != null) {
            binder.unBindView(host);
        }
        binderMap.remove(className);
    }
}

下面是內部需要使用的一些介面和類

/**
 * Created by ason on 2018/1/19.
 * UI繫結解綁介面
 */
public interface ViewBinder<T> {
    void bindView(T host, Object object, ViewFinder finder);
    void unBindView(T host);
}

我們後面生成的類會繼承這個介面,並實現其中的兩個抽象方法。

public interface ViewFinder {
    View findView(Object object, int d);
}
public class ActivityViewFinder implements ViewFinder {
    @Override
    public View findView(Object object, int id) {
        if (!(object instanceof Activity))
        {
            throw new ClassCastException("不能轉換為Activity");
        }
        return ((Activity)object).findViewById(id);
    }
}

complier模組

complier是一個java library,作用是根據註解在編譯期間自動生成java程式碼。

編寫註解處理器類,處理器的核心介面為:javax.annotation.processing.Processor,還提供了一個此介面的實現類:javax.annotation.processing.AbstractProcessor。我們需要繼承AbstractProcessor自來編寫一個處理器類來,用於實現自己的處理邏輯,註解處理器核心處理方法是process()。


@AutoService(Processor.class)
public class ViewBinderProcessor extends AbstractProcessor {
    private Filer mFiler; //檔案相關的輔助類
    private Elements mElementUtils; //元素相關的輔助類
    private Messager mMessager; //日誌相關的輔助類
    private Map<String, AnnotatedClass> mAnnotatedClassMap;

    /**
     * @param processingEnv ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }


    /**
     * process() 方法會被呼叫多次, 直到沒有新的類產生為止.因為新生成的檔案中也可能包含目標註解,它們會繼續被 Processor 處理.
     * process()方法的返回值,若返回false,表示本輪註解未宣告並且可能要求後續其它的Processor處理它們;
     * 若返回true,則代表這些註解已經宣告並且不要求後續Processor來處理它們。
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();
        try {
            processBindView(roundEnv);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            error(e.getMessage());
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateFile().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {

        //roundEnv.getElementsAnnotatedWith(BindView.class)拿到所有被@BindView註解的成員變數
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotatedClass.addField(bindViewField);
        }
    }

    /**
     * 通過Eelment獲取到註解類的全名,使用這個全名從mAnnotatedClassMap找到與其相關的AnnotatedClass類,沒有則建立一個並放入mAnnotatedClassMap
     */
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        String fullName = typeElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(typeElement, mElementUtils);
            mAnnotatedClassMap.put(fullName, annotatedClass);
        }
        return annotatedClass;
    }

    private void error(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }


    /**
     * 指定使用的Java版本,預設為版本6,通常這裡返回SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 指定註解處理器可解析的註解型別,它也可能是 “name.*” 形式的名稱,表示所有以 “name.” 開頭的規範名稱的註釋型別集合。最後,”*” 自身表示所有註釋型別的集合,包括空集。注意,Processor 不應宣告 “*”,除非它實際處理了所有檔案;宣告不必要的註釋可能導致在某些環境中的效能下降。
     * @return 它的返回值是一個字串的集合,包含本處理器想要處理的註解型別的合法全稱。
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }
}

@AutoService:用來幫助我們自動生成META-INF,目錄結構如下:
這裡寫圖片描述
為了便於編譯器註釋工具執行能夠執行ViewBinderProcessor,必須要用一個服務檔案來註冊它。檔案裡面就是寫著Processor的位置。

auto-service需要在配置檔案中配置,build.gradle檔案內容如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //auto-service:用來幫助我們自動生成META-INF
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.8.0'
    implementation project(":annotations")
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

其中 javapoet 提供了各種 API 讓你去生成 Java 程式碼檔案。

然後處理器使用到的工具類Filer、Elements和Messager。

  • Filter 註解處理器可用此建立新檔案(原始檔、類檔案、輔助資原始檔)。由此方法建立的原始檔和類檔案將由管理它們的工具(javac)。
  • Element 程式的元素, 例如包, 類、方法、欄位. 每個 Element 代表一個靜態的,語言級別的構件。
  • Messager
    註解處理器用此來報告錯誤訊息、警告和其他通知的方式。可以為它的方法傳遞元素、註解、註解值,以提供訊息的位置提示,不過,這類位置提示可能是不可用的,或者只是一個大概的提示。列印錯誤種類的日誌將會產生一個錯誤。

AnnotedClass用於處理生成對應類的資訊。

public class AnnotatedClass {
    private static class TypeUtil {
        static final ClassName BINDER = ClassName.get("com.dkw.api", "ViewBinder");
        static final ClassName PROVIDER = ClassName.get("com.dkw.api", "ViewFinder");
    }

    private TypeElement mTypeElement;
    private ArrayList<BindViewField> mFields;
    private Elements mElements;

    AnnotatedClass(TypeElement typeElement, Elements elements) {
        mTypeElement = typeElement;
        mElements = elements;
        mFields = new ArrayList<>();
    }

    void addField(BindViewField field) {
        mFields.add(field);
    }

    /**
     * 通過javapoet生成
     */
    JavaFile generateFile() {
        //generateMethod
        MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.PROVIDER, "finder");

        for (BindViewField field : mFields) {
            // find views
            bindViewMethod.addStatement("host.$N = ($T)(finder.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId());
        }

        MethodSpec.Builder unBindViewMethod = MethodSpec.methodBuilder("unBindView")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                .addAnnotation(Override.class);
        for (BindViewField field : mFields) {
            unBindViewMethod.addStatement("host.$N = null", field.getFieldName());
        }

        //generaClass
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewBinder")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))
                .addMethod(bindViewMethod.build())
                .addMethod(unBindViewMethod.build())
                .build();

        String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile.builder(packageName, injectClass).build();
    }
}

BindViewField物件用於被註解的成員變數

public class BindViewField {
    private VariableElement mVariableElement;
    private int mResId;

    BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mVariableElement = (VariableElement) element;
        BindView bindView = mVariableElement.getAnnotation(BindView.class);
        mResId = bindView.value();
        if (mResId < 0) {
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
                            mVariableElement.getSimpleName()));
        }
    }

    /**
     * 獲取變數名稱
     *
     * @return
     */
    Name getFieldName() {
        return mVariableElement.getSimpleName();
    }

    /**
     * 獲取變數id
     *
     * @return
     */
    int getResId() {
        return mResId;
    }

    /**
     * 獲取變數型別
     *
     * @return
     */
    TypeMirror getFieldType() {
        return mVariableElement.asType();
    }
}

app測試模組

在app模組的build.gradle檔案中新增如下依賴:

    compile project(':api')
    //使用註解
    implementation project(':annotations')
    //對註解的處理
    annotationProcessor project(':compiler')

在Activity中使用

public class MainActivity extends Activity {

    @BindView(R.id.hello)
    TextView hello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyViewBinder.bind(this);
        hello.setText("butterknife");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        MyViewBinder.unBind(this);
    }
}

對應生成的類:

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bindView(MainActivity host, Object source, ViewFinder finder) {
    host.hello = (TextView)(finder.findView(source, 2131165237));
  }

  @Override
  public void unBindView(MainActivity host) {
    host.hello = null;
  }
}

最後說明一下整個執行路程:

  1. 首先在需要使用註解的地方(MainActivity的成員變數)加上註解,在onCreate方法中呼叫bind方法。

  2. 檔案編譯時,找到全部包含註解的類,根據類的資訊生成相應的檔案(MainActivity$$ViewBinder),這個過程在註解處理器process方法是需要被多次處理的,直到沒有新的類生成。

  3. 在上一步中已經將需要繫結的view的id生成到了相應方法當中,在Android系統執行onCreate方法時,呼叫我們的bind方法將我們的view都例項化上。