網易考拉Android客戶端路由匯流排設計
1.前言
當前,Android路由框架已經有很多了,如雨後春筍般出現,大概是因為去年提出了Android元件化的概念。當一個產品的業務規模上升到一定程度,或者是跨團隊開發時,團隊/模組間的合作問題就會暴露出來。如何保持團隊間業務的往來?如何互不影響或干涉對方的開發進度?如何呼叫業務方的功能?元件化給上述問題提供了一個答案。元件化所要解決的核心問題是解耦,路由正是為了解決模組間的解耦而出現的。本文闡述了考拉Android端的路由設計方案,儘管與市面上的方案大同小異,但更多的傾向於與考拉業務進行一定程度的結合。
1.1 傳統的頁面跳轉
頁面跳轉主要分為三種,App頁面間跳轉、H5跳轉回App頁面以及App跳轉至H5。
App頁面間跳轉
App頁面間的跳轉,對於新手來說一般會在跳轉的頁面使用如下程式碼:
Intent intent = new Intent(this, MainActivity.class);intent.putExtra("dataKey", "dataValue");startActivity(intent);
對於有一定經驗的程式員,會在跳轉的類生成自己的跳轉方法:
public class OrderManagerActivity extends BaseActivity {public static void launch(Context context, int startTab) {Intent i = new Intent(context, OrderManagerActivity.class);i.putExtra(INTENT_IN_INT_START_TAB, startTab);context.startActivity(i);}}
無論使用哪種方式,本質都是生成一個Intent,然後再通過Context.startActivity(Intent)/Activity.startActivityForResult(Intent, int)實現頁面跳轉。這種方式的不足之處是當包含多個模組,但模組間沒有相互依賴時,這時候的跳轉會變得相當困難。如果已知其他模組的類名以及對應的路徑,可以通過Intent.setComponent(Component)方法啟動其他模組的頁面,但往往模組的類名是有可能變化的,一旦業務方把模組換個名字,這種隱藏的Bug對於開發的內心來說是崩潰的。另一方面,這種重複的模板程式碼,每次至少寫兩行才能實現頁面跳轉,程式碼存在冗餘。
H5-App頁面跳轉
對於考拉這種電商應用,活動頁面具有時效性和即時性,這兩種特性在任何時候都需要得到保障。運營隨時有可能更改活動頁面,也有可能要求點選某個連結就能跳轉到一個App頁面。傳統的做法是對WebViewClient.shouldOverrideUrlLoading(WebView, String)進行攔截,判斷url是否有對應的App頁面可以跳轉,然後取出url中的params封裝成一個Intent傳遞並啟動App頁面。
感受一下在考拉App工程中曾經出現過的下面這段程式碼:
public static Intent startActivityByUrl(Context context, String url, boolean fromWeb, boolean outer) {if (StringUtils.isNotBlank(url) && url.startsWith(StringConstants.REDIRECT_URL)) {try {String realUrl = Uri.parse(url).getQueryParameter("target");if (StringUtils.isNotBlank(realUrl)) {url = URLDecoder.decode(realUrl, "UTF-8");}} catch (Exception e) {e.printStackTrace();}}Intent intent = null;try {Uri uri = Uri.parse(url);String host = uri.getHost();List<String> pathSegments = uri.getPathSegments();String path = uri.getPath();int segmentsLength = (pathSegments == null ? 0 : pathSegments.size());if (!host.contains(StringConstants.KAO_LA)) {return null;}if((StringUtils.isBlank(path))){do something...return intent;}if (segmentsLength == 2 && path.startsWith(StringConstants.JUMP_TO_GOODS_DETAIL)) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_SPRING_ACTIVITY_TAB)) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_SPRING_ACTIVITY_DETAIL) && segmentsLength == 3) {do something...} else if (path.startsWith(StringConstants.START_CART) && segmentsLength == 1) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_COUPON_DETAIL)|| (path.startsWith(StringConstants.JUMP_TO_COUPON) && segmentsLength == 2)) {do something...} else if (canOpenMainPage(host, uri.getPath())) {do something...} else if (path.startsWith(StringConstants.START_ORDER)) {if (!UserInfo.isLogin(context)) {do something...} else {do something...}} else if (path.startsWith(StringConstants.START_SAVE)) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_NEW_DISCOVERY)) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_NEW_DISCOVERY_2) && segmentsLength == 3) {do something...} else if (path.startsWith(StringConstants.START_BRAND_INTRODUCE)|| path.startsWith(StringConstants.START_BRAND_INTRODUCE2)) {do something...} else if (path.startsWith(StringConstants.START_BRAND_DETAIL) && segmentsLength == 2) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_ORDER_DETAIL)) {if (!UserInfo.isLogin(context) && outer) {do something...} else {do something...}} else if (path.startsWith("/cps/user/certify.html")) {do something...} else if (path.startsWith(StringConstants.IDENTIFY)) {do something...} else if (path.startsWith("/album/share.html")) {do something...} else if (path.startsWith("/album/tag/share.html")) {do something...} else if (path.startsWith("/live/roomDetail.html")) {do something...} else if (path.startsWith(StringConstants.JUMP_TO_ORDER_COMMENT)) {if (!UserInfo.isLogin(context) && outer) {do something...} else {do something...}} else if (openOrderDetail(url, path)) {if (!UserInfo.isLogin(context) && outer) {do something...} else {do something...}} else if (path.startsWith(StringConstants.JUMP_TO_SINGLE_COMMENT)) {do something...} else if (path.startsWith("/member/activity/vip_help.html")) {do something...} else if (path.startsWith("/goods/search.html")) {do something...} else if(path.startsWith("/afterSale/progress.html")){do something...} else if(path.startsWith("/afterSale/apply.html")){do something...} else if(path.startsWith("/order/track.html")) {do something...}} catch (Exception e) {e.printStackTrace();}if (intent != null && !(context instanceof Activity)) {intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}return intent;}
這段程式碼整整260行,看到程式碼時我的內心是崩潰的。這種做法的弊端在於:
-
判斷不合理。上述程式碼僅判斷了HOST是否包含StringConstants.KAO_LA,然後根據PATH區分跳轉到哪個頁面,PATH也只判斷了起始部分,當URL越來越多的時候很有可能造成誤判。
-
耦合性太強。已知攔截的所有頁面的引用都必須能夠拿到,否則無法跳轉;
-
程式碼混亂。PATH非常多,從眾多的PATH中匹配多個已知的App頁面,想必要判斷匹配規則就要寫很多函式解決;
-
攔截過程不透明。開發者很難在URL攔截的過程中加入自己的業務邏輯,如打點、啟動Activity前新增特定的Flag等;
-
沒有優先順序概念,也無法降級處理。同一個URL,只要第一個匹配到App頁面,就只能開啟這個頁面,無法通過調整優先順序跳轉到別的頁面或者使用H5開啟。
App頁面-H5跳轉
這種情況不必多說,啟動一個WebViewActivity即可。
1.2 頁面路由的意義
路由最先被應用於網路中,路由的定義是通過互聯的網路把資訊從源地址傳輸到目的地址的活動。頁面跳轉也是相當於從源頁面跳轉到目標頁面的過程,每個頁面可以定義為一個統一資源識別符號(URI),在網路當中能夠被別人訪問,也可以訪問已經被定義了的頁面。路由常見的使用場景有以下幾種:
-
App接收到一個通知,點選通知開啟App的某個頁面(OuterStartActivity)
-
瀏覽器App中點選某個連結開啟App的某個頁面(OuterStartActivity)
-
App的H5活動頁面開啟一個連結,可能是H5跳轉,也可能是跳轉到某一個native頁面(WebViewActivity)
-
開啟頁面需要某些條件,先驗證完條件,再去開啟那個頁面(需要登入)
-
App內的跳轉,可以減少手動構建Intent的成本,同時可以統一攜帶部分引數到下一個頁面(打點)
除此之外,使用路由可以避免上述弊端,能夠降低開發者頁面跳轉的成本。
2.考拉路由匯流排
2.1 路由框架

