本文開源實驗室原創,轉載請以連結形式註明地址:kymjs.com/code/2018/0…
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 生成的類的內容集中到一個統一的類中,在執行時載入這個固定類(事實上我們就是這麼做的),這樣就能大大提高初始化時的速度了。