Android APT快速教程

Android APT快速教程

簡介

APT(Annotation Processing Tool)即註解處理器,是一種用來處理註解的工具。JVM會在編譯期就運行APT去掃描處理代碼中的註解而後輸出java文件。java

image

簡單來講~~就是你只須要添加註解,APT就能夠幫你生成須要的代碼git

許多的Android開源庫都使用了APT技術,如ButterKnife、ARouter、EventBus等程序員

動手實現一個簡單的APT

image

小目標

在使用Java開發Android時,頁面初始化的時候咱們一般要寫大量的view = findViewById(R.id.xx)代碼github

做爲一個優(lan)秀(duo)的程序員,咱們如今就要實現一個APT來完成這個繁瑣的工做,經過一個註解就能夠自動給View得到實例api

本demo地址bash

第零步 建立一個項目

建立一個項目,名稱叫 apt_demo 建立一個Activity,名稱叫 MainActivity 在佈局中添加一個TextView, id爲test_textviewapp


image

第一步 自定義註解

建立一個Java Library Module名稱叫 apt-annotation框架

在這個module中建立自定義註解 @BindViewide

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
複製代碼
  • @Retention(RetentionPolicy.CLASS):表示這個註解保留到編譯期
  • @Target(ElementType.FIELD):表示註解範圍爲類成員(構造方法、方法、成員變量)

第二步 實現APT Compiler

建立一個Java Library Module名稱叫 apt-compiler工具

在這個Module中添加依賴

dependencies {
    implementation project(':apt-annotation')
}
複製代碼

image

在這個Module中建立BindViewProcessor

直接給出代碼~~

public class BindViewProcessor extends AbstractProcessor {
    private Filer mFilerUtils;       // 文件管理工具類
    private Types mTypesUtils;    // 類型處理工具類
    private Elements mElementsUtils;  // Element處理工具類

    private Map<TypeElement, Set<ViewInfo>> mToBindMap = new HashMap<>(); //用於記錄須要綁定的View的名稱和對應的id

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFilerUtils = processingEnv.getFiler();
        mTypesUtils = processingEnv.getTypeUtils();
        mElementsUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes; //將要支持的註解放入其中
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();// 表示支持最新的Java版本
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        System.out.println("start process");
        if (set != null && set.size() != 0) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView註解標記的element

            categories(elements);//對不一樣的Activity進行分類

            //對不一樣的Activity生成不一樣的幫助類
            for (TypeElement typeElement : mToBindMap.keySet()) {
                String code = generateCode(typeElement);    //獲取要生成的幫助類中的全部代碼
                String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //構建要生成的幫助類的類名

                //輸出幫助類的java文件,在這個例子中就是MainActivity$$Autobind.java文件
                //輸出的文件在build->source->apt->目錄下
                try {
                    JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
                    Writer writer = jfo.openWriter();
                    writer.write(code);
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            return true;
        }

        return false;
    }

    //將須要綁定的View按不一樣Activity進行分類
    private void categories(Set<? extends Element> elements) {
        for (Element element : elements) {  //遍歷每個element
            VariableElement variableElement = (VariableElement) element;    //被@BindView標註的應當是變量,這裏簡單的強制類型轉換
            TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //獲取表明Activity的TypeElement
            Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views儲存着一個Activity中將要綁定的view的信息
            if (views == null) {    //若是views不存在就new一個
                views = new HashSet<>();
                mToBindMap.put(enclosingElement, views);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);    //獲取到一個變量的註解
            int id = bindAnnotation.value();    //取出註解中的value值,這個值就是這個view要綁定的xml中的id
            views.add(new ViewInfo(variableElement.getSimpleName().toString(), id));    //把要綁定的View的信息存進views中
        }

    }

    //按不一樣的Activity生成不一樣的幫助類
    private String generateCode(TypeElement typeElement) {
        String rawClassName = typeElement.getSimpleName().toString(); //獲取要綁定的View所在類的名稱
        String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //獲取要綁定的View所在類的包名
        String helperClassName = rawClassName + "$$Autobind";   //要生成的幫助類的名稱

        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n");   //構建定義包的代碼
        builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //構建import類的代碼

        builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper");   //構建定義幫助類的代碼
        builder.append(" {\n"); //代碼格式,能夠忽略
        builder.append("\t@Override\n");    //聲明這個方法爲重寫IBindHelper中的方法
        builder.append("\tpublic void inject(" + "Object" + " target ) {\n");   //構建方法的代碼
        for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍歷每個須要綁定的view
            builder.append("\t\t"); //代碼格式,能夠忽略
            builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n");    //強制類型轉換

            builder.append("\t\t"); //代碼格式,能夠忽略
            builder.append("substitute." + viewInfo.viewName).append(" = ");    //構建賦值表達式
            builder.append("substitute.findViewById(" + viewInfo.id + ");\n");  //構建賦值表達式
        }
        builder.append("\t}\n");    //代碼格式,能夠忽略
        builder.append('\n');   //代碼格式,能夠忽略
        builder.append("}\n");  //代碼格式,能夠忽略

