引言
一般來說,我們在項目開發中,功能性類似的同一層級,會有許多相同邏輯。很多時候,一個簡單有效的方法,就是定義base類,比如我們已經司空見慣以至于寫習慣了的BaseActivity、BaseFragment、BasePresenter等,然后在子類對應的流程節點中,對super進行調用。
而在父類中實現的這些邏輯,被設計之初就是對應的某些特定的切面(Aspect),可以稱作AOP思想的一種體現。
但在某些情況下,很多邏輯并不適合放在Activity基類中,在無法多繼承的限制下,就需要通過代理或包裝的方式來實現解耦,例如androidx中的LifeCycle相關框架。
同樣的,面向對象的思想中,組合也是優于繼承的。
特別是對于一些可以高度抽象、代碼高度一致的情況,為了避免大量的重復性代碼,可以通過APT或ASM進行代碼的自動生成,或者對編譯完的class進行字節碼操作插樁。今天主要來聊一聊APT的使用。
APT 是什么
APT 全稱 Annotation Processing Tool,即注解處理器,APT 工具是用于注解處理的命令行程序,它可以找到源碼中對應注解的對象(類、方法、字段)并使用注解處理器對其進行處理。
進而根據需求在編譯期生成一些附加的代碼,然后與源碼共同進行編譯。最后打包到一起。
注解處理器是基于注解(Annotation)的,需要定義一些注解,對應到想要處理的對象。例如ButterKnife對字段的注入,需要定義字段上的注解;例如ARouter對頁面的關聯,需要定義類上的注解。
然后利用 javax.annotation.processing.Processor 對代碼中帶有對應注解的部分進行掃描,拿到注解下的類信息、字段信息、方法信息,按想要的邏輯進行自動編碼,生成供后期調用的邏輯。
路由框架要解決什么問題
在單項目單模塊的開發場景中,activity之間的相互跳轉沒有任何障礙,彼此間的代碼都是可見的,如果需要統一跳轉邏輯,方便review路由線路,也只需要一個工具類,把頁面引用、需要的參數進行簡單封裝而已。
但在模塊化的開發中,模塊間沒有互相依賴,無法感知其他模塊中的具體類型,就無法拿到構建intent需要的Activity.class。
當然也可以定義公用的字符串常量,使用反射進行class的加載。但反射的性能較差,固定的少量使用還好,但隨著項目增大,頁面數量上漲,頁面間數據交互復雜化,過多的反射使用就可能成為性能瓶頸。
而且,既然都要對頁面路由進行封裝了,難道就不搞個傳遞數據的自動注入?還用getExtras、putExtras一個一個的賦值?不會吧不會吧?
好,現在我們有了初步的預期,想要一個可以跨模塊的,可以根據字符串或數字對應頁面并實現跳轉的,可以傳遞各種參數、而且可以在目標頁面中自動注入參數的腳手架!
這篇文章,我們就一起來實現一個自己的路由框架。
本文很多地方參考了ARouter的實現,并做了許多簡化,重在說明APT的使用和路由框架的思想。
先來畫幾個圖,清晰一下總流程
如上圖,模塊化的項目中,主要業務功能集中在幾個功能模塊里,能夠引用的邏輯只有自身包含的,和基礎功能集中所包含的。而在APP模塊中,可以拿到所有模塊的引用
所以,我們可以在APP中,在啟動時,將各個模塊中想要暴露的XXX.class收集起來,放到基礎功能中某個地方中存起來,然后在功能模塊里,就可以使用約定好的方式,通過某個數字或字符串標簽,路由到對應頁面中了。
如上圖,這個流程用文字來描述就是:
- 你要有一個路由器框架??????
- 在每個功能模塊中,準備好自己包含的路由信息(有幾個頁面可以被別人調,每個頁面對應什么標簽)。
- app啟動時,收集所有功能模塊中暴露出的路由信息,寫到公用路由器的路由表中。
- 功能中需要跳轉時,調用路由器提供的方法,由路由器查詢路由表,并執行跳轉。
好!現在就能寫一個路由框架啦!
是的,根據上面的分析,現在真的能寫出路由框架了,其實比想象的還要簡單。
只需要定義這樣兩個類,就算是完事了,請看下面的代碼:
- Router 單例路由器,包含路由表,提供跳轉方法。
- MetaProvider 元信息提供者接口,需要功能模塊中提供實現,供app調用收集。
路由器
/** 沒錯這就是路由器本器 */
class Router private constructor() {
companion object {
val instance = Router()
}
// 路由表,存著支持跳轉的class信息
private val metaMap = hashMapOf<String, Class<*>>()
// 使用這個方法注入元信息,可多次使用
fun injectMeta(provider: MetaProvider) {
metaMap.putAll( provider.getMetaHere() )
}
fun jumpTo(context: Context, tag: String) {
if(null == metaMap[tag]) {
// 找不到,就打個日志好了
} else {
context.startActivity(Intent(context, metaMap[tag]))
}
}
}
元信息提供者
interface MetaProvider {
fun getMetaHere(): Map<String, Class<*>>
}
模塊中的具體實現
A 模塊中有這么一個實現類,其他模塊也是類似
class A_MetaProvider : MetaProvider {
// 在模塊中寫這個,當然可以使用自己包含的頁面引用
override fun getMetaHere(): Map<String, Class<*>> {
return mapOf(
"A-Activity111" to Activity111::class.java,
"A-Activity222" to Activity222::class.java,
"A-Activity333" to Activity333::class.java,
)
}
}
就這,已經可以用辣
class SimpleApplication: Application() {
// 分別調用幾個模塊中的提供者,把所有模塊提供的元信息都塞到路由表里面
override fun onCreate() {
super.onCreate()
Router.instance.injectMeta(A_MetaProvider())
Router.instance.injectMeta(B_MetaProvider())
Router.instance.injectMeta(C_MetaProvider())
}
}
具體使用跳轉的時候就像這樣:
----------- 省略一大堆 ------------
btnTest.setOnClickListener {
Router.instance.jumpTo(this@SimpleActivity, "A-Activity222")
}
----------- 省略一大堆 ------------
是的沒錯,雖然很多情況還沒有考慮,但這還真就是我們路由框架的關鍵流程,這個sdk中的全部,就是這少到可憐的一個類,和一個接口。
真的,就這樣吧,一滴都沒有了。
欸?好像哪里不太對,這個文章標題是啥來著???
嘿,皮一下,就很舒服。
??????????????????????????????????????????????????
從上面的代碼中可以看出,有一些代碼是屬于重復性的模板代碼的。
比如功能模塊中的Provider實現類...們,
比如Application中幾乎完全相同的三句注入代碼。
這種規律性的重復代碼,很適合在編譯期生成,可以通過APT的能力,定義一個類注解,掛在Activity類上,注解的值就是tag字符串,這樣開發者就不需要再寫這些類似清單的蛋疼代碼了。
現在開始操刀進行APT改造
現在我們主要的任務是如何自動生成元信息提供者,和自動化的注入邏輯。路由類和提供者接口是不需要動的。
首先在sdk模塊中定義一個注解
- 這個當然是要寫到路由框架模塊中的
@Target(AnnotationTarget.CLASS) // 這個代表此注解可以在類上使用
@Retention(AnnotationRetention.SOURCE) // 這個是作用范圍,只在源碼期有效,編譯完就沒了,反射拿不到
annotation class RouteMeta(val value: String)
掛到Activity上
@RouteMeta("A-Activity222")
class SimpleActivity2: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// do something
}
}
然后跳轉還是上面寫過的onClick中的內容。
因為是跨模塊的開發,我們定義一個變量聲明在模塊的gradle中
--- build.gradle --- 這里我們定一個 "ROUTER_NAME" 變量,值就是模塊項目名,稍后APT中會使用到
android {
......
defaultConfig {
......
javaCompileOptions {
annotationProcessorOptions {
arguments = [ROUTER_NAME : project.getName()]
}
}
}
}
新建一個注解處理器的module,我們就叫它route-apt
這個庫中不會包含安卓資源,所以建個java library就可以了,需要添加google的auto-service庫,我們的全部本事都靠它才能施展。還需要把注解類按原包結構直接復制一份過來。
因為java-library不能依賴android-library,所以其實最好是把注解單獨作為一個模塊,這樣sdk和apt都可以對其依賴。
gradle全文:
apply plugin: 'java-library'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation 'com.google.auto.service:auto-service:1.0.1'
}
最核心的是一個處理器類,繼承AbstractProcessor
因為我抄了很多現有java邏輯,這一部分就不用kotlin了(才不是懶呢)。
/** 這類就是我們所有處理的入口了 */
public class MetaProcessor extends AbstractProcessor {
private static final String KEY_ROUTER_NAME = "ROUTER_NAME";
private Messager messager;
private Elements elementUtils;
private Types types;
// 這里重寫一下初始化,拿一些工具,因為這部分代碼是編譯時過程的一部分,沒有debug的機會,
// 只能在每個關鍵階段多打一些日志來驗證了。
// 當然,processingEnv其實是父類的成員變量,這里不拿也可以在任何地方拿到。
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
types = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
}
// 這個方法重寫一下,返回我們要處理的注解類名,用于過濾
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new HashSet<>();
supportTypes.add(RouteMeta.class.getCanonicalName());
return supportTypes;
}
// 重點就在這里了,這是處理的入口方法,會從這里開始處理源代碼,并根據邏輯生成新代碼。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
process方法邏輯相對復雜,我們拿出來單獨看
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
String routerName = processingEnv.getOptions().get(KEY_ROUTER_NAME);
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: --------- process start --------- " + routerName);
// 搞一個map在循環時找到目標暫存進去
Map<String, TypeElement> elementMap = new HashMap<>();
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(RouteMeta.class);
for (Element item : elements) {
if(isActivity(item.asType())) {
RouteMeta routeMeta = item.getAnnotation(RouteMeta.class);
messager.printMessage(Diagnostic.Kind.NOTE,
"MetaProcessor: found activity: " + item.asType().toString()
+ ", path = " + routeMeta.value());
if(!elementMap.containsKey(routeMeta.value())) {
elementMap.put(routeMeta.value(), (TypeElement) item);
}
}
}
messager.printMessage(Diagnostic.Kind.NOTE,
"MetaProcessor: find activity with annotation end, count is " + elementMap.size());
// 已經找到了當前模塊中所有帶有 RouteMeta 注解的activity,開始生成對應provider類
generateCode(routerName, elementMap);
return true;
}
// 判斷是否是activity類型
private boolean isActivity(TypeMirror typeMirror) {
TypeMirror activityType = elementUtils.getTypeElement("android.app.Activity").asType();
if(!types.isSubtype(typeMirror, activityType)) {
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: unsupported annotation on type: " + typeMirror.toString());
return false;
}
return true;
}
// 生成包含全部帶注解的 activity 的 provider,這部分就是寫代碼了,根據拿到的信息,一句一句拼出來
private void generateCode(String routerName, Map<String, TypeElement> elementMap) {
if(elementMap.isEmpty()) {
return;
}
String packageName = "com.example.router";
String className = routerName + "_$_RouteProvider";
StringBuilder stringBuffer = new StringBuilder("package ").append(packageName).append(";\n\n");
stringBuffer.append("import java.util.Map;\n");
stringBuffer.append("import java.util.HashMap;\n");
stringBuffer.append("import com.example.route_sdk.MetaProvider;\n\n");
stringBuffer.append("public class ").append(className).append(" implements MetaProvider {\n");
stringBuffer.append("public Map<String, Class<?>> getMetaHere() {\n");
stringBuffer.append("HashMap<String, Class<?>> map = new HashMap<>();\n");
elementMap.forEach( (k, v) -> {
stringBuffer.append("map.put(")
.append("\"").append(k).append("\"")
.append(", ")
.append(v.asType().toString())
.append(".class);\n");
});
stringBuffer.append("return map;\n");
stringBuffer.append("}\n");
stringBuffer.append("}\n");
try {
Writer writer = processingEnv.getFiler()
.createSourceFile(className)
.openWriter();
writer.write(stringBuffer.toString());
writer.close();
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: write java file done, class name is: " + className);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: catch IOException when write java file, class name is: " + className);
e.printStackTrace();
}
}
為了讓gradle識別我們的processor,需要添加配置文件。
在src\main下創建文件夾resources,在其中創建 META-INF\services,
創建一個文本文件:javax.annotation.processing.Processor
內容是處理器全類名,如:com.example.route_apt.MetaProcessor
好,代碼寫完,build一下項目,控制臺中可以看到輸出的日志了,代表我們的Processor生效了:
然后,在模塊下的build文件夾中可以分別找到生成的java文件,和臨時編譯的class文件:
build\generated\ap_generated_sources\debug\out 下的 xxx.java
build\intermediates\javac\debug\classes\com\example\person 下的 xxx.class
(我的studio版本是北極狐,gradle版本7.0,不同版本下的臨時文件目錄可能不一致。)
最后再簡化一下初始注入邏輯
APT的工作此時已經完成了,上面生成的類會隨著項目代碼一起打包到最終的apk中,代碼中也是可見的。在初始化時可以通過直接引用來完成路由注入。
但我們在知道其命名規則的情況下,完全可以通過類名反射的方式來使用,避免寫過多重復代碼,這部分的反射調用數量等于模塊的數量,不會太多,性能也不會成為問題。
在Router里加一個注入方法:
public class Router {
..................
............
/**
* 通過模塊名和固定的包名和類后綴,拼接要加載的Provider類名,通過反射進行路由信息收集
* @param projectNames 要加載的模塊工程名
*/
public void injectFromProject(String... projectNames) {
String packageName = "com.example.router.";
String classSuffix = "_$_RouteProvider";
try {
for (String item : projectNames) {
MetaProvider provider = (MetaProvider) Class.forName(packageName + item + classSuffix).newInstance();
injectMeta(provider);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
這個方法需要傳入模塊名,還是會有一些耦合性,阿里的ARouter這部分邏輯是遍歷apk中所有dex,根據包名和類型來找出對應的Provider 的。這部分代碼比較多,這里就不貼了。
最后,在Application的onCreate中,調用此方法完成初始化,就可以啦!
Router.getInstance().injectFromProject("app", "person", "order", "xxxx");
????????????????????????????????????????????????????????????????????
--------------------------- 完結,撒花 ---------------------------
????????????????????????????????????????????????????????????????????