WMRouter使用和原始碼分析

CloudClimb發表於2019-04-28

前言

專案元件化過程中使用了WMRouter,為了更好的理解並使用。花了一週的時間研究了一下WMRouter(v1.2.0版本)。下面從四個方面說下自己的理解,希望能給大家提供幫助。

  1. what it is? what can it do?
  2. how to use it ?
  3. how it works ?
  4. why design like that?

一,WMRouter是什麼,能解決什麼問題?

1.1,什麼是路由框架?

路由框架,也就是路由的作用。路由是起什麼作用呢?就像送快遞,從小縣城到省會,再從省會發到北京分撥中心,然後再從北京分撥中心發到回龍觀,再從回龍觀發到具體的小區。路由框架解決的就是如何從A頁面跳轉到B頁面的問題,其會在A和B之間建立數個節點。通過這些節點依次進行轉發,最終達到目的地。

WMRouter使用和原始碼分析

1.2,為什麼需要路由框架?

Android原生已經支援AndroidManifest去管理App跳轉,為什麼要有路由庫?

  • 顯示Intent:專案龐大以後,類依賴耦合太大,不適合元件化拆分
  • 隱式Intent:協作困難,呼叫時候不知道調什麼引數。每個註冊了Scheme的Activity都可以直接開啟,有安全風險
  • AndroidMainfest集中式管理比較臃腫
  • 無法動態修改路由,如果頁面出錯,無法動態降級
  • 無法動態攔截跳轉,譬如未登入的情況下,開啟登入頁面,登入成功後接著開啟剛才想開啟的頁面
  • H5、Android、iOS地址不一樣,不利於統一跳轉

1.3,WMRouter的特點。

WMRouter是一款Android路由框架,主要提供URI分發、ServiceLoader兩大功能(後面會看到,URI分發功能也是用ServiceLoader實現的)

URI分發功能可用於跨module的頁面跳轉、動態下發URI連結的跳轉等場景,特點如下:

  1. 跳轉的頁面支援配置scheme、host、path。
  2. 支援URI正則匹配。
  3. 支援頁面Exported控制,特定頁面不允許外部跳轉
  4. 預設使用註解配置自動註冊,也支援Java程式碼動態註冊。
  5. 某些頁面需要登入等條件才能進入的時候,可以配置攔截器,可在跳轉前執行同步/非同步操作。
  6. 支援單次跳轉特殊操作:Intent設定Extra/Flags、設定跳轉動畫、自定義StartActivity操作等
  7. 支援配置單次和全域性跳轉監聽(可以實現降級策略,也可以自定義處理邏輯)
  8. 完全元件化設計,核心元件均可擴充套件、按需組合,實現靈活強大的功能

WMRouter提供了ServiceLoader模組,類似Java中的 java.util.ServiceLoader,但功能更加完善。通過ServiceLoader可以在一個App的多個模組之間通過介面呼叫程式碼,實現模組解耦,便於實現元件化、模組間通訊,以及和依賴注入類似的功能等。其特點如下:

  1. 使用註解自動配置
  2. 支援獲取介面的所有實現,或根據Key獲取特定實現
  3. 支援獲取Class或獲取例項
  4. 支援無參構造、Context構造,或自定義Factory、Provider構造
  5. 支援單例管理
  6. 支援方法呼叫

二,WMRouter怎麼用?

2.1,URI分發功能基本使用

詳細使用參見WMRouter設計與使用文件,這裡就大概說下總體的流程。

第一步,新增依賴

  • 根目錄的build.gradle配置外掛
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        // Android Gradle外掛
        classpath 'com.android.tools.build:gradle:3.2.1'
        // 新增WMRouter外掛
        classpath "com.sankuai.waimai.router:plugin:1.x"
    }
}
複製程式碼
  • Application模組中的build.gradle:
apply plugin: 'com.android.application'
// 應用WMRouter外掛
apply plugin: 'WMRouter'
複製程式碼
  • 新增對wmrouter的依賴。如果有基礎依賴庫,可以新增到基礎依賴庫。這樣不用每個module都新增。
compile 'com.sankuai.waimai.router:router:1.x'
複製程式碼
  • 在使用了註解的每個模組中配置註解生成器。注意是每個使用了註解的模組都要配置。
annotationProcessor 'com.sankuai.waimai.router:compiler:1.x'
複製程式碼

第二步,Proguard配置

  • WMRouter已經內建了Proguard配置。(詳見原始碼router/proguard-rules.pro),並且在router的build.gradle中已經配置了consumerProguardFiles屬性。所以使用AAR依賴時一般不需要重複配置。這一點挺好的,如果我們自己寫SDK的話,也建議這樣做。
# 保留ServiceLoaderInit類,需要反射呼叫
-keep class com.sankuai.waimai.router.generated.ServiceLoaderInit { *; }
# 避免註解在shrink階段就被移除,導致obfuscate階段註解失效、實現類仍然被混淆
-keep @interface com.sankuai.waimai.router.annotation.RouterService
複製程式碼

第三步,初始化SDK

在Application.onCreate中初始化:最簡單的方式初始化方式就兩行程式碼。

// 建立RootHandler
DefaultRootUriHandler rootHandler = new DefaultRootUriHandler(context);
// 初始化,必須在主執行緒呼叫
Router.init(rootHandler);
複製程式碼

第四步,配置跳轉activity

跳轉的目標Activity,新增註解@RouterUri

@RouterUri(path = "/test/schemehost", scheme = "test", host = "test.demo.com")
public class AdvancedDemoActivity extends BaseActivity {
    ...
}
複製程式碼

第五步, 發起URI跳轉

發起跳轉有好幾種方式,常用的有以下三種。其實最常用的是方式三。

// 方式1,直接傳context和URI
Router.startUri(context, "/account");

// 方式2,或構造一個UriRequest
Router.startUri(new UriRequest(context, "/account"));

// 方式3,使用DefaultUriRequest,最常用
new DefaultUriRequest(context, uri)//傳入context和目標uri
        // startActivityForResult使用的RequestCode
        .activityRequestCode(100)
        // 設定跳轉來源,預設為內部跳轉,還可以是來自WebView、來自Push通知等。
        // 目標Activity可通過UriSourceTools區分跳轉來源。
        .from(UriSourceTools.FROM_INTERNAL)
        // Intent加引數
        .putIntentExtra("test-int", 1)
        .putIntentExtra("test-string", "str")
        // 設定Activity跳轉動畫
        .overridePendingTransition(R.anim.enter_activity, R.anim.exit_activity)
        // 監聽跳轉完成事件
        .onComplete(new OnCompleteListener() {
            @Override
            public void onSuccess(@NonNull UriRequest request) {
                ToastUtils.showToast(request.getContext(), "跳轉成功");
            }

            @Override
            public void onError(@NonNull UriRequest request, int resultCode) {

            }
        })
        // 這裡的start實際也是呼叫了Router.startUri方法
        .start();
複製程式碼

2.2, URI分發功能的高階配置

上面5步只是最基本的URI分發功能使用,SDK還提供了很多設定,方便在實際專案中使用。另外,在使用過程中,還有一些比較容易遺漏的點。下面進行詳細說明。

2.2.1,依賴和混淆配置

  • 注意:如果專案配置的Android Gradle外掛版本比WMRouter依賴的版本低,預設會覆蓋為高版本(可通過./gradlew buildEnvironment命令檢視classpath的依賴關係)。如果不希望被覆蓋,可以嘗試把配置改成:

    classpath("com.sankuai.waimai.router:plugin:1.x") {
        exclude group: 'com.android.tools.build'
    }
    複製程式碼
  • 如果使用了@RouterService註解和ServiceLoader載入例項的功能,會反射呼叫構造方法,應根據實際情況配置Proguard,避免實現類中的構造方法被移除,示例如下。

    # 使用了RouterService註解的實現類,需要避免Proguard把構造方法、方法等成員移除(shrink)或混淆(obfuscate),導致無法反射呼叫。實現類的類名可以混淆。
    -keepclassmembers @com.sankuai.waimai.router.annotation.RouterService class * { *; }
    複製程式碼
    • 這裡的實際情況指的是?到底什麼情況下才必須在專案中配置呢?檢視原始碼後發現,用到反射的地方有:DefaultFactory,ProviderPool,以及自定義的IFactory。 預設是使用DefaultFactory進初始化,最終是使用clazz.newInstance()進行例項化物件的。如果沒有使用自定義工廠、@RouterProvider、或者非預設引數建構函式之外的其他建構函式,就不用新增上面的配置。但是個人感覺,安全起見,最好一開始就將所有混淆都配置上,防止後面更改了邏輯之後遺漏了。
    // CustomFactory.java--自定義工廠
    
        IFactoryService service4 = Router.getService(IFactoryService.class, "/factory", new IFactory() {
            @NonNull
            @Override
            public <T> T create(@NonNull Class<T> clazz) throws Exception {
                return clazz.getConstructor(String.class).newInstance("CreateByCustomFactory");
            }
        });
    複製程式碼

