溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

發(fā)布時間:2021-12-02 13:59:07 來源:億速云 閱讀:131 作者:iii 欄目:移動開發(fā)

本篇內(nèi)容介紹了“網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

1.前言

當(dāng)前,Android路由框架已經(jīng)有很多了,如雨后春筍般出現(xiàn),大概是因為去年提出了Android組件化的概念。當(dāng)一個產(chǎn)品的業(yè)務(wù)規(guī)模上升到一定程度,或者是跨團(tuán)隊開發(fā)時,團(tuán)隊/模塊間的合作問題就會暴露出來。如何保持團(tuán)隊間業(yè)務(wù)的往來?如何互不影響或干涉對方的開發(fā)進(jìn)度?如何調(diào)用業(yè)務(wù)方的功能?組件化給上述問題提供了一個答案。組件化所要解決的核心問題是解耦,路由正是為了解決模塊間的解耦而出現(xiàn)的。

1.1 傳統(tǒng)的頁面跳轉(zhuǎn)

頁面跳轉(zhuǎn)主要分為三種,App頁面間跳轉(zhuǎn)、H5跳轉(zhuǎn)回App頁面以及App跳轉(zhuǎn)至H5。

App頁面間跳轉(zhuǎn)

App頁面間的跳轉(zhuǎn),對于新手來說一般會在跳轉(zhuǎn)的頁面使用如下代碼:

Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("dataKey", "dataValue");
startActivity(intent);

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

對于有一定經(jīng)驗的程序員,會在跳轉(zhuǎn)的類生成自己的跳轉(zhuǎn)方法:

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);
    }
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

無論使用哪種方式,本質(zhì)都是生成一個Intent,然后再通過Context.startActivity(Intent)/Activity.startActivityForResult(Intent, int)實現(xiàn)頁面跳轉(zhuǎn)。這種方式的不足之處是當(dāng)包含多個模塊,但模塊間沒有相互依賴時,這時候的跳轉(zhuǎn)會變得相當(dāng)困難。如果已知其他模塊的類名以及對應(yīng)的路徑,可以通過Intent.setComponent(Component)方法啟動其他模塊的頁面,但往往模塊的類名是有可能變化的,一旦業(yè)務(wù)方把模塊換個名字,這種隱藏的Bug對于開發(fā)的內(nèi)心來說是崩潰的。另一方面,這種重復(fù)的模板代碼,每次至少寫兩行才能實現(xiàn)頁面跳轉(zhuǎn),代碼存在冗余。

H5-App頁面跳轉(zhuǎn)

對于考拉這種電商應(yīng)用,活動頁面具有時效性和即時性,這兩種特性在任何時候都需要得到保障。運(yùn)營隨時有可能更改活動頁面,也有可能要求點(diǎn)擊某個鏈接就能跳轉(zhuǎn)到一個App頁面。傳統(tǒng)的做法是對WebViewClient.shouldOverrideUrlLoading(WebView, String)進(jìn)行攔截,判斷url是否有對應(yīng)的App頁面可以跳轉(zhuǎn),然后取出url中的params封裝成一個Intent傳遞并啟動App頁面。

感受一下在考拉App工程中曾經(jīng)出現(xiàn)過的下面這段代碼:

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;
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

這段代碼整整260行,看到代碼時我的內(nèi)心是崩潰的。這種做法的弊端在于:

  • 判斷不合理。上述代碼僅判斷了HOST是否包含StringConstants.KAO_LA,然后根據(jù)PATH區(qū)分跳轉(zhuǎn)到哪個頁面,PATH也只判斷了起始部分,當(dāng)URL越來越多的時候很有可能造成誤判。

  • 耦合性太強(qiáng)。已知攔截的所有頁面的引用都必須能夠拿到,否則無法跳轉(zhuǎn);

  • 代碼混亂。PATH非常多,從眾多的PATH中匹配多個已知的App頁面,想必要判斷匹配規(guī)則就要寫很多函數(shù)解決;

  • 攔截過程不透明。開發(fā)者很難在URL攔截的過程中加入自己的業(yè)務(wù)邏輯,如打點(diǎn)、啟動Activity前添加特定的Flag等;

  • 沒有優(yōu)先級概念,也無法降級處理。同一個URL,只要第一個匹配到App頁面,就只能打開這個頁面,無法通過調(diào)整優(yōu)先級跳轉(zhuǎn)到別的頁面或者使用H5打開。
    App頁面-H5跳轉(zhuǎn)

