Android組件化實戰四: APT的介紹與使用

前言

上一篇文章分析了組件化模塊交互的兩種實現方式,對於全局Map保存目標Activity的路徑信息和類對象方案,如果Activity的數量繁多,則需要在主模塊app的application中重複執行多次保存操作,既不優雅,又不符合實際開發場景,我們想到的解決之一就是想辦法生成一個來完成這個重複枯燥的任務,生成一個類來幫我們找到跳轉目標Activity的class對象。好比Butterknife生成一個文件專門完成findViewById的操作一樣。這就涉及到註解處理器即apt技術,接下來就瞭解apt的使用和原理吧

什麼是APT

APT全稱爲Annotation Processing Tool ,翻譯過來就是註解處理器,是一種處理註釋/註解的工具,它對源代碼文件中進行檢測找出其中的Annotation,根據註解自動生成代碼,如果想要自定義的註解處理器能夠正常運行,必須要通過APT工具來進行處理,也可以這樣理解,只有通過聲明APT工具後,程序在編譯期間自定義的註解解釋器才能執行。

通俗理解:根據規則,幫我們生成代碼、生成類文件

結構體語言

如果對HTML有所瞭解,就知道它是又element組成的結構體

<html>
    <body>
        <div>
            ...
        </div>
    </body>
</html>

而對於java源文件來說,它同樣是一種結構體語言,我們不可能把類名寫在包名之上,也不可能把屬性寫在類名之上,這是規則,包名、類名、成員屬性、成員方法,與之相對應的就是程序元素/節點

package com.example.modular_apt;     // PackageElement 包元素/節點

public class User {                  // TypeElement類元素/節點
    private String name;             // VariableElement屬性元素/節點

    public User() {                  // ExecutableElement方法元素/節點
    }
    private void setName(String name) {
        this.name = name;
    }
}

Element程序元素

  • PackageElement: 表示一個包程序元素。提供對有關包及其成員的信息的訪問
  • TypeElement: 表示一個類或接口程序元素。提供對有關類型及其成員的信息的訪問
  • VariableElement: 表示一個字段、enum常量、方法或構造方法參數、局部變量或異常參數
  • ExecutableElement: 表示某個類或接口的方法、構造方法或初始化程序(靜態或實例)

需要掌握的API

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lWJcTISk-1592439507441)(C:\Users\dell\Pictures\Camera Roll\組件化\APT涉及的api.png)]

創建工程實戰

構建app module, 在該module中有MainActivity、OrderActivity、PersonalActivity三個Activity,通過apt技術即註解處理器,根據對應的註解,生成類文件,獲取對應Activity的class對象。這樣即使在不同的模塊,只要拿到了目標Activity的class對象,就可以進行跳轉了。

構建java library annotation 在該java庫中有一個類註解ARouter,註解處理器庫就是根據這個註解的參數,獲取類名。

@Target(ElementType.TYPE) // 該註解作用在類之上
@Retention(RetentionPolicy.CLASS) // 要在編譯時進行一些預處理操作。註解會在class文件中存在
public @interface ARouter {
    // 詳細路由路徑(必填),如:"/app/MainActivity"
    String path();

    // 路由組名(選填,如果開發者不填寫,可以從path中截取出來)
    String group() default "";
}

構建java library compiler,在該java庫中也只有一個核心類ARouterProcessor,即註解處理類。核心方法就是process,在下面代碼中,該方法的主要邏輯就是,遍歷所有使用了類註解ARouter的類集合set,根據類節點(信息)獲取包節點(信息),然後根據包節點獲取類名,如果等於註解傳遞的參數中的類名,就生成對應的文件,具體邏輯如下:

// AutoService則是固定的寫法,加個註解即可
// 通過auto-service中的@AutoService可以自動生成AutoService註解處理器,用來註冊
// 用來生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
// 允許/支持的註解類型,讓註解處理器處理(新增annotation module)
@SupportedAnnotationTypes({"com.example.annotation.ARouter"})
// 指定JDK編譯版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 註解處理器接收的參數
@SupportedOptions("content")
public class ARouterProcessor extends AbstractProcessor {

    // 操作Element工具類 (類、函數、屬性都是Element)
    private Elements elementUtils;