2.2.2,常用的設定

  • com.sankuai.waimai.router.core.RootUriHandler#setGlobalOnCompleteListener:設定全域性跳轉完成的監聽,可以在其中跳轉失敗時執行全域性降級邏輯。
  • com.sankuai.waimai.router.common.DefaultRootUriHandler#lazyInit:提前初始化(個人感覺,初始化指的是,掃描所有註解生成的註冊程式碼、反射獲取class,建立介面的例項、執行註冊,從而生成路由表的過程)。該方法最好放到子執行緒執行,防止啟動過慢。如果沒有提前執行該方法,也會在路由分發的過程中,執行註冊。
  • 配置檢查與Debugger配置。使用註解進行配置,註解往往分散在一個工程的不同程式碼檔案甚至不同的工程中。如果沒有很好的文件或程式碼約束,很容易出現多個頁面配置了相同的URI或Service導致衝突的問題。 因此WMRouter在註解生成階段、APK打包階段,使用註解生成器和Gradle外掛進行檢查,檢查到配置衝突或錯誤會拋異常,中斷編譯。WMRouter中的Debugger用於除錯和Log輸出,執行時也會對一些配置進行檢查,如果出現配置用法錯誤或其他嚴重問題會呼叫Debugger.fatal()丟擲。 Debugger建議配置使用DefaultLogger:
    • 測試環境下開啟Debug模式,fatal錯誤會丟擲異常及時暴漏問題;
    • 線上環境關閉Debug模式,發生問題不拋異常;可以通過覆寫DefaultLogger上報Error和Fatal級別的問題。
DefaultRootUriHandler rootHandler = new DefaultRootUriHandler(context);

//設定全域性跳轉完成的監聽,可以在其中跳轉失敗時執行全域性降級邏輯。
//在DefaultRootUriHandler中預設配置的GlobalOnCompleteListener會在跳轉失敗時彈Toast提示使用者
rootHandler.setGlobalOnCompleteListener();

// 自定義Logger
DefaultLogger logger = new DefaultLogger() {
    @Override
    protected void handleError(Throwable t) {
        super.handleError(t);
        // 此處上報Fatal級別的異常
    }
};
// 設定Logger
Debugger.setLogger(logger);
// Log開關,建議測試環境下開啟,方便排查問題。
Debugger.setEnableLog(true);
// 除錯開關,建議測試環境下開啟。除錯模式下,嚴重問題直接拋異常,及時暴漏出來。
Debugger.setEnableDebug(true);


Router.init(rootHandler);
 // 後臺執行緒懶載入
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void[] objects) {
            Router.lazyInit();
            return null;
        }
    }.execute();

複製程式碼
  • 跳轉來源與Exported控制
    • com.sankuai.waimai.router.common.DefaultUriRequest#from:設定跳轉來源引數,包括內部跳轉、外部跳轉、來自WebView的跳轉、來自Push通知的跳轉等,也可以自定義跳轉來源,具體實現參考UriSourceTools。
    • 跳轉來源可以用於實現Exported控制(SDK中註解裡面的屬性)、埋點統計、特殊業務邏輯等。其中Exported控制類似Android中Activity原生的Exported屬性,預設為false,表示不允許來自外部的跳轉,從而避免一些安全問題或功能異常。外部跳轉由UriProxyActivity統一接收,然後呼叫WMRouter跳轉並設定from為UriSourceTools.FROM_EXTERNAL,之後UriHandler通過跳轉來源和頁面的Exported配置即可判斷是否允許跳轉。
    • 通過UriSourceTools.setDisableExportedControl可以開啟或關閉Exported控制。
    /** 無效來源 */
    public static final int FROM_INVALID = 0;
    /** 外部跳轉 */
    public static final int FROM_EXTERNAL = FROM_INVALID + 1;
    /** 內部跳轉*/
    public static final int FROM_INTERNAL = FROM_EXTERNAL + 1;
    /** 從WebView跳轉 */
    public static final int FROM_WEBVIEW = FROM_INTERNAL + 1;
    /** 從Push跳轉 */
    public static final int FROM_PUSH = FROM_WEBVIEW + 1;
複製程式碼

2.3,URI分發功能的註解

2.3.1,RouterUri註解

最常用,基本只用這個註解就可以滿足URI分發需求。根據URI的scheme+host,尋找並分發給對應的PathHandler,之後PathHandler再根據path匹配RouterUri註解配置的節點。可用於Activity或UriHandler的非抽象子類(Activity也會被轉化成UriHandler,在Activity中可以通過Intent.getData()獲取到URI)

引數如下:

  • path:跳轉URI要用的path,必填。path應該以"/"開頭,支援配置多個path。
  • scheme、host:跳轉URI的scheme和host,可選。
  • exported:是否允許外部跳轉,可選,預設為false。
  • interceptors:要新增的Interceptor,可選,支援配置多個。

說明:

  1. WMRouter支援多scheme+host+path的跳轉,也支援只有path的跳轉。如果RouterUri中配置了scheme、host、path,則跳轉時應使用scheme+host+path的完整路徑;如果RouterUri中只配置了path,則跳轉應直接使用path。

  2. 由於多數場景下往往只需要一個固定的scheme+host,不想在每個RouterUri註解上都寫一遍scheme、host,這種場景可以在初始化時用new DefaultRootUriHandler("scheme", "host")指定預設的scheme、host,RouterUri沒有配置的欄位會使用這個預設值。

舉例

1、使用者賬戶頁面只配置path;跳轉前要先登入,因此新增了一個LoginInterceptor。

@RouterUri(path = "/account", interceptors = LoginInterceptor.class)
public class UserAccountActivity extends Activity {

}
複製程式碼
Router.startUri(context, "/account");
複製程式碼

2、一個頁面配置多個path。

@RouterUri(scheme = "demo_scheme", host = "demo_host", path = {"/path1""/path2"})
public class TestActivity extends Activity {

}
複製程式碼
Router.startUri(context, "demo_scheme://demo_host/path1");
Router.startUri(context, "demo_scheme://demo_host/path2");
複製程式碼

3、根據後臺下發的ABTest策略,同一個連結跳轉不同的Activity。其中AbsActivityHandler是WMRouter提供的用於跳轉Activity的UriHandler通用基類。

@RouterUri(path = "/home")
public class HomeABTestHandler extends AbsActivityHandler {

    @NonNull
    @Override
    protected Intent createIntent(@NonNull UriRequest request) {
        if (FakeABTestService.getHomeABStrategy().equals("A")) {
            return new Intent(request.getContext(), HomeActivityA.class);
        } else {
            return new Intent(request.getContext(), HomeActivityB.class);
        }
    }
}
複製程式碼
Router.startUri(context, "/home");
複製程式碼

2.3.2,RouterRegex註解

RouterRegex註解也可以用於Activity和UriHandler,通過正則進行URI匹配。

引數如下:

  • regex:正規表示式,必填。用於匹配完整的URI字串。
  • priority:優先順序,數字越大越先匹配,可選,預設為0。優先順序相同時,不保證先後順序。
  • exported:是否允許外部跳轉,可選,預設為false。
  • interceptors:要新增的Interceptor,可選,支援配置多個。
舉例

1、對於指定域名的http(s)連結,使用特定的WebViewActivity開啟。

@RouterRegex(regex = "http(s)?://(.*\\.)?(meituan|sankuai|dianping)\\.(com|info|cn).*", priority = 2)
public class WebViewActivity extends BaseActivity {

}
複製程式碼

2、對於其他http(s)連結,使用系統瀏覽器開啟。

