Android 通過 APT 解耦模塊依賴 原 薦

本文開源實驗室原創,轉載請以鏈接形式註明地址:https://kymjs.com/code/2018/08/12/01
Android APT 的新玩法,生成類的特殊加載方式。在 Android 多 module 工程中使用 APT,會出現類衝突問題,如果你也碰上這種問題,希望本文對你有所幫助。

對本文有任何問題,可加我的個人微信:kymjs123

APT 是什麼?Annotation Process Tool,註解處理工具。
這本是 Java 的一個工具,但 Android 也可以使用,他可以用來處理編譯過程時的某些操作,比如 Java 文件的生成,註解的獲取等。

在 Android 上,我們使用 APT 通常是爲了生成某些處理標註有指定註解的方法、類或變量,比如 EventBus3.0開始,就是使用 APT 去處理onEvent 註解的;dagger2、butterknife 等著名的開源庫也都是使用 APT 去實現的。再舉一個大家非常熟悉的實際使用場景:在 Android 模塊化重構的過程中,就會需要大量用到 APT 去生成作爲跨模塊轉發層的中間類,在我之前講《餓了麼模塊化平臺設計》中的鐵金庫 IronBank 就大量使用了 APT 與 AOP 技術去實現跨模塊的處理工作。

實現 APT

當然,本文要講的是 APT 的新玩法,講 APT demo 的文章有太多了,大家隨便網上搜一下就一大把,如果會了的同學,可以跳過本節。
要實現一個簡單的 APT demo 是很容易的。首先在 idea 中創建一個 Java 工程(由於 Android Studio 不能直接創建 Java 工程,我們選用 idea 更簡單)

1、首先創建一個我們需要處理的註解聲明:

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
public @interface Produce {

    Class<?> returnType() default Produce.class;

    Class<?>[] params() default {};
}

關於註解類的創建以及上面各個給註解類加註解的含義,在我很早之前的一篇博客《Android註解式綁定控件,沒你想象的那麼難》中已經有很詳細的介紹了,不知道的同學可以再去看一看。

2、第二步,我們爲了之後處理方便,創建一個 JavaBean 用來封裝需要的數據。

class ItemData {
    Element element;
    String className = "";
    String returnType = "";
    String methodName = "";
    String[] params = {};
}

3、最後就是最重要的一個類了:註解是處理方式

public class MyAnnotationProcessor extends AbstractProcessor {
}

所有的註解處理類必須繼承自系統的AbstractProcessor,如果想要讓這個註解處理類生效,還要在我們的工程中創建一個 meta 文件,meta 文件中寫好要提供註解處理功能的那個類的包名+類名。比如我的是這樣寫的:
開源實驗室

3.1、重寫兩個方法

public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportTypes = new HashSet<>();
        supportTypes.add(Produce.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        boolean isProcess = false;
        try {
            isProcess = true;
            List<ItemData> creatorList = parseProduce(roundEnvironment);
            genJavaFile(creatorList);
        } catch (Exception e) {
            isProcess = false;
        }
        return isProcess;
    }
}

getSupportedAnnotationTypes是用來告訴 APT,我要關注的註解類型是哪些類型。這裏只有一個註解@Produce所以我們的 set 就只添加了一個類型。
process()就是真正用於處理註解的函數,這裏我是通過parseProduce()返回了所有被@Produce修飾的方法的信息,就是我們前面封裝的 JavaBean,包含了方法所在類名、方法返回值、方法名、方法參數等信息。
然後再通過genJavaFile()去生成方法對應的跨模塊的中間類。

生成類文件

在 APT 中,要生成一個類辦法有很多,比如讀取某個 Java 文件模板,將文件內的類模板轉換成目標代碼;可以使用square公司開源的javapoet庫,通過傳參直接輸出目標類文件;也可以最簡單的直接通過輸出流將一個 Java 代碼字符串輸出到文件中。

比如,寫 demo 我就直接用輸出 Java 字符串的辦法了。(代碼節選,刪掉多餘類聲明、try...catch)