這種情況不必多說,啟動一個WebViewActivity即可。

1.2 頁面路由的意義

路由最先被應(yīng)用于網(wǎng)絡(luò)中,路由的定義是通過互聯(lián)的網(wǎng)絡(luò)把信息從源地址傳輸?shù)侥康牡刂返幕顒印m撁嫣D(zhuǎn)也是相當(dāng)于從源頁面跳轉(zhuǎn)到目標(biāo)頁面的過程,每個頁面可以定義為一個統(tǒng)一資源標(biāo)識符(URI),在網(wǎng)絡(luò)當(dāng)中能夠被別人訪問,也可以訪問已經(jīng)被定義了的頁面。路由常見的使用場景有以下幾種:

  1. App接收到一個通知,點(diǎn)擊通知打開App的某個頁面(OuterStartActivity)

  2. 瀏覽器App中點(diǎn)擊某個鏈接打開App的某個頁面(OuterStartActivity)

  3. App的H5活動頁面打開一個鏈接,可能是H5跳轉(zhuǎn),也可能是跳轉(zhuǎn)到某一個native頁面(WebViewActivity)

  4. 打開頁面需要某些條件,先驗證完條件,再去打開那個頁面(需要登錄)

  5. App內(nèi)的跳轉(zhuǎn),可以減少手動構(gòu)建Intent的成本,同時可以統(tǒng)一攜帶部分參數(shù)到下一個頁面(打點(diǎn))
    除此之外,使用路由可以避免上述弊端,能夠降低開發(fā)者頁面跳轉(zhuǎn)的成本。

2.考拉路由總線

2.1 路由框架

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

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

2.2 路由設(shè)計思路

總的來說,考拉路由設(shè)計追求的是功能模塊的解耦,能夠?qū)崿F(xiàn)基本路由功能,以及開發(fā)者使用上足夠簡單??祭酚傻那皟蓚€階段對于路由使用者幾乎是無成本的,只需要在使用路由的頁面定義一個類注解@Router即可,頁面路由的使用也相當(dāng)簡單,后面會詳細(xì)介紹。

功能設(shè)計

路由在一定程度上和網(wǎng)絡(luò)請求是類似的,可以分為請求、處理以及響應(yīng)三個階段。這三個階段對使用者來說既可以是透明的,也可以在路由過程中進(jìn)行攔截處理??祭酚煽蚣苣壳爸С值墓δ苡校?/p>