@RouterRegex(regex = "http(s)?://.*", priority = 1)
public class SystemBrowserHandler extends UriHandler {

    @Override
    protected boolean shouldHandle(@NonNull UriRequest request) {
        return true;
    }

    @Override
    protected void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback) {
        try {
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_VIEW);
            intent.setData(request.getUri());
            request.getContext().startActivity(intent);
            callback.onComplete(UriResult.CODE_SUCCESS);
        } catch (Exception e) {
            callback.onComplete(UriResult.CODE_ERROR);
        }
    }
}
複製程式碼

2.3.3,RouterPage註解

RouterPage註解用於指定內部頁面跳轉,和RouterUri註解相比,RouterPage註解對應的scheme和host為固定的wm_router://page,不可配置,exported為false也不可配置。感覺這個是由於歷史原因存在的一個註解。本質和RouterUri註解是一樣的。我們自己的專案不會用到這個。所以不詳細介紹了。有興趣的小夥伴可以自行檢視WMRouter設計與使用文件

2.4,核心元件的擴充套件

2.4.1,自定義UriHandler

  • 上面2.3.1中的HomeABTestHandler就屬於自定義UriHandler,只不過繼承了AbsActivityHandler而不是UriHandler。實際上,AbsActivityHandler也是UriHandler的子類(後面分析原始碼的時候會講到)。更多情況下,是直接繼承UriHandler來實現自定義的需求。
  • 這是另一個例子:注意,直接繼承UriHandler的實現類,要重寫shouldHandle方法和handleInternal方法。
/** 跳轉到系統自帶瀏覽器 */
@RouterRegex(regex = DemoConstant.HTTP_URL_REGEX)
public class SystemBrowserHandler extends UriHandler {

    @Override
    protected boolean shouldHandle(@NonNull UriRequest request) {
        return true;
    }

    @Override
    protected void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback) {
        try {
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_VIEW);
            intent.setData(request.getUri());
            request.getContext().startActivity(intent);
            callback.onComplete(UriResult.CODE_SUCCESS);
        } catch (Exception e) {
            callback.onComplete(UriResult.CODE_ERROR);
        }
    }
}

複製程式碼
  • 還需要注意:如果自定義的UriHandler上面有@RouterUri、@RouterRegex或者@RouterPage註解,註解會自動將自定義UriHandler註冊到路由表。如果沒有這三個註解,那麼需要在Router.init之前,將自定義的UriHandler新增到RootUriHandler的例項中(DefaultRootUriHandler是最常用的RootUriHandler例項)。
// 建立RootHandler
DefaultRootUriHandler rootHandler = new DefaultRootUriHandler(context);
rootHandler.addChildHandler(new UriHandler() {
            @Override
            protected boolean shouldHandle(@NonNull UriRequest request) {
                return false;
            }
            @Override
            protected void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback) {

            }
        });
// 初始化,必須在主執行緒呼叫
Router.init(rootHandler);
複製程式碼

2.4.2,自定義UriInterceptor

  • UriInterceptor為攔截器,不做最終的URI跳轉操作,但可以在最終的跳轉前進行各種同步/非同步操作,常見操作舉例如下:

    • URI跳轉攔截,禁止特定的URI跳轉,直接返回403(例如禁止跳轉非meituan域名的HTTP連結)
    • URI引數修改(例如在HTTP連結末尾新增query引數)
    • 各種中間處理(例如開啟登入頁登入、獲取定位、髮網路請求)
    • ……
  • 每個UriHandler都可以新增若干UriInterceptor。在UriHandler基類中,handle()方法先呼叫抽象方法shouldHandle()判斷是否要處理UriRequest,如果需要處理,則逐個執行Interceptor,最後再呼叫handleInternal()方法進行跳轉操作。 舉例來說,跳轉某些頁面需要先登入,可以實現一個LoginInterceptor如下。

public class LoginInterceptor implements UriInterceptor {

    @Override
    public void intercept(@NonNull UriRequest request, @NonNull final UriCallback callback) {
        final FakeAccountService accountService = FakeAccountService.getInstance();
        if (accountService.isLogin()) {
            // 已經登入,不需處理,繼續跳轉流程
            callback.onNext();
        } else {
            // 沒登入,提示登入並啟動登入頁
            Toast.makeText(request.getContext(), "請先登入~", Toast.LENGTH_SHORT).show();
            accountService.registerObserver(new FakeAccountService.Observer() {
                @Override
                public void onLoginSuccess() {
                    accountService.unregisterObserver(this);
                    // 登入成功,繼續跳轉
                    callback.onNext();
                }

                @Override
                public void onLoginFailure() {
                    accountService.unregisterObserver(this);
                    // 登入失敗,終止流程,返回錯誤ResultCode
                    callback.onComplete(CustomUriResult.CODE_LOGIN_FAILURE);
                }
            });
            // 啟動登入頁
            startActivity(request.getContext(), LoginActivity.class);
        }
    }
}
複製程式碼

