天天看點

網易考拉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),在網絡當中能夠被别人通路,也可以通路已經被定義了的頁面。路由常見的使用場景有以下幾種:

  1. App接收到一個通知,點選通知打開App的某個頁面(OuterStartActivity)
  2. 浏覽器App中點選某個連結打開App的某個頁面(OuterStartActivity)
  3. App的H5活動頁面打開一個連結,可能是H5跳轉,也可能是跳轉到某一個native頁面(WebViewActivity)
  4. 打開頁面需要某些條件,先驗證完條件,再去打開那個頁面(需要登入)
  5. App内的跳轉,可以減少手動建構Intent的成本,同時可以統一攜帶部分參數到下一個頁面(打點)

    除此之外,使用路由可以避免上述弊端,能夠降低開發者頁面跳轉的成本。

2.考拉路由總線

2.1 路由架構

網易考拉Android用戶端路由總線設計

考拉路由架構主要分為三個子產品:路由收集、路由初始化以及頁面路由。路由收集階段,定義了基于Activity類的注解,通過Android Processing Tool(以下簡稱“APT”)收集路由資訊并生成路由表類;路由初始化階段,根據生成的路由表資訊注入路由字典;頁面路由階段,則通過路由字典查找路由資訊,并根據查找結果定制不同的路由政策略。

2.2 路由設計思路

總的來說,考拉路由設計追求的是功能子產品的解耦,能夠實作基本路由功能,以及開發者使用上足夠簡單。考拉路由的前兩個階段對于路由使用者幾乎是無成本的,隻需要在使用路由的頁面定義一個類注解@Router即可,頁面路由的使用也相當簡單,後面會詳細介紹。

功能設計

路由在一定程度上和網絡請求是類似的,可以分為請求、處理以及響應三個階段。這三個階段對使用者來說既可以是透明的,也可以在路由過程中進行攔截處理。考拉路由架構目前支援的功能有:

1.支援基本Activity的啟動,以及startActivityForResult回調;

2.支援不同協定執行不同跳轉;(kaola://、http(s)://、native://等) 3.支援多個SCHEME/HOST/PATH跳轉至同一個頁面;((pre.).kaola.com(.hk))

4.支援路由的正則比對;

5.支援Activity啟動使用不同的Flag;

6.支援路由的優先級配置;

7.支援對路由的動态攔截、監聽以及降級;

以上功能保證了考拉業務子產品間的解耦,也能夠滿足目前産品和營運的需求。

接口設計

網易考拉Android用戶端路由總線設計

一個好的子產品或架構,需要事先設計好接口,預留足夠的權限供調用者支配,才能滿足各種各樣的需求。考拉路由架構在設計過程中使用了常見的設計模式,如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() {
            @Override
            public 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 {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // 初始化相關環境資訊
        mFiler = processingEnv.getFiler();
        elementUtil = processingEnv.getElementUtils();
        typeUtil = processingEnv.getTypeUtils();
        Log.setLogger(processingEnv.getMessager());
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportAnnotationTypes = new HashSet<>();
        // 擷取需要處理的注解類型,目前隻處理Router注解
        supportAnnotationTypes.add(Router.class.getCanonicalName());
        return supportAnnotationTypes;
    }
    @Override
    public 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.java
    JavaFile.builder(RouterProvider.OUTPUT_DIRECTORY, typeSpec.build()).build().writeTo(mFiler);
    // Generate RouterGenerator
    JavaFile.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 {
  @Override
  public 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(); 
}
           

路由的查找過程相對複雜,除了周遊路由初始化以後導入記憶體的路由表,還需要判斷各種各樣的前置條件。具體的條件判斷代碼中有相關注釋。

@Override
public 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;
}
@Override
public 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);
    }
    // 生成相應的Intent
    if (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

支援異步

路由緩存

路由智能優先級(調用過的,放最前面)

內建權限管理

考慮需要登入的情況,統一處理

總結

考拉路由架構與其他路由架構相比,目前功能較簡單,目的也僅是支援頁面跳轉。為了達到對開發者友好、使用簡單的目的,本文在設計路由架構的過程中使用了一些簡單的設計模式,使得整個系統的可擴充性較強,也能夠充分的滿足考拉的業務需求。

更多Android進階技術,職業生涯規劃,産品,思維,行業觀察,談天說地。加Android架構師

群;701740775。

繼續閱讀