本文開源實驗室原創,轉載請以連接形式註明地址: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 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 生成的類的內容集中到一個統一的類中,在運行時加載這個固定類(事實上咱們就是這麼作的),這樣就能大大提升初始化時的速度了。