    // type(類信息)工具類,包含用於操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用來報告錯誤,警告和其他提示信息
    private Messager messager;

    // 文件生成器 類/資源,Filter用來創建新的源文件,class文件以及輔助文件
    private Filer filer;

    // 該方法主要用於一些初始化的操作,通過該方法的參數ProcessingEnvironment可以獲取一些列有用的工具類
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 父類受保護屬性,可以直接拿來使用。
        // 其實就是init方法的參數ProcessingEnvironment
        // processingEnv.getMessager(); //參考源碼64行
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();

        // 通過ProcessingEnvironment去獲取build.gradle(app module)傳過來的參數
        String content = processingEnvironment.getOptions().get("content");
        // 有坑:Diagnostic.Kind.ERROR,異常會自動結束,不像安卓中Log.e那麼好使
        messager.printMessage(Diagnostic.Kind.NOTE, content);
    }

    /**
     * 相當於main函數,開始處理註解
     * 註解處理器的核心方法,處理具體的註解,生成Java文件
     *
     * @param set              使用了支持處理註解的節點集合(類 上面寫了註解)
     * @param roundEnvironment 當前或是之前的運行環境,可以通過該對象查找找到的註解。
     * @return true            表示後續處理器不會再處理(已經處理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // 獲取所有帶ARouter註解的 類節點
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // 遍歷所有類節點
        for (Element element : elements) {
            // 通過類節點獲取包節點(也就是包名:com.example.xxx)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            // 獲取簡單類名(不帶包名)
            String className = element.getSimpleName().toString();
            messager.printMessage(Diagnostic.Kind.NOTE, "被註解的類有:" + className);
            // 最終想生成的類文件名
            String finalClassName = className + "$$ARouter";

            // 公開課寫法,也是EventBus寫法(https://github.com/greenrobot/EventBus)
            try {
                // 創建一個新的源文件(Class),並返回一個對象以允許寫入它
                JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);
                // 定義Writer對象,開啓寫入
                Writer writer = sourceFile.openWriter();
                // 設置包名
                writer.write("package " + packageName + ";\n");

                writer.write("public class " + finalClassName + " {\n");

                writer.write("public static Class<?> findTargetClass(String path) {\n");

                // 獲取類之上@ARouter註解的path值
                ARouter aRouter = element.getAnnotation(ARouter.class);

                writer.write("if (path.equalsIgnoreCase(\"" + aRouter.path() + "\")) {\n");

                writer.write("return " + className + ".class;\n}\n");

                writer.write("return null;\n");

                writer.write("}\n}");

                // 最後結束別忘了
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }
}

生成對應的java文件如下:

public class MainActivity$$ARouter {
    public static Class<?> findTargetClass(String path) {
        if (path.equalsIgnoreCase("/app/MainActivity")) {
            return MainActivity.class;
        }
        return null;
    }
}

同理,只要在OrderActivity和PersonalActivity分別添加對應的類註解,@ARouter(path = “/app/OrderActivity”)和@ARouter(path = “/app/PersonalActivity”),也會生成對應的java文件OrderActivity $ $ ARouter和PersonalActivity$$ ARouter。

此時從MainActivity跳轉OrderActivity的邏輯如下:

Class<?> targetClass = OrderActivity$$ARouter.findTargetClass("/app/OrderActivity");
startActivity(new Intent(this, targetClass));

表明上看這是多此一舉,直接用下面的方式跳轉不香嗎呢?原因是如果目標Activity在不同的子模塊,即子模塊相互沒有依賴關係,是無法進行下面的操作的。這裏爲了便於演示,所有將三個Activity放在一個模塊中了。

startActivity(new Intent(this, OrderActivity.class));

注意事項

  • 註解處理器依賴
//註解處理器依賴
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
  • 亂碼問題
// java控制檯輸出中文亂碼
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
  • 傳參問題
defaultConfig {
    ...
    //在gradle文件中配置選項參數值(用於APT傳參接收)
    //切記:必須寫在defaultConfig節點下
    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [content: 'hello apt']
        }
    }
}

代碼鏈接:https://github.com/xpf-android/Modular_APT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章