需要注意的是:

  • 每個UriHandler都可以新增若干UriInterceptor(通過com.sankuai.waimai.router.core.UriHandler#addInterceptor方法新增),如果新增多個攔截器。則會安裝新增的順序封裝成一個攔截器鏈,依次執行。熟悉責任鏈模式的小夥伴應該很容易就能看明白。感興趣的小夥伴可以檢視原始碼,com.sankuai.waimai.router.core.UriHandler#handle和com.sankuai.waimai.router.core.ChainedInterceptor。 - 如果當前攔截器的intercept方法中執行了com.sankuai.waimai.router.core.UriCallback#onNext方法,就會接著執行下一個攔截器。 - 如果當前攔截器的intercept方法中執行了com.sankuai.waimai.router.core.UriCallback#onComplete方法,就會結束攔截器鏈的呼叫。開始執行UriHandler的handleInternal方法。 - 如果當攔截器鏈所有的攔截器都遍歷完畢了。開始執行UriHandler的handleInternal方法。
  • 路由分發可能會經過好幾層UriHandler(比如從DefaultRootUriHandler分發到UriAnnotationHandler,然後再從UriAnnotationHandler分發到PathHandler,再從PathHandler分發到ActivityHandler),而每一個UriHandler的例項都有自己的一串攔截器鏈。因為SDK是通過RootUriHandlerstartUri開始分發的,所以,如果要新增全域性的攔截器,就可以通過給RootUriHandler的子類例項物件,比如DefaultRootUriHandler物件新增攔截器的方式實現。
  • 攔截器的新增方式有兩種,一種是寫在上述三個註解裡面(最終通過UriTargetTools類的parse方法新增到目標UriHandler的攔截器鏈中),一種是直接通過UriHandler的addInterceptor方法新增。
  • 攔截器只能新增到UriHandler的例項物件中(可以是activity或者其他UriHandler實現類),並不能新增到某個方法上,這有什麼影響呢?比如商品詳情頁activity有個加購物車按鈕,點選聊天按鈕,會判斷是否登陸,如果已經登陸了,直接執行加購物車程式碼邏輯。如果沒有登陸,會先跳轉到登陸頁面,然後登陸成功之後再繼續執行加購物車程式碼邏輯。這個加購物車方法,是沒有辦法通過新增攔截器的方式解決的。除非是某個activity,必須登陸才能進入,這種情況下才可以通過給這個activity新增登陸攔截器。感興趣的小夥伴可以參考github的issue:關於登入攔截器的問題

2.4.3,自定義RootUriHandler

根據實際情況,可以自定義具有各種功能的UriHandler和UriInterceptor,前面已經提到,不再贅述。一般使用DefaultRootHandler和DefaultUriRequest,以及少量自定義的UriHandler已經可以滿足絕大多數需求。如果有更復雜的場景需要,WMRouter中的核心元件可以通過繼承、組合等方式實現更靈活的定製。例如自定義RootUriHandler示例如下:

// 自定義RootUriHandler
public class CustomRootUriHandler extends RootUriHandler {
    // ...
    public CustomRootUriHandler() {
        // 新增Uri註解支援
        addHandler(new UriAnnotationHandler());
        // 新增一個自定義的HttpHandler
        addHandler(new CustomHttpHandler());
    }
}

// 自定義UriRequest
public class CustomUriRequest extends UriRequest {
    // ...
    public CustomUriRequest setCustomProperties(String s) {
        putField("custom_properties", s);
        return this;
    }
}

// 初始化
Router.init(new CustomRootUriHandler());

// 啟動Uri
CustomUriRequest request = new CustomUriRequest(mContext, url)
    .setCustomProperties("xxx");
Router.startUri(request);
複製程式碼
  • 個人感覺,自定義RootUriHandler一般沒有必要,如果真的有特殊需求,建議看懂原始碼的執行邏輯之後才開始動手。要知道,RootUriHandler的startUri方法是整個router的開始路由分發的入口。DefaultRootUriHandler構造方法中新增了SDK預設支援的各種子節點(UriAnnotationHandler,RegexAnnotationHandler,PageAnnotationHandler,StartUriHandler)。自定義的RootUriHandler最好也要加上這些子節點,否則會影響SDK的基本功能。
 public DefaultRootUriHandler(Context context,
                                 @Nullable String defaultScheme, @Nullable String defaultHost) {
        super(context);
        mPageAnnotationHandler = createPageAnnotationHandler();
        mUriAnnotationHandler = createUriAnnotationHandler(defaultScheme, defaultHost);
        mRegexAnnotationHandler = createRegexAnnotationHandler();

        // 按優先順序排序,數字越大越先執行

        // 處理RouterPage註解定義的內部頁面跳轉,如果註解沒定義,直接結束分發
        addChildHandler(mPageAnnotationHandler, 300);
        // 處理RouterUri註解定義的URI跳轉,如果註解沒定義,繼續分發到後面的Handler
        addChildHandler(mUriAnnotationHandler, 200);
        // 處理RouterRegex註解定義的正則匹配
        addChildHandler(mRegexAnnotationHandler, 100);
        // 新增其他使用者自定義Handler...

        // 都沒有處理,則嘗試使用預設的StartUriHandler直接啟動Uri
        addChildHandler(new StartUriHandler(), -100);
        // 全域性OnCompleteListener,用於輸出跳轉失敗提示資訊
        setGlobalOnCompleteListener(DefaultOnCompleteListener.INSTANCE);
    }
複製程式碼

2.4.4,自定義ActivityLauncher

通過檢視原始碼發現,所有Activity型別的UriHandler(就是通過在Activity類名上面新增註解,從而通過UriTargetTools的toHandler方法,生成的UriHandler例項),路由分發的最後一步(跳轉該activity),都是通過ActivityLauncher介面的startActivity方法執行的。而SDK提供了ActivityLauncher介面的預設實現類DefaultActivityLauncher。我們可以在這裡hook一些核心的方法,執行自己的跳轉邏輯。比如下面的例子,跳轉到Activity之前,判斷intent中的context是不是Activity型別的,如果不是,那麼加上Intent.FLAG_ACTIVITY_NEW_TASK

public class XinActivityLauncher extends DefaultActivityLauncher {
    //...省略程式碼
    @Override
    protected int startActivityByDefault(UriRequest request, Context context, Intent intent, Integer requestCode, boolean internal) {
        try {
            Bundle options = (Bundle)request.getField(Bundle.class, FIELD_START_ACTIVITY_OPTIONS);
            if (requestCode != null && context instanceof Activity) {
                ActivityCompat.startActivityForResult((Activity)context, intent, requestCode, options);
            } else {
                if (!(context instanceof Activity)) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }
                ActivityCompat.startActivity(context, intent, options);
            }

            this.doAnimation(request);
            if (internal) {
                request.putField(FIELD_STARTED_ACTIVITY, 1);
                Debugger.i("    internal activity started, request = %s", new Object[]{request});
            } else {
                request.putField(FIELD_STARTED_ACTIVITY, 2);
                Debugger.i("    external activity started, request = %s", new Object[]{request});
            }

            return 200;
        } catch (ActivityNotFoundException var7) {
            Debugger.w(var7);
            return 404;
        } catch (SecurityException var8) {
            Debugger.w(var8);
            return 403;
        }
    }
    //...省略程式碼
}
複製程式碼

2.5,ServiceLoader的使用

2.5.1,什麼是ServiceLoader?有什麼作用?

簡單來說,ServiceLoader的核心作用就是:根據介面(或抽象類)名,找到介面(或抽象類)的具體例項,如果一個介面對應多個例項,那麼再根據不同例項的key找到具體的介面例項。

在實現元件化的專案中,很可能多個業務module之間是沒有依賴關係的但是確實有可能不同的業務module之間還是有業務邏輯的耦合的。比如:

  • 跨module跳轉頁面,通過WMRouter的URI分發功能解決;
  • 業務層,業務moduleA需要用到業務moduleB的某些程式碼邏輯。這個時候就輪到ServiceLoader發揮作用了。可以把這部分邏輯抽象成一個介面(抽象類)。將介面類下沉到基礎module中,然後介面的實現放到業務moduleB,介面的實現類上面新增@RouterService註解,這樣業務moduleA就能夠通過WMRouter獲取業務moduleB的介面實現了。

WMRouter使用和原始碼分析
WMRouter使用和原始碼分析

2.5.2,怎麼使用?

ServiceLoader模組使用主要分三步:

  1. 定義Java介面或抽象類。如果需要跨module呼叫介面實現,要把介面下沉,確保不同module都能獲取介面抽象類。
  2. 實現介面或抽象類,然後新增@RotuerService註解(介面抽象類,key,是否單例)。
  3. 通過Router的一系列getService方法獲取介面的實現類的Class或者例項物件。

RouterService註解

通過RouterService註解宣告實現類所實現的介面(或繼承的父類,例如Activity、Fragment、Object等,後文不再重複說明),一個介面可以有多個實現類,一個類也可以同時實現多個介面。RouterService註解的引數如下:

  • interfaces:必選引數。宣告實現的介面,可配置多個。
  • key:可選引數。同一介面的不同實現類,通過唯一的key進行區分。
  • singleton:可選引數。宣告實現類是否為單例,預設為false。

示例如下:

public interface IService {

}

@RouterService(interfaces = IService.class, key = 'key1')
public static class ServiceImpl1 implements IService {

}

@RouterService(interfaces = IService.class, key = 'key2', singleton = true)
public static class ServiceImpl2 implements IService {

}
複製程式碼

獲取實現類的方式

可以直接獲取實現類的Class,例如獲取Activity的Class進行頁面跳轉。

  • 指定介面和Key,獲取某個實現類的Class(要求註解宣告時指定了Key)
Class<IService> clazz = Router.getServiceClass(IService.class, "key1");
複製程式碼
  • 指定介面,獲取註解宣告的所有實現類的Class
List<Class<IService>> classes = Router.getAllServiceClasses(IService.class);
複製程式碼

獲取實現類的例項

ServiceLoader更常見的使用場景,是獲取實現類的例項而不是Class。實現類的構造在ServiceLoader中最終由Factory實現,構造失敗會返回null或空陣列。

  • 無引數構造
// 使用無參建構函式
IService service = Router.getService(IService.class, "key1");
List<IService> list = Router.getAllServices(IService.class);
複製程式碼
  • Context引數構造
// 使用Context引數構造
IService service = Router.getService(IService.class, context);
List<IService> list = Router.getAllServices(IService.class, context);
複製程式碼
  • 自定義Factory通過反射構造

對於實現類有特殊建構函式的情況,可以通過Factory自行從class獲取構造方法進行構造,示例如下:

// 使用自定義Factory
IFactory factory = new IFactory() {
    public Object create(Class clazz) {
        return clazz.getConstructor().newInstance();
    }
};
IService service = Router.getService(IService.class, factory);
List<IService> list = Router.getAllServices(IService.class, factory);
複製程式碼
  • 使用Provider提供例項

在宣告實現類時,可以在類中定義一個返回值型別為該實現類且無引數的靜態方法,並使用RouterProvider註解標註。當呼叫Router獲取例項時,如果沒有指定Factory,則優先呼叫Provider方法獲取例項,找不到Provider再使用無引數構造。使用示例如下:


@RouterService(interfaces = IService.class, key = 'key', singleton = true)
public static class ServiceImpl implements IService {

    public static final ServiceImpl INSTANCE = new ServiceImpl();

    // 使用註解宣告該方法是一個Provider
    @RouterProvider
    public static ServiceImpl provideInstance() {
        return INSTANCE;
    }
}