        return builder.toString();
    }

    //要綁定的View的信息載體
    class ViewInfo {
        String viewName;    //view的變量名
        int id; //xml中的id

        public ViewInfo(String viewName, int id) {
            this.viewName = viewName;
            this.id = id;
        }
    }
}

複製代碼

image

image

2-1 繼承AbstractProcessor抽象類

咱們本身實現的APT類須要繼承AbstractProcessor這個類,其中須要重寫如下方法:

  • init(ProcessingEnvironment processingEnv)
  • getSupportedAnnotationTypes()
  • getSupportedSourceVersion()
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

2-2 init(ProcessingEnvironment processingEnv)方法

這不是一個抽象方法,但progressingEnv參數給咱們提供了許多有用的工具,因此咱們須要重寫它

@Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFilerUtils = processingEnv.getFiler();
        mTypesUtils = processingEnv.getTypeUtils();
        mElementsUtils = processingEnv.getElementUtils();
    }
複製代碼
  • mFilterUtils 文件管理工具類,在後面生成java文件時會用到
  • mTypesUtils 類型處理工具類,本例不會用到
  • mElementsUtils Element處理工具類,後面獲取包名時會用到

限於篇幅就不展開對這幾個工具的解析,讀者能夠自行查看文檔~

2-3 getSupportedAnnotationTypes()

由方法名咱們就能夠看出這個方法是提供咱們這個APT可以處理的註解

這也不是一個抽象方法,但仍須要重寫它,不然會拋出異常(滑稽),至於爲何有興趣能夠自行查看源碼~

這個方法有固定寫法~

@Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes; //將要支持的註解放入其中
    }
複製代碼

2-4 getSupportedSourceVersion()

顧名思義,就是提供咱們這個APT支持的版本號

這個方法和上面的getSupportedAnnotationTypes()相似,也不是一個抽象方法,但也須要重寫,也有固定的寫法

@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();// 表示支持最新的Java版本
    }
複製代碼

2-5 process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

最主要的方法,用來處理註解,這也是惟一的抽象方法,有兩個參數

  • set 參數是要處理的註解的類型集合
  • roundEnv表示運行環境,能夠經過這個參數得到被註解標註的代碼塊
@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        System.out.println("start process");
        if (set != null && set.size() != 0) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView註解標記的element

            categories(elements);//對不一樣的Activity進行分類

            //對不一樣的Activity生成不一樣的幫助類
            for (TypeElement typeElement : mToBindMap.keySet()) {
                String code = generateCode(typeElement);    //獲取要生成的幫助類中的全部代碼
                String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //構建要生成的幫助類的類名

                //輸出幫助類的java文件,在這個例子中就是MainActivity$$Autobind.java文件
                //輸出的文件在build->source->apt->目錄下
                try {
                    JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
                    Writer writer = jfo.openWriter();
                    writer.write(code);
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            return true;
        }
        return false;
    }
複製代碼

process方法的大概流程是:

  1. 掃描全部被@BindView標記的Element
  2. 遍歷Element,調用categories方法,把全部須要綁定的View變量按所在的Activity進行分類,把對應關係存在mToBindMap中
  3. 遍歷全部Activity的TypeElment,調用generateCode方法得到要生成的代碼,每一個Activity生成一個幫助類

2-6 categories方法

把全部須要綁定的View變量按所在的Activity進行分類,把對應關係存在mToBindMap中

private void categories(Set<? extends Element> elements) {
        for (Element element : elements) {  //遍歷每個element
            VariableElement variableElement = (VariableElement) element;    //被@BindView標註的應當是變量,這裏簡單的強制類型轉換
            TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //獲取表明Activity的TypeElement
            Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views儲存着一個Activity中將要綁定的view的信息
            if (views == null) {    //若是views不存在就new一個
                views = new HashSet<>();
                mToBindMap.put(enclosingElement, views);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);    //獲取到一個變量的註解
            int id = bindAnnotation.value();    //取出註解中的value值,這個值就是這個view要綁定的xml中的id
            views.add(new ViewInfo(variableElement.getSimpleName().toString(), id));    //把要綁定的View的信息存進views中
        }
    }
複製代碼

註解應該已經很詳細了~
實現方式僅供參考,讀者能夠有本身的實現

2-7 generateCode方法

按照不一樣的TypeElement生成不一樣的幫助類(注:參數中的TypeElement對應一個Activity)

private String generateCode(TypeElement typeElement) {
        String rawClassName = typeElement.getSimpleName().toString(); //獲取要綁定的View所在類的名稱
        String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //獲取要綁定的View所在類的包名
        String helperClassName = rawClassName + "$$Autobind";   //要生成的幫助類的名稱

        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n");   //構建定義包的代碼
        builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //構建import類的代碼

        builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper");   //構建定義幫助類的代碼
        builder.append(" {\n"); //代碼格式,能夠忽略
        builder.append("\t@Override\n");    //聲明這個方法爲重寫IBindHelper中的方法
        builder.append("\tpublic void inject(" + "Object" + " target ) {\n");   //構建方法的代碼
        for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍歷每個須要綁定的view
            builder.append("\t\t"); //代碼格式,能夠忽略
            builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n");    //強制類型轉換

            builder.append("\t\t"); //代碼格式,能夠忽略
            builder.append("substitute." + viewInfo.viewName).append(" = ");    //構建賦值表達式
            builder.append("substitute.findViewById(" + viewInfo.id + ");\n");  //構建賦值表達式
        }
        builder.append("\t}\n");    //代碼格式,能夠忽略
        builder.append('\n');   //代碼格式,能夠忽略
        builder.append("}\n");  //代碼格式,能夠忽略

        return builder.toString();
    }