1.支持基本Activity的啟動,以及startActivityForResult回調(diào);?
2.支持不同協(xié)議執(zhí)行不同跳轉(zhuǎn);(kaola://、http(s)://、native://等)? 3.支持多個SCHEME/HOST/PATH跳轉(zhuǎn)至同一個頁面;((pre.).kaola.com(.hk))?
4.支持路由的正則匹配;?
5.支持Activity啟動使用不同的Flag;?
6.支持路由的優(yōu)先級配置;?
7.支持對路由的動態(tài)攔截、監(jiān)聽以及降級;?
以上功能保證了考拉業(yè)務(wù)模塊間的解耦,也能夠滿足目前產(chǎn)品和運(yùn)營的需求。

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

一個好的模塊或框架,需要事先設(shè)計好接口,預(yù)留足夠的權(quán)限供調(diào)用者支配,才能滿足各種各樣的需求??祭酚煽蚣茉谠O(shè)計過程中使用了常見的設(shè)計模式,如Builder模式、Factory模式、Wrapper模式等,并遵循了一些設(shè)計原則。(最近在看第二遍Effective Java,對以下原則深有體會,推薦看一下)

針對接口編程,而不是針對實現(xiàn)編程
這條規(guī)則排在最前面的原因是,針對接口編程,不管是對開發(fā)者還是對使用者,真的是百利而無一害。在路由版本迭代的過程中,底層對接口無論實現(xiàn)怎樣的修改,也不會影響到上層調(diào)用。對于業(yè)務(wù)來說,路由的使用是無感知的。

考拉路由框架在設(shè)計過程中并未完全遵循這條原則,下一個版本的迭代會盡量按照這條原則來實現(xiàn)。但在路由過程中的關(guān)鍵步驟都預(yù)留了接口,具體有:

RouterRequestCallback

public interface RouterRequestCallback {    void onFound(RouterRequest request, RouterResponse response);    boolean onLost(RouterRequest request);
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

路由表中是否能夠匹配到路由信息的回調(diào),如果能夠匹配,則回調(diào)onFound(),如果不能夠匹配,則返回onLost()。onLost()的結(jié)果由開發(fā)來定義,如果返回的結(jié)果是true,則認(rèn)為開發(fā)者處理了這次路由不匹配的結(jié)果,最終返回RouterResult的結(jié)果是成功路由。

RouterHandler

public interface RouterHandler extends RouterStarter {    RouterResponse findResponse(RouterRequest request); 
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

路由處理與啟動接口,根據(jù)給定的路由請求,查找路由信息,根據(jù)路由響應(yīng)結(jié)果,分發(fā)給相應(yīng)的啟動器執(zhí)行后續(xù)頁面跳轉(zhuǎn)。這個接口的設(shè)計不太合理,功能上不完善,后續(xù)會重新設(shè)計這個接口,讓調(diào)用方有權(quán)限干預(yù)查找路由的過程。

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);
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

匹配到路由信息后,真正執(zhí)行路由過程的回調(diào)。beforeRoute()這個方法是在真正路由之前的回調(diào),如果開發(fā)者返回true,則認(rèn)為這條路由信息已被調(diào)用者攔截,不會再回調(diào)后面的doRoute()以及執(zhí)行路由。在路由過程中發(fā)生的任何異常,都會回調(diào)errorRoute()方法,這時候路由中斷。

ResponseInvoker

public interface ResponseInvoker {    void invoke(Context context, Intent intent, Object... args);
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

路由執(zhí)行者。如果開發(fā)需要執(zhí)行路由前進(jìn)行一些全局操作,例如添加額外的信息傳入到下一個Activity,則可以自己實現(xiàn)這個接口。路由框架提供默認(rèn)的實現(xiàn):ActivityInvoker。開發(fā)也可以繼承ActivityInvoker,重寫invoke()方法,先實現(xiàn)自己的業(yè)務(wù)邏輯,再調(diào)用super.invoke()方法。

OnActivityResultListener

public interface OnActivityResultListener {    void onActivityResult(int requestCode, int resultCode, Intent data);
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

特別強(qiáng)調(diào)一下這個Listener。本來這個回調(diào)的作用是方便調(diào)用者在執(zhí)行startActivityForResult的時候可以通過回調(diào)來告知結(jié)果,但由于不保留活動的限制,離開頁面以后這個監(jiān)聽器是無法被系統(tǒng)保存(saveInstanceState)的,因此不推薦在Activity/Fragment中使用回調(diào),而是在非Activity組件/模塊里使用,如View/Window/Dialog。這個過程已經(jīng)由core包里的CoreBaseActivity實現(xiàn),開發(fā)使用的時候,可以直接調(diào)用CoreBaseActivity.startActivityForResult(intent, requestCode, onActivityResultListener),也可以通過KaolaRouter.with(context).url(url).startForResult(requestCode, onActivityResultListener)調(diào)用。例如,要啟動訂單管理頁并回調(diào):

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());
            }
        });

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

RouterResult

public interface RouterResult {    boolean isSuccess();    RouterRequest getRouterRequest();    RouterResponse getRouterResponse();
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

告知路由的結(jié)果,路由結(jié)果可以被干預(yù),例如RouterRequestCallback.onLost(),返回true的時候,路由也是成功的。這個接口不管路由的成功或失敗都會返回。

不隨意暴露不必要的API

“要區(qū)別設(shè)計良好的模塊與設(shè)計不好的模塊,最重要的因素在于,這個模塊對于外部的其他模塊而?言,是否隱藏其內(nèi)部數(shù)據(jù)和其他實現(xiàn)細(xì)節(jié)。設(shè)計良好的模塊會隱藏所有的實現(xiàn)細(xì)節(jié),把它的API與它的實現(xiàn)清晰地隔離開來。然后,模塊之間只通過它們的API進(jìn)行通信,一個模塊不需要知道其他模塊的內(nèi)部工作情況。這被稱為封裝(encapsulation)?!保ㄕ訣ffective Java, P58)