// 呼叫時不傳Factory,優先找Provider,找不到再使用無引數構造
IService service = Router.getService(IService.class, "key");
List<IService> list = Router.getAllServices(IService.class);
複製程式碼

singleton引數說明

註解宣告為singleton的單例實現類,在呼叫getService()/getAllServices()方式獲取例項時,例項會由單例快取池管理,WMRouter中不會重複構造,且執行緒安全。

注意:當通過ServiceLoader獲取Class、直接呼叫等其他方式使用實現類時,應避免重複建立物件,否則會導致單例失效。可以結合Provider確保例項不會重複建立。

三,為了解決這些問題,WMRouter內部是如何實現的。分析原始碼

WMRouter的核心原理大概就是,通過註解標註路由資訊,在編譯期動態掃描路由資訊,生成載入路由表資訊的java類。並利用 gradle transform和asm生成載入全部路由資訊的class檔案。在app執行時,路由框架反射呼叫這個class檔案,從而完成了路由表的裝載。

WMRouter使用和原始碼分析

3.1,整體流程

3.1.1,路由關係生成

編譯時註解生成ServiceInit_*類,UriAnnotationInit_*類等輔助註冊程式碼。

  • 首先,編譯的時候,根據@RouterUri,@RouterRegex,@RouterPage,@RouterService註解,生成輔助程式碼。詳細的檔案見下圖。具體的生成原理參見路由節點的動態生成

  • 這裡需要注意,@RouterUri,@RouterRegex,@RouterPage這三個註解,會同時生成兩個檔案。以@RouterUri為例進行說明:

    • 檔案1,UriAnnotationInit_**類,其init方法中的每一行,都是本module中使用@RouterUri註解的類的註冊到UriAnnotationHandler的執行程式碼。所謂註冊,其實質就是建立對映關係。需要注意UriAnnotationHandler這個註冊過程,一旦執行,就會開啟整個UriAnnotationHandler這個分支的所有註冊過程。
    • 檔案2,ServiceInit_**類,其init方法中,通過ServiceLoader.put()方法,建立了介面抽象類(IUriAnnotationInit.class),介面實現類(com.sankuai.waimai.router.generated.UriAnnotationInit_72565413b8384a4bebb02d352762d60d.class),介面實現類的key(com.sankuai.waimai.router.generated.UriAnnotationInit_72565413b8384a4bebb02d352762d60d),這三者之間的對映關係。
  • 還需要注意。編譯過程只是生成了能夠進行註冊的程式碼。但是程式碼並沒有執行。只有等到開啟提前載入(執行Router.lazyInit()),或者開啟跳轉(Router#startUri(com.sankuai.waimai.router.core.UriRequest))的時候,才開始註冊。

public class UriAnnotationInit_72565413b8384a4bebb02d352762d60d implements IUriAnnotationInit {
  public void init(UriAnnotationHandler handler) {
    handler.register("", "", "/advanced_demo", "com.sankuai.waimai.router.demo.advanced.AdvancedDemoActivity", false);
  }
}

複製程式碼
public class ServiceInit_eb71854fbd69455ef4e0aa026c2e9881 {
  public static void init() {
    ServiceLoader.put(IUriAnnotationInit.class, "com.sankuai.waimai.router.generated.UriAnnotationInit_72565413b8384a4bebb02d352762d60d", com.sankuai.waimai.router.generated.UriAnnotationInit_72565413b8384a4bebb02d352762d60d.class, false);
  }
}

複製程式碼

WMRouter使用和原始碼分析

gradle外掛,生成ServiceLoaderInit

  • ServiceLoaderInit類,只有一個init方法。其中包含了整個專案所有module的ServiceInit_**輔助類的init執行程式碼。ServiceInit_**輔助類是上面通過註解生成的。
  • 無論使用哪種方式開啟路由分發過程,真正開始分發之前,都會首先執行ServiceLoaderInit類的init方法。為什麼呢?上面我們說到,註解只是生成輔助程式碼,但是並沒有執行。只有執行了ServiceLoaderInit類的init方法,才會真正的執行路由表註冊程式碼,建立真正的對映關係。另外需要注意,改方法執行後,只會建立第一層關係。只有等到開啟提前載入(執行Router.lazyInit()),或者開啟跳轉(Router#startUri(com.sankuai.waimai.router.core.UriRequest))的時候,才開始執行UriAnnotationHandler等子節點分支的註冊程式碼(initAnnotationConfig()),建立對映關係。

WMRouter使用和原始碼分析
詳細過程,參見路由節點的載入

public class ServiceLoaderInit
{
  public static void init()
  {
    ServiceInit_aea7f96d0419b507d9b0ef471913b2f5.init();
    ServiceInit_f3649d9f5ff15a62b844e64ca8434259.init();
    ServiceInit_eb71854fbd69455ef4e0aa026c2e9881.init();
    ServiceInit_b57118238b4f9112ddd862e55789c834.init();
    ServiceInit_f1e07218f6691f962a9f674eb5b4b8bd.init();
    ServiceInit_e694d982fb5d7a3a8c6b7085829e74a6.init();
    ServiceInit_ee5f6404731417fe1433da40fd3c9708.init();
    ServiceInit_9482ef47a8cf887ff1dc4bf705d5fc0a.init();
    ServiceInit_36ed390bf4b81a8381d45028b37cc645.init();
  }
}

複製程式碼

3.1.2,分發過程

  • 通過Router#startUri(com.sankuai.waimai.router.core.UriRequest)開啟跳轉
  • 其中呼叫了getRootHandler().startUri(request);其中getRootHandler()方法獲取RootUriHandler的例項,預設是DefaultRootUriHandler物件。
  • DefaultRootUriHandler是RootUriHandler的子類。其startUri方法呼叫的是父類的。RootUriHandler#startUri方法。
  • 然後其中各種判斷之後,最終呼叫UriHandler#handle方法開始分發。
    /**
     * 處理URI。通常不需要覆寫本方法。
     *
     * @param request  URI跳轉請求
     * @param callback 處理完成後的回撥
     */
    public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
        if (shouldHandle(request)) {
            Debugger.i("%s: handle request %s", this, request);
            if (mInterceptor != null && !request.isSkipInterceptors()) {
                mInterceptor.intercept(request, new UriCallback() {
                    @Override
                    public void onNext() {
                        handleInternal(request, callback);
                    }

                    @Override
                    public void onComplete(int result) {
                        callback.onComplete(result);
                    }
                });
            } else {
                handleInternal(request, callback);
            }
        } else {
            Debugger.i("%s: ignore request %s", this, request);
            callback.onNext();
        }
    }
複製程式碼
  • UriHandler#handle方法中,首先呼叫UriHandler#shouldHandle方法,判斷是否應該處理。這裡要注意,UriHandler#handle在各個UriHanlder的子類中都會呼叫(責任鏈模式)。
  • 分析handle方法的時候,一定要注意當前的UriHandler的具體例項是什麼
    • 第一次執行UriHandler#handle方法,執行第一句程式碼shouldHandle。
    • 當前例項是DefaultRootUriHandler物件,其父類是RootUriHandler,其父類的父類是ChainedHandler。因為DefaultRootUriHandler和RootUriHandler都沒有實現shouldHandle方法而ChainedHandler實現了。
    • 所以,第一次執行UriHandler#handle方法的第一句shouldHandle,執行的是ChainedHandler的shouldHandle方法。該方法會判斷mHandlers是否為空,在DefaultRootUriHandler的建構函式裡面,已經新增了UriAnnotationHandler,PageAnnotationHandler,RegexAnnotationHandler,StartUriHandler等UriHandler物件。所以,shouldHandle返回true,繼續向下執行。
    • 然後判斷攔截器,這塊不影響主體流程,我們先略過,直接看handleInternal(request, callback)這行程式碼。然後和shouldHandle方法一樣,DefaultRootUriHandler例項的handleInternal方法,也是用的ChainedHandler的實現。其handleInternal方法中,呼叫了next方法。開始了對DefaultRootUriHandler的mHandlers集合的遍歷。我們檢視next方法可以發現,在遍歷過程中,如果某個UriHandler執行過程中,呼叫了onNext回撥(代表自己不負責這個request),會繼續執行next方法,交給下一個UriHandler執行。如果如果某個UriHandler執行過程中,呼叫了onComplete回撥,代表這個request是其負責的,就結束遍歷。
    • 舉個例子。上面說過了,在DefaultRootUriHandler的建構函式裡面給mHandlers新增了各種UriHandler物件。比如ChainedHandler的next方法輪詢過程中,正在獲取的UriHandler例項是UriAnnotationHandler,然後呼叫UriAnnotationHandler的handle方法。
    • UriAnnotationHandler的handle中,首先會呼叫LazyInitHelper的ensureInit方法。實際上呼叫的是UriAnnotationHandler的initAnnotationConfig()方法。在該方法中,呼叫com.sankuai.waimai.router.components.DefaultAnnotationLoader#load方法。最終呼叫的是ServiceLoader.load(clazz).getAll(),來獲取所有UriAnnotationInit介面的實現(通過註解生成的),然後呼叫這些實現的init方法。完成了UriAnnotationHandler分支的路由關係的註冊。詳情可參看下面ServiceLoader的講解。
    • 然後呼叫super.handle(),又回到了UriHandler的handle()中。最終呼叫了UriAnnotationHandler的shouldHandle和handleInternal方法。在handleInternal方法中,通過request找到PathHandler,然後交給PathHandler執行其handle方法。然後PathHandler的handleInternal方法中,又根據request找到其內部註冊的UriHandler,如果能找到,就交給這個子UriHandler去執行,如果找不到,就自己執行。這樣就完成了分發過程。是不是感覺跟事件分發很像,只不過是這裡分發的是一個包裝了地址的request而已。
 public DefaultRootUriHandler(Context context,
                                 @Nullable String defaultScheme, @Nullable String defaultHost) {
        super(context);
        mPageAnnotationHandler = createPageAnnotationHandler();
        mUriAnnotationHandler = createUriAnnotationHandler(defaultScheme, defaultHost);
        mRegexAnnotationHandler = createRegexAnnotationHandler();

        // 按優先順序排序,數字越大越先執行

        // 處理RouterPage註解定義的內部頁面跳轉,如果註解沒定義,直接結束分發
        addChildHandler(mPageAnnotationHandler, 300);
        // 處理RouterUri註解定義的URI跳轉,如果註解沒定義,繼續分發到後面的Handler
        addChildHandler(mUriAnnotationHandler, 200);
        // 處理RouterRegex註解定義的正則匹配
        addChildHandler(mRegexAnnotationHandler, 100);
        // 新增其他使用者自定義Handler...

        // 都沒有處理,則嘗試使用預設的StartUriHandler直接啟動Uri
        addChildHandler(new StartUriHandler(), -100);
        // 全域性OnCompleteListener,用於輸出跳轉失敗提示資訊
        setGlobalOnCompleteListener(DefaultOnCompleteListener.INSTANCE);
    }
     @Override
    public void handle(@NonNull UriRequest request, @NonNull UriCallback callback) {
        mInitHelper.ensureInit();
        super.handle(request, callback);
    }

複製程式碼
ChainedHandler{
     @Override
    protected boolean shouldHandle(@NonNull UriRequest request) {
        return !mHandlers.isEmpty();
    }
     @Override
    protected void handleInternal(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
        next(mHandlers.iterator(), request, callback);
    }

    private void next(@NonNull final Iterator<UriHandler> iterator, @NonNull final UriRequest request,
                      @NonNull final UriCallback callback) {
        if (iterator.hasNext()) {
            UriHandler t = iterator.next();
            t.handle(request, new UriCallback() {
                @Override
                public void onNext() {
                    next(iterator, request, callback);
                }

                @Override
                public void onComplete(int resultCode) {
                    callback.onComplete(resultCode);
                }
            });
        } else {
            callback.onNext();
        }
    }
}
複製程式碼
public class UriAnnotationHandler extends UriHandler {
   /**
     * 通過scheme+host找對應的PathHandler,找到了才會處理
     */
    private PathHandler getChild(@NonNull UriRequest request) {
        return mMap.get(request.schemeHost());
    }
    
       @Override
    protected boolean shouldHandle(@NonNull UriRequest request) {
        return getChild(request) != null;
    }

    @Override
    protected void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback) {
        PathHandler pathHandler = getChild(request);
        if (pathHandler != null) {
            pathHandler.handle(request, callback);
        } else {
            // 沒找到的繼續分發
            callback.onNext();
        }
    }
    
}
複製程式碼
public class DefaultAnnotationLoader implements AnnotationLoader {