複製代碼

你們能夠對比生成的代碼

package com.example.apt_demo;
import com.example.apt_api.template.IBindHelper;

public class MainActivity$$Autobind implements IBindHelper {
	@Override
	public void inject(Object target ) {
		MainActivity substitute = (MainActivity)target;
		substitute.testTextView = substitute.findViewById(2131165315);
	}

}
複製代碼

有沒有以爲字符串拼接很麻煩呢,不只麻煩還容易出錯,那麼有沒有更好的辦法呢(留個坑)

一樣的~ 這個方法的設計僅供參考啦啦啦~~~

image

第三步 註冊你的APT

這應該是最簡單的一步
這應該是最麻煩的一步

image

客官別急,往下看就知道了~

註冊一個APT須要如下步驟:

  1. 須要在 processors 庫的 main 目錄下新建 resources 資源文件夾;
  2. 在 resources文件夾下創建 META-INF/services 目錄文件夾;
  3. 在 META-INF/services 目錄文件夾下建立 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件寫入註解處理器的全稱,包括包路徑;

image

正如我前面所說的~
簡單是由於都是一些固定的步驟
麻煩也是由於都是一些固定的步驟

最後一步 對外提供API

4.1 建立一個Android Library Module,名稱叫apt-api,並添加依賴

dependencies {
    ...

    api project(':apt-annotation')
}
複製代碼

image

4.2 分別建立launcher、template文件夾

image

4.3 在template文件夾中建立IBindHelper接口

public interface IBindHelper {
    void inject(Object target);
}
複製代碼

這個接口主要供APT生成的幫助類實現

4.4在launcher文件夾中建立AutoBind

public class AutoBind {
        private static AutoBind instance = null;

        public AutoBind() {
        }

        public static AutoBind getInstance() {
            if(instance == null) {
                synchronized (AutoBind.class) {
                    if (instance == null) {
                        instance = new AutoBind();
                    }
                }
            }
            return instance;
        }

        public void inject(Object target) {
            String className = target.getClass().getCanonicalName();
            String helperName = className + "$$Autobind";
            try {
                IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
                helper.inject(target);
            }   catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
複製代碼

這個類是咱們的API的入口類,使用了單例模式
inject方法是最主要的方法,但實現很簡單,就是經過反射去調用APT生成的幫助類的方法去實現View的自動綁定

完成!拉出來遛遛

在app module裏添加依賴

dependencies {
    ...
    annotationProcessor project(':apt-compiler')
    implementation project(':apt-api')
}
複製代碼

image
咱們來修改MainActivity中的代碼

public class MainActivity extends AppCompatActivity {

    @BindView(value = R.id.test_textview)
    public TextView testTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AutoBind.getInstance().inject(this);
        testTextView.setText("APT 測試");
    }
}
複製代碼

大功告成!咱們來運行項目試試看

image

TextView已經正確顯示了文字,咱們的小demo到這裏就完成啦~

還能夠更好

咱們的APT還能夠變得更簡單!

痛點

  • 生成代碼時字符串拼接麻煩且容易出錯
  • 繼承AbstrctProcessor時要重寫多個方法
  • 註冊APT的步驟繁瑣

下面咱們來逐個擊破~

使用JavaPoet來替代拼接字符串

JavaPoet是一個用來生成Java代碼的框架,對JavaPoet不瞭解的請移步官方文檔
JavaPoet生成代碼的步驟大概是這樣(摘自官方文檔):

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);
複製代碼

使用JavaPoet來生成代碼有不少的優勢,不容易出錯,能夠自動import等等,這裏不過多介紹,有興趣的同窗能夠自行了解

使用註解來代替getSupportedAnnotationTypes()和getSupportedSourceVersion()

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
public class BindViewProcessor extends AbstractProcessor {
    ...
}
複製代碼

這是javax.annotation.processing中提供的註解,直接使用便可

使用Auto-Service來自動註冊APT

這是谷歌官方出品的一個開源庫,能夠省去註冊APT的步驟,只須要一行註釋
先在apt-compiler模塊中添加依賴

dependencies {
    ...
    
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
複製代碼

而後添加註釋便可

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    ...
}
複製代碼

總結

APT是一個很是強大並且頻繁出如今各類開源庫的工具,學習APT不只可讓咱們在閱讀開源庫源碼時遊刃有餘也能夠本身開發註解框架來幫本身寫代碼~ 本demo地址

image
相關文章
相關標籤/搜索