private void genJavaFile(List<Item> pageList) {
    JavaFileObject jfo = processingEnv.getFiler().createSourceFile(PACKAGE + POINT + className);
    PrintStream ps = new PrintStream(jfo.openOutputStream());
    ps.println(String.format("public class %s implements com.kymjs.Interceptor {", className));
    ps.println("\tpublic <T> T interception(Class<T> clazz, Object... params) {");

    for (Item item : pageList) {
        ps.print(String.format("if (%s.class.equals(clazz)", item.returnType));
        // 省略多參數判斷邏輯
        for (int count = 0; count < item.params.length; count++) {

        }
        ps.println(") {");
        ps.print(String.format("\t\t\tobj = (T) %s.%s(", item.className, item.methodName));
        // 參數類型判斷邏輯
        for (int count = 0; count < item.params.length; count++) {

        }
        ps.println(");} else ");
    }
    ps.println("{\n}return obj;}}");
    ps.flush();
}

最終,就會在工程目錄下生成類似這樣的一個文件:開源實驗室

運行時加載類

本節介紹的內容,相關詳細內容建議優先閱讀:《優雅移除模塊間耦合》這篇我在 droidcon 大會上分享的文字稿。
新類生成好了以後,自然需要讓生成的類生效,通常我們之間使用 ClassLoader 加載我們生成好的類。而在生效之前的編譯階段,會碰上一個很大的問題:普通的單 module 的 Android 工程使用 APT 不會有任何問題,但是多 module 使用的時候就會發生每個 module 都有一個包名類名完全相同的生成類,這就會發生類衝突了。

最簡單的解決類衝突的辦法就是讓每次生成的類,類名都不一樣。
比如你可以講類的文件加一個 hashcode或者隨機數後綴,這樣就基本能避免類衝突問題了(只能說基本,畢竟hashcode、random也有重複的機率)。

但是如果類名不一樣的話,如何在運行時通過 ClassLoader 加載一個不知道類名的類呢?有兩種辦法,一種是通過接口遍歷,給每個 APT 生成的類一個空接口父類,在運行時遍歷所有類的父接口,是否是這個接口的,如果是就用ClassLoader加載他;另一種辦法是通過類前綴,比如讓所有類都有一個特殊的前綴,在運行時就能知道所有 APT 生成類了。
這種方法對應的代碼我可以給大家看一下(節選,刪掉某些不重要的代碼):

private void getAllDI(Context context) {
    mInterceptors.writeLock().lock();
    try {
        ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        String path = info.sourceDir;
        DexFile dexfile = new DexFile(path);
        Enumeration entries = dexfile.entries();
        byte isLock = NONE;

        while (entries.hasMoreElements()) {
            String name = (String) entries.nextElement();
            if (name.startsWith(PACKAGE + "." + SUFFIX)) {
                threadIsRunned = true;
                if (isLock <= 0) {
                    mInterceptors.writeLock().lock();
                    isLock = LOCK;
                }
                Class clazz = Class.forName(name);
                if (Interceptor.class.isAssignableFrom(clazz) && !Interceptor.class.equals(clazz)) {
                    mInterceptors.add((Interceptor) clazz.newInstance());
                }
            } else {
                if (isLock > 0) {
                    mInterceptors.writeLock().unlock();
                    isLock = UNLOCK;
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        mInterceptors.writeLock().unlock();
    }
}

由於遍歷所有類是一個耗時操作,所以通常我們將其放在線程中,因此還需要保證多個線程的線程安全問題,防止類還沒有被 ClassLoader 加載,就已經去訪問這個類的情況。

另一種實現方式就是通過額外的 gradle 插件,在編譯期講所有 APT 生成類找到,記錄到某個類中,這樣就可以在加載的時候避免遍歷所有類這步耗時操作。或者,如果實際需求中 APT 生成類中的內容是允許亂序的,比如本例中將所有類中加了@Produce 註解的方法記錄下來這樣的操作,也可以在編譯期,將所有 APT 生成的類的內容集中到一個統一的類中,在運行時加載這個固定類(事實上我們就是這麼做的),這樣就能大大提高初始化時的速度了。

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