    public static final AnnotationLoader INSTANCE = new DefaultAnnotationLoader();

    @Override
    public <T extends UriHandler> void load(T handler,
            Class<? extends AnnotationInit<T>> initClass) {
        List<? extends AnnotationInit<T>> services = Router.getAllServices(initClass);
        for (AnnotationInit<T> service : services) {
            service.init(handler);
        }
    }
}
複製程式碼
public class UriAnnotationInit_179aab35b2125f96c7b066a0a2eccf82 implements IUriAnnotationInit {
  public void init(UriAnnotationHandler handler) {
    handler.register("", "", "/service_loader", "com.sankuai.waimai.router.demo.lib2.advanced.ServiceLoaderActivity", false);
    handler.register("", "", "/lib2", "com.sankuai.waimai.router.demo.lib2.basic.DemoLibActivity2", false);
  }
}

複製程式碼

WMRouter使用和原始碼分析

3.2,核心類

3.2.1,UriHandler及其子類

UriHandler就是我們所說的節點。UriHandler用於處理URI跳轉請求,可以巢狀從而逐層分發和處理請求。UriHandler是非同步結構,接收到UriRequest後處理(例如跳轉Activity等),如果處理完成,則呼叫callback.onComplete()並傳入ResultCode;如果沒有處理,則呼叫callback.onNext()繼續分發。

UriHandler及其實現類的類圖整理如下(簡單起見,省略了部分實現):

WMRouter使用和原始碼分析

  • UriHandler是抽象類,其核心在於handle()方法,另外,shouldHandle()handleInternal()方法是抽象方法,由子類去實現。shouldHandle()方法判斷是否應該在當前UriHandler執行分發。handleInternal()是當前UriHandler執行分發的具體邏輯。
  • RootUriHandlerstartUri()方法,是所有分發跳轉的入口。DefaultRootUriHandlerRootUriHandler的預設實現,其建構函式中新增了PageAnnotationHandler,UriAnnotationHandler,RegexAnnotationHandler,StartUriHandler的例項。然後分發跳轉請求的時候,依次交給這四個UriHandler處理,如果某個UriHandler不能夠處理該請求,則交給下一個執行。如果能夠處理該請求,則交給該UriHandler的子節點繼續分發。比如,如果UriAnnotationHandler能夠處理該請求,會交給其子節點PathHandler處理,然後PathHandler交給ActivityHandler處理。
  • 只有PageAnnotationHandler,UriAnnotationHandler,RegexAnnotationHandler這三個子類複寫了UriHandlerhandle()方法。其複寫的目的是為了在真正開始分發之前,呼叫mInitHelper.ensureInit(),確保該分支的路由表已經生成。
  • PageAnnotationHandler寫死了SCHEME和HOST,所以只處理所有格式為 wm_router://page/* 的URI,根據path匹配。
  • UriAnnotationHandler分發到PathHandler,PathHandler分發到ActivityClassNameHandlerActivityHandler是最常用的分發路徑。
  • 如果PageAnnotationHandler,UriAnnotationHandler,RegexAnnotationHandler及其子節點,都沒有處理某個路由請求(處理的意思是:執行了UriCallback的onComplete回撥),則交給StartUriHandler處理。StartUriHandler會從路由請求UriRequest中獲取uri等引數,直接通過intent跳轉。

3.2.2,UriRequest及其子類

UriRequest中包含Context、URI和Fields,其中Fields為HashMap<String, Object>,可以通過Key存放任意資料。簡單起見,UriRequest類同時承擔了Response的功能,跳轉請求的結果,也會被儲存到Fields中。

  • Intent的Extra引數,Bundle型別
  • 用於startActivityForResult的RequestCode,int型別
  • 用於overridePendingTransition方法的頁面切換動畫資源,int[]型別
  • 本次跳轉結果的監聽器,OnCompleteListener型別