舉個例子,考拉路由框架對路由調(diào)用的入?yún)⒆隽讼拗?,一旦入?yún)?,則不能再做修改,調(diào)用者無需知道路由框架對使用這些參數(shù)怎么實現(xiàn)調(diào)用者想要的功能。實現(xiàn)上,由RouterRequestWrapper繼承自RouterRequestBuilder,后者通過Builder模式給用戶構(gòu)造相關(guān)的參數(shù),前者通過Wrapper模式裝飾RouterRequestBuilder中的所有變量,并在RouterRequestWrapper類中提供所有參數(shù)的get函數(shù),供路由框架使用。

單一職責(zé)

無論是類還是方法,均需要遵循單一職責(zé)原則。一個類實現(xiàn)一個功能,一個方法做一件事。例如,KaolaRouterHandler是考拉路由的處理器,實現(xiàn)了RouterHandler接口,實現(xiàn)路由的查找與轉(zhuǎn)發(fā);RouterRequestBuilder用于收集路由請求所需參數(shù);RouterResponseFactory用于生成路由響應(yīng)的結(jié)果。

提供默認(rèn)實現(xiàn)

針對接口編程的好處是隨時可以替換實現(xiàn),考拉路由框架在路由過程中的所有監(jiān)聽、攔截以及路由過程都提供了默認(rèn)的實現(xiàn)。使用者即可以不關(guān)心底層的實現(xiàn)邏輯,也可以根據(jù)需要替換相關(guān)的實現(xiàn)。

2.3 考拉路由實現(xiàn)原理

考拉路由框架基于注解收集路由信息,通過APT實現(xiàn)路由表的動態(tài)生成,類似于ButterKnife的做法,在運(yùn)行時導(dǎo)入路由表信息,并通過正則表達(dá)式查找路由,根據(jù)路由結(jié)果實現(xiàn)最終的頁面跳轉(zhuǎn)。

收集路由信息

首先定義一個注解@Router,注解里包含了路由協(xié)議、路由主機(jī)、路由路徑以及路由優(yōu)先級。

