Android 經過 APT 解耦模塊依賴

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

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

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

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

實現 APT

固然,本文要講的是 APT 的新玩法,講 APT demo 的文章有太多了,你們隨便網上搜一下就一大把,若是會了的同窗,能夠跳過本節。
要實現一個簡單的 APT demo 是很容易的。首先在 idea 中建立一個 Java 工程(因爲 Android Studio 不能直接建立 Java 工程,咱們選用 idea 更簡單)ide

一、首先建立一個咱們須要處理的註解聲明:模塊化

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

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

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

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

二、第二步,咱們爲了以後處理方便,建立一個 JavaBean 用來封裝須要的數據。工具

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

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

public class MyAnnotationProcessor extends AbstractProcessor {
}

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

3.一、重寫兩個方法

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 生成的類的內容集中到一個統一的類中,在運行時加載這個固定類(事實上咱們就是這麼作的),這樣就能大大提升初始化時的速度了。

相關文章
相關標籤/搜索