image
考拉路由框架主要分為三個模組:路由收集、路由初始化以及頁面路由。路由收集階段,定義了基於Activity類的註解,通過Android Processing Tool(以下簡稱“APT”)收集路由資訊並生成路由表類;路由初始化階段,根據生成的路由表資訊注入路由字典;頁面路由階段,則通過路由字典查詢路由資訊,並根據查詢結果定製不同的路由策略略。
2.2 路由設計思路
總的來說,考拉路由設計追求的是功能模組的解耦,能夠實現基本路由功能,以及開發者使用上足夠簡單。考拉路由的前兩個階段對於路由使用者幾乎是無成本的,只需要在使用路由的頁面定義一個類註解@Router即可,頁面路由的使用也相當簡單,後面會詳細介紹。
功能設計
路由在一定程度上和網路請求是類似的,可以分為請求、處理以及響應三個階段。這三個階段對使用者來說既可以是透明的,也可以在路由過程中進行攔截處理。考拉路由框架目前支援的功能有:
1.支援基本Activity的啟動,以及startActivityForResult回撥;:white_check_mark:
2.支援不同協議執行不同跳轉;(kaola:// 、http(s):// 、native:// 等):white_check_mark: 3.支援多個SCHEME/HOST/PATH跳轉至同一個頁面;((pre.) .kaola.com(.hk)):white_check_mark:
4.支援路由的正則匹配;:white_check_mark:
5.支援Activity啟動使用不同的Flag;:white_check_mark:
6.支援路由的優先順序配置;:white_check_mark:
7.支援對路由的動態攔截、監聽以及降級;:white_check_mark:
以上功能保證了考拉業務模組間的解耦,也能夠滿足目前產品和運營的需求。
介面設計

image
一個好的模組或框架,需要事先設計好介面,預留足夠的許可權供呼叫者支配,才能滿足各種各樣的需求。考拉路由框架在設計過程中使用了常見的設計模式,如Builder模式、Factory模式、Wrapper模式等,並遵循了一些設計原則。(最近在看第二遍Effective Java,對以下原則深有體會,推薦看一下)
針對介面程式設計,而不是針對實現程式設計
這條規則排在最前面的原因是,針對介面程式設計,不管是對開發者還是對使用者,真的是百利而無一害。在路由版本迭代的過程中,底層對介面無論實現怎樣的修改,也不會影響到上層呼叫。對於業務來說,路由的使用是無感知的。
考拉路由框架在設計過程中並未完全遵循這條原則,下一個版本的迭代會盡量按照這條原則來實現。但在路由過程中的關鍵步驟都預留了介面,具體有:
RouterRequestCallback
public interface RouterRequestCallback {void onFound(RouterRequest request, RouterResponse response);boolean onLost(RouterRequest request);}
路由表中是否能夠匹配到路由資訊的回撥,如果能夠匹配,則回撥onFound(),如果不能夠匹配,則返回onLost()。onLost()的結果由開發來定義,如果返回的結果是true,則認為開發者處理了這次路由不匹配的結果,最終返回RouterResult的結果是成功路由。
RouterHandler
public interface RouterHandler extends RouterStarter {RouterResponse findResponse(RouterRequest request); }
路由處理與啟動介面,根據給定的路由請求,查詢路由資訊,根據路由響應結果,分發給相應的啟動器執行後續頁面跳轉。這個介面的設計不太合理,功能上不完善,後續會重新設計這個介面,讓呼叫方有許可權干預查詢路由的過程。
RouterResultCallback
public interface RouterResultCallback {boolean beforeRoute(Context context, Intent intent);void doRoute(Context context, Intent intent, Object extra);void errorRoute(Context context, Intent intent, String errorCode, Object extra);}
匹配到路由資訊後,真正執行路由過程的回撥。beforeRoute()這個方法是在真正路由之前的回撥,如果開發者返回true,則認為這條路由資訊已被呼叫者攔截,不會再回調後面的doRoute()以及執行路由。在路由過程中發生的任何異常,都會回撥errorRoute()方法,這時候路由中斷。
ResponseInvoker
public interface ResponseInvoker {void invoke(Context context, Intent intent, Object... args);}
路由執行者。如果開發需要執行路由前進行一些全域性操作,例如新增額外的資訊傳入到下一個Activity,則可以自己實現這個介面。路由框架提供預設的實現:ActivityInvoker。開發也可以繼承ActivityInvoker,重寫invoke()方法,先實現自己的業務邏輯,再呼叫super.invoke()方法。
OnActivityResultListener
public interface OnActivityResultListener {void onActivityResult(int requestCode, int resultCode, Intent data);}
特別強調一下這個Listener。本來這個回撥的作用是方便呼叫者在執行startActivityForResult的時候可以通過回撥來告知結果,但由於不保留活動的限制,離開頁面以後這個監聽器是無法被系統儲存(saveInstanceState)的,因此不推薦在Activity/Fragment中使用回撥,而是在非Activity元件/模組裡使用,如View/Window/Dialog。這個過程已經由core包裡的CoreBaseActivity實現,開發使用的時候,可以直接呼叫CoreBaseActivity.startActivityForResult(intent, requestCode, onActivityResultListener),也可以通過KaolaRouter.with(context).url(url).startForResult(requestCode, onActivityResultListener)呼叫。例如,要啟動訂單管理頁並回調:
KaolaRouter.with(context).url(url).data("orderId", "replace url param key.").startForResult(1, new OnActivityResultListener() {@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {DebugLog.e(requestCode + " " + resultCode + " " + data.toString());}});
RouterResult
public interface RouterResult {boolean isSuccess();RouterRequest getRouterRequest();RouterResponse getRouterResponse();}
告知路由的結果,路由結果可以被幹預,例如RouterRequestCallback.onLost(),返回true的時候,路由也是成功的。這個介面不管路由的成功或失敗都會返回。
不隨意暴露不必要的API
“要區別設計良好的模組與設計不好的模組,最重要的因素在於,這個模組對於外部的其他模組而⾔言,是否隱藏其內部資料和其他實現細節。設計良好的模組會隱藏所有的實現細節,把它的API與它的實現清晰地隔離開來。然後,模組之間只通過它們的API進行通訊,一個模組不需要知道其他模組的內部工作情況。這被稱為封裝(encapsulation)。”(摘自Effective Java, P58)
舉個例子,考拉路由框架對路由呼叫的入參做了限制,一旦入參,則不能再做修改,呼叫者無需知道路由框架對使用這些引數怎麼實現呼叫者想要的功能。實現上,由RouterRequestWrapper繼承自RouterRequestBuilder,後者通過Builder模式給使用者構造相關的引數,前者通過Wrapper模式裝飾RouterRequestBuilder中的所有變數,並在RouterRequestWrapper類中提供所有引數的get函式,供路由框架使用。
單一職責
無論是類還是方法,均需要遵循單一職責原則。一個類實現一個功能,一個方法做一件事。例如,KaolaRouterHandler是考拉路由的處理器,實現了RouterHandler介面,實現路由的查詢與轉發;RouterRequestBuilder用於收集路由請求所需引數;RouterResponseFactory用於生成路由響應的結果。
提供預設實現
針對介面程式設計的好處是隨時可以替換實現,考拉路由框架在路由過程中的所有監聽、攔截以及路由過程都提供了預設的實現。使用者即可以不關心底層的實現邏輯,也可以根據需要替換相關的實現。
2.3 考拉路由實現原理
考拉路由框架基於註解收集路由資訊,通過APT實現路由表的動態生成,類似於ButterKnife的做法,在執行時匯入路由表資訊,並通過正則表示式查詢路由,根據路由結果實現最終的頁面跳轉。
收集路由資訊
首先定義一個註解@Router,註解裡包含了路由協議、路由主機、路由路徑以及路由優先順序。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Router {/*** URI協議,已經提供預設值,預設實現了四種協議:https、http、kaola、native*/String scheme() default "(https|http|kaola|native)://";/*** URI主機,已經提供預設值*/String host() default "(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?";/*** URI路徑,選填,如果使用預設值,則只支援本地路由,不支援url攔截*/String value() default "";/*** 路由優先順序,預設為0。*/int priority() default 0;}
對於需要使用路由的頁面,只需要在類的宣告處加上這個註解,標明這個頁面對應的路由路徑即可。例如:
@Router("/app/myQuestion.html") public class MyQuestionAndAnswerActivity extends BaseActivity {……}
那麼通過APT生成的標記這個頁面的url則是一個正則表示式:
(https|http|kaola|native)://(pre\.)?(\w+\.)?kaola\.com(\.hk)?/app/myQuestion\.html
路由表則是由多條這樣的正則表示式構成。
生成路由表
路由表的生成需要使用APT工具以及Square公司開源的javapoet類庫,目的是根據我們定義的Router註解讓機器幫我們“寫程式碼”,生成一個Map型別的路由表,其中key根據Router註解的資訊生成對應的正則表示式,value是這個註解對應的類的資訊集合。首先定義一個RouterProcessor,繼承自AbstractProcessor,
public class RouterProcessor extends AbstractProcessor {@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);// 初始化相關環境資訊mFiler = processingEnv.getFiler();elementUtil = processingEnv.getElementUtils();typeUtil = processingEnv.getTypeUtils();Log.setLogger(processingEnv.getMessager());}@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> supportAnnotationTypes = new HashSet<>();// 獲取需要處理的註解型別,目前只處理Router註解supportAnnotationTypes.add(Router.class.getCanonicalName());return supportAnnotationTypes;}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {// 收集與Router相關的所有類資訊,解析並生成路由表Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Router.class);try {return parseRoutes(routeElements);} catch (Exception e) {Log.e(e.getMessage(), e);return false;}}}
上述的三個方法屬於AbstractProcessor的方法,public abstract boolean process(Set annotations, RoundEnvironment roundEnv)是抽象方法,需要子類實現。
private boolean parseRoutes(Set<? extends Element> routeElements) throws IOException {if (null == routeElements || routeElements.size() == 0) {return false;}// 獲取Activity類的型別,後面用於判斷是否是其子類TypeElement typeActivity = elementUtil.getTypeElement(ACTIVITY);// 獲取路由Builder類的標準類名ClassName routeBuilderCn = ClassName.get(RouteBuilder.class);// 構建Map<String, Route>集合String routerConstClassName = RouterProvider.ROUTER_CONST_NAME;TypeSpec.Builder typeSpec = TypeSpec.classBuilder(routerConstClassName).addJavadoc(WARNING_TIPS).addModifiers(PUBLIC);/*** Map<String, Route>*/ParameterizedTypeName inputMapTypeName =ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class),ClassName.get(Route.class));ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeName, ROUTER_MAP_NAME).build();MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO).addAnnotation(Override.class).addModifiers(PUBLIC).addParameter(groupParamSpec);// 將路由資訊放入Map<String, Route>集合中for (Element element : routeElements) {TypeMirror tm = element.asType();Router route = element.getAnnotation(Router.class);// 獲取當前Activity的標準類名if (typeUtil.isSubtype(tm, typeActivity.asType())) {ClassName activityCn = ClassName.get((TypeElement) element);String key = "key" + element.getSimpleName().toString();String routeString = RouteBuilder.assembleRouteUri(route.scheme(), route.host(), route.value());if (null == routeString) {//String keyValue = RouteBuilder.generateUriFromClazz(Activity.class);loadIntoMethodOfGroupBuilder.addStatement("String $N= $T.generateUriFromClazz($T.class)", key,routeBuilderCn, activityCn);} else {//String keyValue = "(" + route.value() + ")|(" + RouteBuilder.generateUriFromClazz(Activity.class) + ")";loadIntoMethodOfGroupBuilder.addStatement("String $N=$S + $S + $S+$T.generateUriFromClazz($T.class)+$S", key, "(", routeString, ")|(",routeBuilderCn, activityCn, ")");}/*** routerMap.put(url, RouteBuilder.build(String url, int priority, Class<?> destination));*/loadIntoMethodOfGroupBuilder.addStatement("$N.put($N, $T.build($N, $N, $T.class))", ROUTER_MAP_NAME,key, routeBuilderCn, key, String.valueOf(route.priority()), activityCn);typeSpec.addField(generateRouteConsts(element));}}// Generate RouterConst.javaJavaFile.builder(RouterProvider.OUTPUT_DIRECTORY, typeSpec.build()).build().writeTo(mFiler);// Generate RouterGeneratorJavaFile.builder(RouterProvider.OUTPUT_DIRECTORY, TypeSpec.classBuilder(RouterProvider.ROUTER_GENERATOR_NAME).addJavadoc(WARNING_TIPS).addSuperinterface(ClassName.get(RouterProvider.class)).addModifiers(PUBLIC).addMethod(loadIntoMethodOfGroupBuilder.build()).build()).build().writeTo(mFiler);return true;}
最終生成的路由表如下:
/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY KAOLA PROCESSOR. */public class RouterGenerator implements RouterProvider {@Overridepublic void loadRouter(Map<String, Route> routerMap) {String keyActivityDetailActivity="(" + "(https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/activity/spring/\\w+" + ")|("+RouteBuilder.generateUriFromClazz(ActivityDetailActivity.class)+")";routerMap.put(keyActivityDetailActivity, RouteBuilder.build(keyActivityDetailActivity, 0, ActivityDetailActivity.class));String keyLabelDetailActivity="(" + "(https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/album/tag/share\\.html" + ")|("+RouteBuilder.generateUriFromClazz(LabelDetailActivity.class)+")";routerMap.put(keyLabelDetailActivity, RouteBuilder.build(keyLabelDetailActivity, 0, LabelDetailActivity.class));String keyMyQuestionAndAnswerActivity="(" + "(https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/app/myQuestion.html" + ")|("+RouteBuilder.generateUriFromClazz(MyQuestionAndAnswerActivity.class)+")";routerMap.put(keyMyQuestionAndAnswerActivity, RouteBuilder.build(keyMyQuestionAndAnswerActivity, 0, MyQuestionAndAnswerActivity.class));……}
其中,RouteBuilder.generateUriFromClazz(Class)的實現如下,目的是生成一條預設的與標準類名相關的native跳轉規則。
public static final String SCHEME_NATIVE = "native://";public static String generateUriFromClazz(Class<?> destination) {String rawUri = SCHEME_NATIVE + destination.getCanonicalName();return rawUri.replaceAll("\\.", "\\\\.");}
可以看到,路由集合的key是一條正則表示式,包括了url攔截規則以及自定義的包含標準類名的native跳轉規則。例如,keyMyQuestionAndAnswerActivity最終生成的key是
((https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/app/myQuestion.html)|(native://com.kaola.modules.answer.myAnswer.MyQuestionAndAnswerActivity)
這樣,呼叫者不僅可以通過預設的攔截規則
(https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/app/myQuestion.html)
跳轉到對應的頁面,也可以通過
(native://com.kaola.modules.answer.myAnswer.MyQuestionAndAnswerActivity)。
這樣的好處是模組間的跳轉也可以使用,不需要依賴引用類。而native跳轉會專門生成一個類RouterConst來記錄,如下:
/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY KAOLA PROCESSOR. */public class RouterConst {public static final String ROUTE_TO_ActivityDetailActivity = "native://com.kaola.modules.activity.ActivityDetailActivity";public static final String ROUTE_TO_LabelDetailActivity = "native://com.kaola.modules.albums.label.LabelDetailActivity";public static final String ROUTE_TO_MyQuestionAndAnswerActivity = "native://com.kaola.modules.answer.myAnswer.MyQuestionAndAnswerActivity";public static final String ROUTE_TO_CertificatedNameActivity = "native://com.kaola.modules.auth.activity.CertificatedNameActivity";public static final String ROUTE_TO_CPSCertificationActivity = "native://com.kaola.modules.auth.activity.CPSCertificationActivity";public static final String ROUTE_TO_BrandDetailActivity = "native://com.kaola.modules.brands.branddetail.ui.BrandDetailActivity";public static final String ROUTE_TO_CartContainerActivity = "native://com.kaola.modules.cart.CartContainerActivity";public static final String ROUTE_TO_SingleCommentShowActivity = "native://com.kaola.modules.comment.detail.SingleCommentShowActivity";public static final String ROUTE_TO_CouponGoodsActivity = "native://com.kaola.modules.coupon.activity.CouponGoodsActivity";public static final String ROUTE_TO_CustomerAssistantActivity = "native://com.kaola.modules.customer.CustomerAssistantActivity";……}
初始化路由
路由初始化在Application的過程中以同步的方式進行。通過獲取RouterGenerator的類直接生成例項,並將路由資訊儲存在sRouterMap變數中。
public static void init() {try {sRouterMap = new HashMap<>();((RouterProvider) (Class.forName(ROUTER_CLASS_NAME).getConstructor().newInstance())).loadRouter(sRouterMap);} catch (Exception e) {e.printStackTrace();}}
頁面路由
給定一個url以及上下文環境,即可使用路由。呼叫方式如下:
KaolaRouter.with(context).url(url).start();
頁面路由分為路由請求生成,路由查詢以及路由結果執行這幾個步驟。路由請求目前較為簡單,僅是封裝了一個RouterRequest介面
public interface RouterRequest {Uri getUriRequest(); }
路由的查詢過程相對複雜,除了遍歷路由初始化以後匯入記憶體的路由表,還需要判斷各種各樣的前置條件。具體的條件判斷程式碼中有相關注釋。
@Overridepublic RouterResponse findResponse(RouterRequest request) {if (null == sRouterMap) {return null;//throw new IllegalStateException(//String.format("Router has not been initialized, please call %s.init() first.",//KaolaRouter.class.getSimpleName()));}if (mRouterRequestWrapper.getDestinationClass() != null) {RouterResponse response = RouterResponseFactory.buildRouterResponse(null, mRouterRequestWrapper);reportFoundRequestCallback(request, response);return response;}Uri uri = request.getUriRequest();String requestUrl = uri.toString();if (!TextUtils.isEmpty(requestUrl)) {for (Map.Entry<String, Route> entry : sRouterMap.entrySet()) {if (RouterUtils.matchUrl(requestUrl, entry.getKey())) {Route routerModel = entry.getValue();if (null != routerModel) {RouterResponse response =RouterResponseFactory.buildRouterResponse(routerModel, mRouterRequestWrapper);reportFoundRequestCallback(request, response);return response;}}}}return null;}@Overridepublic RouterResult start() {// 判斷Context引用是否還存在WeakReference<Context> objectWeakReference = mContextWeakReference;if (null == objectWeakReference) {reportRouterResultError(null, null, RouterError.ROUTER_CONTEXT_REFERENCE_NULL, null);return getRouterResult(false, mRouterRequestWrapper, null);}Context context = objectWeakReference.get();if (context == null) {reportRouterResultError(null, null, RouterError.ROUTER_CONTEXT_NULL, null);return getRouterResult(false, mRouterRequestWrapper, null);}// 判斷路由請求是否有效if (!checkRequest(context)) {return getRouterResult(false, mRouterRequestWrapper, null);}// 遍歷查詢路路由結果RouterResponse response = findResponse(mRouterRequestWrapper);// 判斷路由結果,執行路由結果為空時的攔截if (null == response) {boolean handledByCallback = reportLostRequestCallback(mRouterRequestWrapper);if (!handledByCallback) {reportRouterResultError(context, null, RouterError.ROUTER_RESPONSE_NULL,mRouterRequestWrapper.getRouterRequest());}return getRouterResult(handledByCallback, mRouterRequestWrapper, null);}// 獲取路由結果執行的介面ResponseInvoker responseInvoker = getResponseInvoker(context, response);if (responseInvoker == null) {return getRouterResult(false, mRouterRequestWrapper, response);}Intent intent;try {intent = RouterUtils.generateResponseIntent(context, response, mRouterRequestWrapper);} catch (Exception e) {reportRouterResultError(context, null, RouterError.ROUTER_GENERATE_INTENT_ERROR, e);return getRouterResult(false, mRouterRequestWrapper, response);}// 生成相應的Intentif (null == intent) {reportRouterResultError(context, null, RouterError.ROUTER_GENERATE_INTENT_NULL, response);return getRouterResult(false, mRouterRequestWrapper, response);}// 獲取路由結果回撥介面,如果為空,則使用預設提供的實現RouterResultCallback routerResultCallback = getRouterResultCallback();// 由使用者處理if (routerResultCallback.beforeRoute(context, intent)) {return getRouterResult(true, mRouterRequestWrapper, response);}try {responseInvoker.invoke(context, intent, mRouterRequestWrapper.getRequestCode(),mRouterRequestWrapper.getOnActivityResultListener());routerResultCallback.doRoute(context, intent, null);return getRouterResult(true, mRouterRequestWrapper, response);} catch (Exception e) {reportRouterResultError(context, intent, RouterError.ROUTER_INVOKER_ERROR, e);return getRouterResult(false, mRouterRequestWrapper, response);}}
最終會呼叫ResponseInvoker.invoke()方法執行路由。
3.待開發
職責鏈模式,參考OkHttp
整合Fragment
支援非同步
路由快取
路由智慧優先順序(呼叫過的,放最前面)
整合許可權管理
考慮需要登入的情況,統一處理
總結
考拉路由框架與其他路由框架相比,目前功能較簡單,目的也僅是支援頁面跳轉。為了達到對開發者友好、使用簡單的目的,本文在設計路由框架的過程中使用了一些簡單的設計模式,使得整個系統的可擴充套件性較強,也能夠充分的滿足考拉的業務需求。