每次URI跳轉請求會有一個ResultCode(類似HTTP請求的ResponseCode),表示跳轉結果,也存放在Fields中。常見Code如下,使用者也可以自定義Code,為了避免衝突,自定義Code應使用負數值

  • 200:跳轉成功
  • 301:重定向到其他URI,會再次跳轉
  • 400:請求錯誤,通常是Context或URI為空
  • 403:禁止跳轉,例如跳轉白名單以外的HTTP連結、Activity的exported為false等
  • 404:找不到目標(Activity或UriHandler)
  • 500:發生錯誤

總結來說,UriRequest用於實現一次URI跳轉中所有元件之間的通訊功能。SDK預設提供了DefaultUriRequest,一般用其就可以完成日常需求。

3.2.3,AnnotationInit及其子類

WMRouter使用和原始碼分析
AnnotationInit的作用是,提供統一的,呼叫生成的輔助類的init方法。方便註冊路由表。DefaultAnnotationLoader的load方法,通過ServiceLoader獲取這些AnnotationInit的所有實現,然後呼叫其init方法。

public class UriAnnotationInit_179aab35b2125f96c7b066a0a2eccf82 implements IUriAnnotationInit {
  public void init(UriAnnotationHandler handler) {
    handler.register("", "", "/service_loader", "com.sankuai.waimai.router.demo.lib2.advanced.ServiceLoaderActivity", false);
    handler.register("", "", "/lib2", "com.sankuai.waimai.router.demo.lib2.basic.DemoLibActivity2", false);
  }
}
複製程式碼
/**
 * 使用ServiceLoader載入註解配置
 *
 * Created by jzj on 2018/4/28.
 */
public class DefaultAnnotationLoader implements AnnotationLoader {
    public static final AnnotationLoader INSTANCE = new DefaultAnnotationLoader();
    @Override
    public <T extends UriHandler> void load(T handler,
            Class<? extends AnnotationInit<T>> initClass) {
        List<? extends AnnotationInit<T>> services = Router.getAllServices(initClass);
        for (AnnotationInit<T> service : services) {
            service.init(handler);
        }
    }
}
複製程式碼

3.2.4,ServiceLoader

上面說過,ServiceLoader的原理是首先通過介面名獲取ServiceLoader例項,然後再從ServiceLoader例項中,根據key找到對應的介面實現類。那麼具體過程呢?

儲存對映關係
  1. 呼叫put方法(見下圖),引數有:介面的Class物件,介面實現類中的key,介面實現類的Class物件,以及介面實現類是否要求單例。在put方法中,呼叫SERVICES.put(interfaceClass, loader),存入介面Class物件和ServiceLoader例項的關係。然後呼叫loader.putImpl(key, implementClass, singleton)
  2. 在ServiceLoader例項的putImpl方法中,呼叫mMap.put(key, new ServiceImpl(key, implementClass, singleton)),在ServiceLoader例項中存入key和介面例項class物件的關係。注意這裡建立了一個ServiceImpl物件。那麼ServiceImpl是幹什麼的呢?
    WMRouter使用和原始碼分析
獲取對映關係
  1. 通過getService等方法獲取介面的例項。該方法就一行程式碼ServiceLoader.load(clazz).get(key)
  2. load方法中,首先呼叫sInitHelper.ensureInit()。通過反射,呼叫com.sankuai.waimai.router.generated.ServiceLoaderInit類的init方法(ServiceLoaderInit類是通過gradle外掛生成的)。在init方法中呼叫各個ServiceInit_36ed390bf4b81a8381d45028b37cc645init方法(見下圖)。那麼ServiceLoaderInit類中的init方法中的這些ServiceInit_36ed390bf4b81a8381d45028b37cc645類是什麼呢?這些類中的init方法裡面又是什麼呢?
  3. 然後繼續執行load方法,通過SERVICES.get(interfaceClass)獲取介面對應的ServiceLoader例項。

WMRouter使用和原始碼分析

WMRouter使用和原始碼分析

public class ServiceLoader<I> {
    //...省略部分程式碼
    //儲存了介面類名和其對應的ServiceLoader例項的對應關係。
    private static final Map<Class, ServiceLoader> SERVICES = new HashMap<>();
    //儲存了key和介面的實現類的對應關係。
    private HashMap<String, ServiceImpl> mMap = new HashMap<>();
    
    private static final LazyInitHelper sInitHelper = new LazyInitHelper("ServiceLoader") {
        @Override
        protected void doInit() {
            try {
                // 反射呼叫Init類,避免引用的類過多,導致main dex capacity exceeded問題
                Class.forName(Const.SERVICE_LOADER_INIT)
                        .getMethod(Const.INIT_METHOD)
                        .invoke(null);
                Debugger.i("[ServiceLoader] init class invoked");
            } catch (Exception e) {
                Debugger.fatal(e);
            }
        }
    };
    /**
     * 提供給InitClass使用的初始化介面
     *
     * @param interfaceClass 介面類
     * @param implementClass 實現類
     */
    public static void put(Class interfaceClass, String key, Class implementClass, boolean singleton) {
        ServiceLoader loader = SERVICES.get(interfaceClass);
        if (loader == null) {
            loader = new ServiceLoader(interfaceClass);
            SERVICES.put(interfaceClass, loader);
        }
        loader.putImpl(key, implementClass, singleton);
    }
    private void putImpl(String key, Class implementClass, boolean singleton) {
        if (key != null && implementClass != null) {
            mMap.put(key, new ServiceImpl(key, implementClass, singleton));
        }
    }
    /**
     * 根據介面獲取 {@link ServiceLoader}
     */
    @SuppressWarnings("unchecked")
    public static <T> ServiceLoader<T> load(Class<T> interfaceClass) {
        sInitHelper.ensureInit();
        if (interfaceClass == null) {
            Debugger.fatal(new NullPointerException("ServiceLoader.load的class引數不應為空"));
            return EmptyServiceLoader.INSTANCE;
        }
        ServiceLoader service = SERVICES.get(interfaceClass);
        if (service == null) {
            synchronized (SERVICES) {
                service = SERVICES.get(interfaceClass);
                if (service == null) {
                    service = new ServiceLoader(interfaceClass);
                    SERVICES.put(interfaceClass, service);
                }
            }
        }
        return service;
    }
    /**
     * 建立指定key的實現類例項,使用 {@link RouterProvider} 方法或無引數構造。對於宣告瞭singleton的實現類,不會重複建立例項。
     *
     * @return 找不到或獲取、構造失敗,則返回null
     */
    public static <I, T extends I> T getService(Class<I> clazz, String key) {
        return ServiceLoader.load(clazz).get(key);
    }
    /**
     * 建立指定key的實現類例項,使用 {@link RouterProvider} 方法或無引數構造。對於宣告瞭singleton的實現類,不會重複建立例項。
     *
     * @return 可能返回null
     */
    public <T extends I> T get(String key) {
        return createInstance(mMap.get(key), null);
    }
}
複製程式碼

3.3,用到的設計模式

3.3.1,外觀模式

外觀模式提供一個統一的介面,用來訪問子系統中的一群介面,外觀定義了一個高層介面,讓子系統更容易使用。

WMRouter使用和原始碼分析
com.sankuai.waimai.router.Router這個類就使用了外觀模式。其lazyInit(),startUri(UriRequest request),getService(Class<I> clazz, String key)等方法,都是通過呼叫SDK內部的其他子系統提供的功能實現的。

public class Router {
    /**
     * 此初始化方法的呼叫不是必須的。
     * 使用時會按需初始化;但也可以提前呼叫並初始化,使用時會等待初始化完成。
     * 本方法執行緒安全。
     */
    public static void lazyInit() {
        ServiceLoader.lazyInit();
        getRootHandler().lazyInit();
    }
    public static void startUri(UriRequest request) {
        getRootHandler().startUri(request);
    }
     /**
     * 建立指定key的實現類例項,使用 {@link RouterProvider} 方法或無引數構造。對於宣告瞭singleton的實現類,不會重複建立例項。
     * @return 找不到或獲取、構造失敗,則返回null
     */
    public static <I, T extends I> T getService(Class<I> clazz, String key) {
        return ServiceLoader.load(clazz).get(key);
    }
    ...
}
複製程式碼

3.3.2,單例模式

單例模式:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點