@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.CLASS) 
public @interface Router {    /**
     * URI協(xié)議,已經(jīng)提供默認(rèn)值,默認(rèn)實現(xiàn)了四種協(xié)議:https、http、kaola、native
     */
    String scheme() default "(https|http|kaola|native)://";
    /**
     * URI主機(jī),已經(jīng)提供默認(rèn)值
     */
    String host() default "(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?";    /**
     * URI路徑,選填,如果使用默認(rèn)值,則只支持本地路由,不支持url攔截
     */
    String value() default "";    /**
     * 路由優(yōu)先級,默認(rèn)為0。
     */
    int priority() default 0;
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

對于需要使用路由的頁面,只需要在類的聲明處加上這個注解,標(biāo)明這個頁面對應(yīng)的路由路徑即可。例如:

@Router("/app/myQuestion.html") 
public class MyQuestionAndAnswerActivity extends BaseActivity { 
    ……
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

那么通過APT生成的標(biāo)記這個頁面的url則是一個正則表達(dá)式:

(https|http|kaola|native)://(pre\.)?(\w+\.)?kaola\.com(\.hk)?/app/myQuestion\.html

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

路由表則是由多條這樣的正則表達(dá)式構(gòu)成。

生成路由表

路由表的生成需要使用APT工具以及Square公司開源的javapoet類庫,目的是根據(jù)我們定義的Router注解讓機(jī)器幫我們“寫代碼”,生成一個Map類型的路由表,其中key根據(jù)Router注解的信息生成對應(yīng)的正則表達(dá)式,value是這個注解對應(yīng)的類的信息集合。首先定義一個RouterProcessor,繼承自AbstractProcessor,

public class RouterProcessor extends AbstractProcessor {    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {        super.init(processingEnv);        // 初始化相關(guān)環(huán)境信息
        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相關(guān)的所有類信息,解析并生成路由表
        Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Router.class);        try {            return parseRoutes(routeElements);
        } catch (Exception e) {
            Log.e(e.getMessage(), e);            return false;
        }
    }
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

上述的三個方法屬于AbstractProcessor的方法,public abstract boolean process(Set annotations, RoundEnvironment roundEnv)是抽象方法,需要子類實現(xiàn)。

private boolean parseRoutes(Set<? extends Element> routeElements) throws IOException {    if (null == routeElements || routeElements.size() == 0) {        return false;
    }    // 獲取Activity類的類型,后面用于判斷是否是其子類
    TypeElement typeActivity = elementUtil.getTypeElement(ACTIVITY);    // 獲取路由Builder類的標(biāo)準(zhǔn)類名
    ClassName routeBuilderCn = ClassName.get(RouteBuilder.class);    // 構(gòu)建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);        // 獲取當(dāng)前Activity的標(biāo)準(zhǔn)類名
        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;
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

最終生成的路由表如下:

/**
 * 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));
    ……
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

其中,RouteBuilder.generateUriFromClazz(Class)的實現(xiàn)如下,目的是生成一條默認(rèn)的與標(biāo)準(zhǔn)類名相關(guān)的native跳轉(zhuǎn)規(guī)則。

public static final String SCHEME_NATIVE = "native://";public static String generateUriFromClazz(Class<?> destination) {
    String rawUri = SCHEME_NATIVE + destination.getCanonicalName();    return rawUri.replaceAll("\\.", "\\\\.");
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

可以看到,路由集合的key是一條正則表達(dá)式,包括了url攔截規(guī)則以及自定義的包含標(biāo)準(zhǔn)類名的native跳轉(zhuǎn)規(guī)則。例如,keyMyQuestionAndAnswerActivity最終生成的key是

((https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/app/myQuestion.html)|(native://com.kaola.modules.answer.myAnswer.MyQuestionAndAnswerActivity)

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

這樣,調(diào)用者不僅可以通過默認(rèn)的攔截規(guī)則

(https|http|kaola|native)://(pre\\.)?(\\w+\\.)?kaola\\.com(\\.hk)?/app/myQuestion.html)

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

跳轉(zhuǎn)到對應(yīng)的頁面,也可以通過

(native://com.kaola.modules.answer.myAnswer.MyQuestionAndAnswerActivity)。

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

這樣的好處是模塊間的跳轉(zhuǎn)也可以使用,不需要依賴引用類。而native跳轉(zhuǎn)會專門生成一個類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";
  ……
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

初始化路由

路由初始化在Application的過程中以同步的方式進(jìn)行。通過獲取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();
    }
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

頁面路由

給定一個url以及上下文環(huán)境,即可使用路由。調(diào)用方式如下:

KaolaRouter.with(context).url(url).start();

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

頁面路由分為路由請求生成,路由查找以及路由結(jié)果執(zhí)行這幾個步驟。路由請求目前較為簡單,僅是封裝了一個RouterRequest接口

public interface RouterRequest {    Uri getUriRequest(); 
}

網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么

路由的查找過程相對復(fù)雜,除了遍歷路由初始化以后導(dǎo)入內(nèi)存的路由表,還需要判斷各種各樣的前置條件。具體的條件判斷代碼中有相關(guān)注釋。

@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);
    }    // 遍歷查找路路由結(jié)果
    RouterResponse response = findResponse(mRouterRequestWrapper);    // 判斷路由結(jié)果,執(zhí)行路由結(jié)果為空時的攔截
    if (null == response) {        boolean handledByCallback = reportLostRequestCallback(mRouterRequestWrapper);        if (!handledByCallback) {
            reportRouterResultError(context, null, RouterError.ROUTER_RESPONSE_NULL,
                    mRouterRequestWrapper.getRouterRequest());
        }        return getRouterResult(handledByCallback, mRouterRequestWrapper, null);
    }    // 獲取路由結(jié)果執(zhí)行的接口
    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);
    }    // 生成相應(yīng)的Intent
    if (null == intent) {
        reportRouterResultError(context, null, RouterError.ROUTER_GENERATE_INTENT_NULL, response);        return getRouterResult(false, mRouterRequestWrapper, response);
    }    // 獲取路由結(jié)果回調(diào)接口,如果為空,則使用默認(rèn)提供的實現(xiàn)
    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);
    }
}

最終會調(diào)用ResponseInvoker.invoke()方法執(zhí)行路由。

“網(wǎng)易考拉Android客戶端路由總線設(shè)計的方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI