Android高工面試:如果需要實現一個 路由(Router)框架,講講你的思路
本文首發公眾號:徐公碼字
Android 開發中,元件化,模組化是一個老生常談的問題。隨著專案複雜性的增長,模組化是一個必然的趨勢。除非你能忍受改一下程式碼,就需要六七分鐘的漫長時間。
模組化,元件化隨之帶來的另外一個問題是頁面的跳轉問題,由於程式碼的隔離,程式碼之間有時候會無法互相訪問。於是,路由(Router)框架誕生了。
目前用得比較多的有阿里的 ARouter,美團的 WMRouter,ActivityRouter 等。
今天,就讓我們一起來看一下怎樣實現一個路由框架。 實現的功能有。
- 基於編譯時註解,使用方便
- 結果回撥,每次跳轉 Activity 都會回撥跳轉結果
- 除了可以使用註解自定義路由,還支援手動分配路由
- 支援多模組使用,支援元件化使用
使用說明
基本使用
第一步,在要跳轉的 activity 上面註明 path,
@Route(path = "activity/main") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
在要跳轉的地方
Router.getInstance().build("activity/main").navigation(this);
如果想在多 moule 中使用
第一步,使用 @Modules({"app", "sdk"})
註明總共有多少個 moudle,並分別在 moudle 中註明當前 moudle 的 名字,使用 @Module("")
註解。注意 @Modules({"app", "sdk"}) 要與 @Module("") 一一對應。
在主 moudle 中,
@Modules({"app", "moudle1"}) @Module("app") public class RouterApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Router.getInstance().init(); } }
在 moudle1 中,
@Route(path = "my/activity/main")
@Module("moudle1")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_2);
}
}
這樣就可以支援多模組使用了。
自定義注入 router
Router.getInstance().add("activity/three", ThreeActivity.class);
跳轉的時候呼叫
Router.getInstance().build("activity/three").navigation(this);
結果回撥
路由跳轉結果回撥。
Router.getInstance().build("my/activity/main", new RouterCallback() {
@Override
public boolean beforeOpen(Context context, Uri uri) {
// 在開啟路由之前
Log.i(TAG, "beforeOpen: uri=" + uri);
return false;
}
// 在開啟路由之後(即開啟路由成功之後會回撥)
@Override
public void afterOpen(Context context, Uri uri) {
Log.i(TAG, "afterOpen: uri=" + uri);
}
// 沒有找到改 uri
@Override
public void notFind(Context context, Uri uri) {
Log.i(TAG, "notFind: uri=" + uri);
}
// 發生錯誤
@Override
public void error(Context context, Uri uri, Throwable e) {
Log.i(TAG, "error: uri=" + uri + ";e=" + e);
}
}).navigation(this);
startActivityForResult 跳轉結果回撥
Router.getInstance().build("activity/two").navigation(this, new Callback() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);
}
});
原理說明
實現一個 Router 框架,涉及到的主要的知識點如下:
- 註解的處理
- 怎樣解決多個 module 之間的依賴問題,以及如何支援多 module 使用
- router 跳轉及 activty startActivityForResult 的處理
我們帶著這三個問題,一起來探索一下。
總共分為四個部分,router-annotion, router-compiler,router-api,stub
router-annotion 主要是定義註解的,用來存放註解檔案
router-compiler 主要是用來處理註解的,自動幫我們生成程式碼
router-api 是對外的 api,用來處理跳轉的。
stub 這個是存放一些空的 java 檔案,提前佔坑。不會打包進 jar。
router-annotion
主要定義了三個註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path();
}
@Retention(RetentionPolicy.CLASS)
public @interface Modules {
String[] value();
}
@Retention(RetentionPolicy.CLASS)
public @interface Module {
String value();
}
Route 註解主要是用來註明跳轉的 path 的。
Modules 註解,註明總共有多少個 moudle。
Module 註解,註明當前 moudle 的名字。
Modules,Module 註解主要是為了解決支援多 module 使用的。
router-compiler
router-compiler 只有一個類 RouterProcessor,他的原理其實也是比較簡單的,掃描那些類用到註解,並將這些資訊存起來,做相應的處理。這裡是會生成相應的 java 檔案。
主要包括以下兩個步驟
- 根據是否有
@Modules
@Module
註解,然後生成相應的RouterInit
檔案 - 掃描
@Route
註解,並根據moudleName
生成相應的 java 檔案
註解基本介紹
在講解 RouterProcessor 之前,我們先來了解一下註解的基本知識。
如果對於自定義註解還不熟悉的話,可以先看我之前寫的這兩篇文章。Android 自定義編譯時註解1 - 簡單的例子,Android 編譯時註解 —— 語法詳解
public class RouterProcessor extends AbstractProcessor {
private static final boolean DEBUG = true;
private Messager messager;
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
mFiler = processingEnv.getFiler();
UtilManager.getMgr().init(processingEnv);
}
/**
* 定義你的註解處理器註冊到哪些註解上
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Route.class.getCanonicalName());
annotations.add(Module.class.getCanonicalName());
annotations.add(Modules.class.getCanonicalName());
return annotations;
}
/**
* java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
首先我們先來看一下 getSupportedAnnotationTypes
方法,這個方法返回的是我們支援掃描的註解。
註解的處理
接下來我們再一起來看一下 process
方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 註解為 null,直接返回
if (annotations == null || annotations.size() == 0) {
return false;
}
UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process");
boolean hasModule = false;
boolean hasModules = false;
// module
String moduleName = "RouterMapping";
Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
if (moduleList != null && moduleList.size() > 0) {
Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
moduleName = moduleName + "_" + annotation.value();
hasModule = true;
}
// modules
String[] moduleNames = null;
Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
if (modulesList != null && modulesList.size() > 0) {
Element modules = modulesList.iterator().next();
moduleNames = modules.getAnnotation(Modules.class).value();
hasModules = true;
}
debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv);
debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule);
// RouterInit
if (hasModules) { // 有使用 @Modules 註解,生成 RouterInit 檔案,適用於多個 moudle
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) { // 沒有使用 @Modules 註解,並且有使用 @Module,生成相應的 RouterInit 檔案,使用與單個 moudle
debug("generate default RouterInit");
generateDefaultRouterInit();
}
// 掃描 Route 註解
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
List<TargetInfo> targetInfos = new ArrayList<>();
for (Element element : elements) {
System.out.println("elements =" + elements);
// 檢查型別
if (!Utils.checkTypeValid(element)) continue;
TypeElement typeElement = (TypeElement) element;
Route route = typeElement.getAnnotation(Route.class);
targetInfos.add(new TargetInfo(typeElement, route.path()));
}
// 根據 module 名字生成相應的 java 檔案
if (!targetInfos.isEmpty()) {
generateCode(targetInfos, moduleName);
}
return false;
}
,首先判斷是否有註解需要處理,沒有的話直接返回 annotations == null || annotations.size() == 0
。
接著我們會判斷是否有 @Modules
註解(這種情況是多個 moudle 使用),有的話會呼叫 generateModulesRouterInit(String[] moduleNames)
方法生成 RouterInit java 檔案,當沒有 @Modules
註解,並且沒有 @Module
(這種情況是單個 moudle 使用),會生成預設的 RouterInit 檔案。
private void generateModulesRouterInit(String[] moduleNames) {
MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
for (String module : moduleNames) {
initMethod.addStatement("RouterMapping_" + module + ".map()");
}
TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(initMethod.build())
.build();
try {
JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit)
.build()
.writeTo(mFiler);
} catch (Exception e) {
e.printStackTrace();
}
}
假設說我們有"app","moudle1" 兩個 moudle,那麼我們最終生成的程式碼是這樣的。
public final class RouterInit {
public static final void init() {
RouterMapping_app.map();
RouterMapping_moudle1.map();
}
}
如果我們都沒有使用 @Moudles 和 @Module 註解,那麼生成的 RouterInit 檔案大概是這樣的。
public final class RouterInit {
public static final void init() {
RouterMapping.map();
}
}
這也就是為什麼有 stub module 的原因。因為預設情況下,我們需要藉助 RouterInit 去初始化 map。如果沒有這兩個檔案,ide 編輯器 在 compile 的時候就會報錯。
compileOnly project(path: ':stub')
我們引入的方式是使用 compileOnly,這樣的話再生成 jar 的時候,不會包括這兩個檔案,但是可以在 ide 編輯器中執行。這也是一個小技巧。
Route 註解的處理
我們回過來看 process 方法連對 Route 註解的處理。
// 掃描 Route 自己註解
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
List<TargetInfo> targetInfos = new ArrayList<>();
for (Element element : elements) {
System.out.println("elements =" + elements);
// 檢查型別
if (!Utils.checkTypeValid(element)) continue;
TypeElement typeElement = (TypeElement) element;
Route route = typeElement.getAnnotation(Route.class);
targetInfos.add(new TargetInfo(typeElement, route.path()));
}
// 根據 module 名字生成相應的 java 檔案
if (!targetInfos.isEmpty()) {
generateCode(targetInfos, moduleName);
}
首先會掃描所有的 Route 註解,並新增到 targetInfos list 當中,接著呼叫 generateCode
方法生成相應的檔案。
private void generateCode(List<TargetInfo> targetInfos, String moduleName) {
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("map")
// .addAnnotation(Override.class)
.addModifiers(Modifier.STATIC)
.addModifiers(Modifier.PUBLIC);
// .addParameter(parameterSpec);
for (TargetInfo info : targetInfos) {
methodSpecBuilder.addStatement("com.xj.router.api.Router.getInstance().add($S, $T.class)", info.getRoute(), info.getTypeElement());
}
TypeSpec typeSpec = TypeSpec.classBuilder(moduleName)
// .addSuperinterface(ClassName.get(interfaceType))
.addModifiers(Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.addJavadoc("Generated by Router. Do not edit it!\n")
.build();
try {
JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, typeSpec)
.build()
.writeTo(UtilManager.getMgr().getFiler());
System.out.println("generateCode: =" + Constants.ROUTE_CLASS_PACKAGE + "." + Constants.ROUTE_CLASS_NAME);
} catch (Exception e) {
e.printStackTrace();
System.out.println("generateCode:e =" + e);
}
}
這個方法主要是使用 javapoet 生成 java 檔案,關於 javaposet 的使用可以見官網文件,生成的 java 檔案是這樣的。
package com.xj.router.impl;
import com.xj.arounterdemo.MainActivity;
import com.xj.arounterdemo.OneActivity;
import com.xj.arounterdemo.TwoActivity;
/**
* Generated by Router. Do not edit it!
*/
public class RouterMapping_app {
public static void map() {
com.xj.router.api.Router.getInstance().add("activity/main", MainActivity.class);
com.xj.router.api.Router.getInstance().add("activity/one", OneActivity.class);
com.xj.router.api.Router.getInstance().add("activity/two", TwoActivity.class);
}
}
可以看到我們定義的註解資訊,最終都會呼叫 Router.getInstance().add()
方法存放起來。
router-api
這個 module 主要是多外暴露的 api,最主要的一個檔案是 Router。
public class Router {
private static final String TAG = "ARouter";
private static final Router instance = new Router();
private Map<String, Class<? extends Activity>> routeMap = new HashMap<>();
private boolean loaded;
private Router() {
}
public static Router getInstance() {
return instance;
}
public void init() {
if (loaded) {
return;
}
RouterInit.init();
loaded = true;
}
}
當我們想要初始化 Router 的時候,代用 init 方法即可。 init 方法會先判斷是否初始化過,沒有初始化過,會呼叫 RouterInit#init 方法區初始化。
而在 RouterInit#init 中,會呼叫 RouterMap_{@moduleName}#map 方法初始化,改方法又呼叫 Router.getInstance().add()
方法,從而完成初始化
router 跳轉回調
public interface RouterCallback {
/**
* 在跳轉 router 之前
* @param context
* @param uri
* @return
*/
boolean beforeOpen(Context context, Uri uri);
/**
* 在跳轉 router 之後
* @param context
* @param uri
*/
void afterOpen(Context context, Uri uri);
/**
* 沒有找到改 router
* @param context
* @param uri
*/
void notFind(Context context, Uri uri);
/**
* 跳轉 router 錯誤
* @param context
* @param uri
* @param e
*/
void error(Context context, Uri uri, Throwable e);
}
public void navigation(Activity context, int requestCode, Callback callback) {
beforeOpen(context);
boolean isFind = false;
try {
Activity activity = (Activity) context;
Intent intent = new Intent();
intent.setComponent(new ComponentName(context.getPackageName(), mActivityName));
intent.putExtras(mBundle);
getFragment(activity)
.setCallback(callback)
.startActivityForResult(intent, requestCode);
isFind = true;
} catch (Exception e) {
errorOpen(context, e);
tryToCallNotFind(e, context);
}
if (isFind) {
afterOpen(context);
}
}
private void tryToCallNotFind(Exception e, Context context) {
if (e instanceof ClassNotFoundException && mRouterCallback != null) {
mRouterCallback.notFind(context, mUri);
}
}
主要看 navigation 方法,在跳轉 activity 的時候,首先會會呼叫 beforeOpen 方法回撥 RouterCallback#beforeOpen。接著 catch exception 的時候,如果發生錯誤,會呼叫 errorOpen 方法回撥 RouterCallback#errorOpen 方法。同時呼叫 tryToCallNotFind 方法判斷是否是 ClassNotFoundException,是的話回撥 RouterCallback#notFind。
如果沒有發生 eception,會回撥 RouterCallback#afterOpen。
Activity 的 startActivityForResult 回撥
可以看到我們的 Router 也是支援 startActivityForResult 的
Router.getInstance().build("activity/two").navigation(this, new Callback() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);
}
});
它的實現原理其實很簡單,是藉助一個空白 fragment 實現的,原理的可以看我之前的這一篇文章。
Android Fragment 的妙用 - 優雅地申請許可權和處理 onActivityResult
小結
如果覺得效果不錯的話,請到 github 上面 star, 謝謝。 Router
我們的 Router 框架,流程大概是這樣的。
題外話
看了上面的文章,文章一開頭提到的三個問題,你懂了嗎,歡迎在評論區留言評論。
- 註解的處理
- 怎樣解決多個 module 之間的依賴問題,以及如何支援多 module 使用
- router 跳轉及 activty startActivityForResult 的處理
其實,現在很多 router 框架都藉助 gradle 外掛來實現。這樣有一個好處,就是在多 moudle 使用的時候,我們只需要 apply plugin
就 ok,對外遮蔽了一些細節。但其實,他的原理跟我們上面的原理都是差不多的。
文末
紙上說來終覺淺,時間比較充裕的小夥伴建議到我的B站觀看視訊講解:APT技術實現元件路由框架
歡迎關注我的簡書,分享Android乾貨,交流Android技術。
對文章有何見解,或者有何技術問題,都可以在評論區一起留言討論,我會虔誠為你解答。
也歡迎大家來我的B站找我玩,有各類Android架構師進階技術難點的視訊講解
B站直通車:https://space.bilibili.com/544650554