WMRouter使用和原始碼分析
單例模式常見的有七種寫法,WMRouter用到的有以下幾種:

餓漢式

public class DefaultActivityLauncher implements ActivityLauncher {
    public static final DefaultActivityLauncher INSTANCE = new DefaultActivityLauncher();
}
複製程式碼

雙重校驗鎖

LazyInitHelper的performInit()方法,用到了雙重校驗鎖的思想。保證只初始化一遍。

public abstract class LazyInitHelper {
    private void performInit() {
        if (!mHasInit) {
            synchronized (this) {
                if (!mHasInit) {
                    mHasInit = true;
                    long ts = 0;
                    final boolean enableLog = Debugger.isEnableLog();
                    if (enableLog) {
                        ts = SystemClock.uptimeMillis();
                    }
                    try {
                        doInit();
                    } catch (Throwable t) {
                        Debugger.fatal(t);
                    }
                    if (enableLog) {
                        Debugger.i("%s init cost %s ms", mTag,
                                SystemClock.uptimeMillis() - ts);
                    }
                }
            }
        }
    }
}
複製程式碼

使用容器實現單例模式

/**
 * 單例快取
 *
 * Created by jzj on 2018/3/29.
 */
public class SingletonPool {

    private static final Map<Class, Object> CACHE = new HashMap<>();

    @SuppressWarnings("unchecked")
    public static <I, T extends I> T get(Class<I> clazz, IFactory factory) throws Exception {
        if (clazz == null) {
            return null;
        }
        if (factory == null) {
            factory = RouterComponents.getDefaultFactory();
        }
        Object instance = getInstance(clazz, factory);
        Debugger.i("[SingletonPool]   get instance of class = %s, result = %s", clazz, instance);
        return (T) instance;
    }

    @NonNull
    private static Object getInstance(@NonNull Class clazz, @NonNull IFactory factory) throws Exception {
        Object t = CACHE.get(clazz);
        if (t != null) {
            return t;
        } else {
            synchronized (CACHE) {
                t = CACHE.get(clazz);
                if (t == null) {
                    Debugger.i("[SingletonPool] >>> create instance: %s", clazz);
                    t = factory.create(clazz);
                    //noinspection ConstantConditions
                    if (t != null) {
                        CACHE.put(clazz, t);
                    }
                }
            }
            return t;
        }
    }
}
複製程式碼

3.3.3,工廠方法模式

工廠方法模式(Factory Method Pattern),在實際開發過程中我們都習慣於直接使用 new 關鍵字用來建立一個物件,可是有時候物件的創造需要一系列的步驟:你可能需要計算或取得物件的初始設定;選擇生成哪個子物件例項;或在生成你需要的物件之前必須先生成一些輔助功能的物件,這個時候就需要了解該物件建立的細節,也就是說使用的地方與該物件的實現耦合在了一起,不利於擴充套件,為了解決這個問題就需要用到我們的工廠方法模式,它適合那些建立複雜的物件的場景,工廠方法模式也是一個使用頻率很高的設計模式

WMRouter使用和原始碼分析
WMRouter中,IFactory是抽象工廠。工廠方法是create()方法,生產的產品是泛型T。ContextFactory是具體工廠,其create方法,通過獲取包含context的建構函式,建立T的例項。EmptyArgsFactory是另外一個具體工廠,其create方法,通過clazz.newInstance()無參建構函式建立例項。

/**
 * 從Class構造例項
 */
public interface IFactory {
    @NonNull
    <T> T create(@NonNull Class<T> clazz) throws Exception;
}

複製程式碼
public class ContextFactory implements IFactory {
    private final Context mContext;
    public ContextFactory(Context context) {
        mContext = context;
    }
    @Override
    public <T> T create(@NonNull Class<T> clazz) throws Exception {
        return clazz.getConstructor(Context.class).newInstance(mContext);
    }
}
複製程式碼
public class EmptyArgsFactory implements IFactory {
    public static final EmptyArgsFactory INSTANCE = new EmptyArgsFactory();
    private EmptyArgsFactory() {
    }
    @Override
    public <T> T create(@NonNull Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }
}
複製程式碼

通過工廠方法獲取例項

public class ServiceLoader<I> {
    private <T extends I> T createInstance(@Nullable ServiceImpl impl, @Nullable IFactory factory) {
        //...省略部分程式碼
        Class<T> clazz = (Class<T>) impl.getImplementationClazz();
        //通過工廠方法獲取例項
        T t = factory.create(clazz);
        Debugger.i("[ServiceLoader] create instance: %s, result = %s", clazz, t);
        return t;
    }
}
複製程式碼

3.3.4,責任鏈模式

責任鏈模式是一種物件的行為模式。在責任鏈模式裡,很多物件由每一個物件對其下家的引用而連線起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個物件決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個物件最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。

WMRouter使用和原始碼分析
UML圖中的succeesor表示的是責任鏈中的下一個Handler

WMRouter路由分發時序圖

WMRouter使用和原始碼分析

  • 上圖中的ChainedHandler實際上是DefaultRootUriHandler,因為DefaultRootUriHandlerChainedHandler的子類,而且沒有實現handleInternal方法。所以呼叫的是ChainedHandler的handleInternal方法,最終呼叫的是next(Iterator<UriHandler> iterator,UriRequest request,UriCallback callback)方法。而該方法中,在某個節點的onNext回撥裡面又遞迴呼叫了其自己,這樣就建立了鏈式關係。
  • WMRouter中,UriHandler的若干子類構成了一個責任鏈,UriInterceptor的若干子類構成了另一個責任鏈。
public class ChainedHandler extends UriHandler {
    @Override
    protected void handleInternal(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
        next(mHandlers.iterator(), request, callback);
    }

    private void next(@NonNull final Iterator<UriHandler> iterator, @NonNull final UriRequest request,
                      @NonNull final UriCallback callback) {
        if (iterator.hasNext()) {
            UriHandler t = iterator.next();
            t.handle(request, new UriCallback() {
                @Override
                public void onNext() {
                    next(iterator, request, callback);
                }

                @Override
                public void onComplete(int resultCode) {
                    callback.onComplete(resultCode);
                }
            });
        } else {
            callback.onNext();
        }
    }
}
複製程式碼

四,為什麼要這麼設計?

寫到這裡,有點心虛,畢竟不是自己寫的框架,也不知道當時別人開發的時候的思路。只能說大體上猜測一下。

  • 首先要結合自己的業務背景和需求,看看有哪些問題需要解決?
  • 檢視現在有哪些比較成熟的輪子,能不能解決自己的問題,有沒有什麼坑?
    • github上的開源框架
    • 很多大廠,有自己的基礎技術部門,有很多內部的工具框架。
  • 針對自己的特殊業務需求,需求的緊急和重要程度,以及人力和排期,怎麼解決這些問題?
    • 資源緊張,需求等級不高,就直接用現有的輪子
    • 如果資源充足,而且現有輪子不能夠很好滿足需求,可以自己研究一下實現原理,根據自己的需求造一個新輪子

關於這個框架誕生的需求背景,感覺美團外賣Android平臺化架構演進實踐WMRouter:美團外賣Android開源路由框架說的很詳細,大家可以學習一下,在遇到問題的時候怎麼選擇解決方案,怎樣設計架構。

關於通用的路由需求,感覺這篇文章說的挺好,大家可以學習一下Android 元件化 —— 路由設計最佳實踐

五,擴充學習

  • WMRouter,美團外賣團隊出品,目前已有1.3K-star
  • ARouter,阿里,目前已有9.5K-star
  • Andromeda,愛奇藝,目前已有1.8K-star
  • ActivityRouter,個人專案,目前已有2.7K-star
  • Router,個人專案,目前已有1.3K-star

參考文章:

WMRouter:美團外賣Android開源路由框架

github.com/meituan/WMR…

WMRouter設計與使用文件

WMRouter原始碼分析(1)-基本結構分析

Android 元件化 —— 路由設計最佳實踐

一文了解Android中路由(Router)的實現

美團外賣開源路由框架 WMRouter 原始碼分析

生成帶混淆配置的aar庫

寫給 Android 開發者的混淆使用手冊

Android混淆——瞭解這些就夠了

細說反射,Java 和 Android 開發者必須跨越的坎

java/android 設計模式學習筆記目錄

相關文章