一個Android路由框架的誕生之路

頭條祁同偉發表於2018-05-15

經過前面三篇文章,相信大家對元件化都有了一定程度的理解。

在這個過程中一直強調了元件化的一個基礎設施:路由!沒有它元件化可以說是寸步難行,本篇文章我們就來談談一個元件化路由框架誕生過程中的那些思考

本文概述

1、為什麼需要路由

這個問題其實我們之前談到過,而且有過元件化實踐或者嘗試的同學一定有切身感受。明確一個前提:各個業務模組之間不會是相互隔離而是必然存在一些互動的;

  • 在Module A需要跳轉到Module B某介面,而我們一般都是使用強引用的Class顯式的呼叫;
  • 在Module A需要呼叫Module B提供的方法,例如別的Module呼叫使用者模組退出登入的方法;

這兩種呼叫形式大家很容易明白,正常開發中大家也是毫不猶豫的呼叫。但是我們在元件化開發的時候卻有很大的問題:

  • 模組B的Activity Class在自己的Module B,那Module A必然引用不到,顯式跳轉行不通;
  • 同理,直接去呼叫某個Module的方法也行不通;

由此:必然需要一種支援元件化需求的互動方式,提供UI跳轉以及方法呼叫的能力。

2、一個路由庫需要滿足什麼

首先這個路由庫也是一個技術元件,在整體元件化層次的設計中處於Lib層,作為一項基礎庫。那麼路由庫不僅僅需要滿足自身的能力,同時勢必要滿足一項基礎庫該有的條件:

  • Api友好,接入簡單、低成本
  • 具備UI跳轉和方法呼叫的能力
  • 功能穩定
  • 可定製化

3、淘汰過的方式

任何系統或框架,雖然在高版本中看起來都很完美,但是實際上一開始並非就是如此,都是一步步實踐、迭代改善到基於當前相對完美狀態的。比如我們之前就思考過如下方式:

3.1、基於隱式意圖

各位老司機都知道,Android中開啟一個Activity,可以有兩種方式,顯示意圖和隱式意圖。既然顯式意圖導致了強引用,那麼我們使用隱式意圖,既可以開啟Activity,同時也不會造成Module間的強引用。

評價:這種方式確實可以完成路由的UI跳轉功能,但是依賴於Manifest檔案的修改,同時引數也存在不便傳遞的問題,因此不做推薦。

3.2、基於事件,使用廣播或EventBus

這種思路也很容易想到,既然不能直接互動,那麼就隱式的來,在需要互動的地方發通知,然後接收方根據不同的通知型別做出不同的處理。

    /**
     * EventBus的事件類
     */
    public class InteractEvent {
        public int type;
        public String param;// eg:String型別引數一
        public ParamObject paramObject;// eg:Object型別引數二
    }

    /**
     * 處理不同的互動設定
     * @param event
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(InteractEvent event) {
        if(event.type == EventType.JUMP_LOGINACTIVITY){
            Intent intent = new Intent(mContext,LoginActivity.class);
            intent.putExtra("param",event.param);
            intent.putExtra("paramObject",event.paramObject);
            mContext.startActivity(intent);
        }
    }
複製程式碼

評價:這種互動的方式是可行的,但是可以明顯看出,比較複雜,對於介面跳轉比較多的場景,接入及維護成本較高。

3.3、呼叫一個固定的方法

我們在需要互動的類中加上方法,方法簽名固定,然後給互動類打上一個標籤。這樣在別的元件中我們需要這個互動類的時候通過標籤拿到,呼叫這個固定的方法。思路是這樣,以下提供一種方式的虛擬碼,有不同的實現。

    public interface IDoAction{
        void doAction(HashMap hashMap);
    }
    
    public class LoginActivity extends Activity implements IDoAction{
        public void doAction(HashMap hashMap){
            // 1.獲取引數、Type
            hashMap.get();
            // 2.跳轉
        }
    }
    
    // 呼叫
    Dispatcher.get(url).doAction(hashMap);
複製程式碼

備註使用HashMap作為引數的原因:每個互動類需要的引數不一樣,而方法簽名必須固定才能通過介面去呼叫,傳遞HashMap這個引數可以包含多個不同型別的引數。

評價:最不推薦,使用繁瑣,侵入性太強,改造成本極大。

4、一種好的路由實踐

總結以上幾種不好的實踐方式,都在於侵入性強,接入及維護成本高等。那反過來推就是一個好的路由需要具備低侵入性、易接入、自動化等特性。 上述第三種方案我們可以吸收一點的是每個互動類打上一個標籤,記錄這個對映關係,方便在別的Module進行獲取。

eg: easyrouter://main/Detail    ————》 MainDetailActivity
複製程式碼

順著這個對映往下想,這個對映儲存了標籤和Activity,那麼開啟Activity只需要知道這個標籤即可。舉例:標籤A和ActivityA對應,那麼我們只要遇到標籤A就知道它想要開啟的是ActivityA。同時如果我們處理好了開啟Activity需要的傳參問題就離自動化邁出了一大步。

問題就簡化成了兩個:

  1. 對映關係,我們可以使用String字串作為標籤,既保證通用性又可以保證唯一性。利用一個Map儲存這個字串和Activity的對映關係,這樣可以保證在別的Module能通過字串獲取到我們需要的Activity
  2. 傳參以及Activity各種特性(利用動畫、onActivityResult生命週期)的支援

關於第二個問題實際上就是將這個字串儘可能多的解析到Android多需要的資料,比如引數傳遞、動畫、生命週期等。關於這個解析可以有兩種方式:

  1. 直接簡單粗暴在String後面拼一個引數,這個引數的格式是Json,到達目標介面之後目標介面再去解析;
  2. 制定一定規則通過路由就解決好,到目標介面直接像正常Android開發一樣去獲取;
eg:     easyrouter://routertest?name=liuzhao&company=mycompany
複製程式碼

5、方法呼叫的實現

方法呼叫看起來都可以通過上述:基於事件及呼叫一個固定的方法等方式來實現,但是使用起來必定複雜無比,各個Module之間互動不僅改造困難,維護成本也很高。

注意各個Module向外提供的方法必定不一樣:需要不同的方法簽名。而且從改造及維護成本考慮,最好可以像是在一個Module裡一樣直接呼叫,IDE可以自動提示出來方法引數。

那我們就想把Module向外提供的方法內聚到一個類裡,只向外暴露這個類,簡稱這個Module的互動服務類。這樣別的Module呼叫的時候就可以想直接呼叫普通類的方法一樣簡單方便了。

那我們就剩下一個問題:別的Module如何獲取你的互動服務類呢?很容易想到上面提到的對映,但是此種場景下如果使用字串做Key真的可以嗎? 如果使用字串做Key,別的Module拿到的Value只能確定是一個Class,具體的Class型別卻不清楚,呼叫具體的方法尤其是被IDE提示,更是不可能。問題又簡化成了如何讓我們知道拿到的Class中有哪些方法呢?

經過多次思考,終於想到了一個解決方案:Module需要向外暴露的方法,我們通過一個Interface來定義,這個Interface定義在Lib層也就是說每個Module都可以訪問到,而儲存對映關係的Key我們也使用這個Interface。那麼對映表裡儲存的就是:

private static Map<Module暴露介面Interface, Module暴露介面的實現類InterfaceImpl> moduleInteracts = new HashMap<>();
複製程式碼

那麼別的Module在獲取這個服務類時就可以直接通過在Lib層定義的Interface來獲取,然後通過泛型轉換成這個介面,而後直接呼叫相應方法即可,就像呼叫一個普通方法一樣簡單

public static <T extends IBaseModuleService> T getModuleService(Class<T> tClass) {
    if (moduleInteracts.containsKey(tClass)) {
        return (T) moduleInteracts.get(tClass);
    }
    return null;
}

呼叫:EasyRouter.getModuleService(BaseModuleService.ModuleInteractService.class).runModuleInteract(MainActivity.this);
               
複製程式碼

6、路由的最佳實踐

6.1、編譯時註解

經過四、五兩節我們知道了路由相對較好的實踐,但同時我們能否讓這個過程自動化呢?其實可以藉助編譯時註解技術自動生成對映表,這樣在接入的時候就更加簡單方便,只需要在對應的Activity上打上一個註解,配置相應的字串,這個對映表就自動生成。

@DisPatcher("easyrouter://routertest")
public class MainActivity extends Activity {
    ......
}

生成程式碼

@Override
public void initActivityMap(HashMap<String, Class> activityMap) {
    activityMap.put("easyrouter://routertest", MainActivity.class);
}

複製程式碼

6.2、攔截器

6.2.1、統一判斷

在實際開發中,我們經常會遇到些統一的操作,比如某些應用是需要使用者先登陸的,那麼在使用者瀏覽之後的下一步操作時使用者進行各種點選都需要進行判斷是否登陸,未登入則跳轉到登陸介面,登陸之後則放行。

正常情況我們需要在每一個點選的地方進行判斷,但是明顯費時費力,既然我們已經做了路由,所有的介面跳轉都需要經過我們,那我們自然可以進行統一的判斷,在路由進行分發時候進行判斷,滿足攔截器條件則進行攔截。

一個Android路由框架的誕生之路

6.2.2、重定向

如果我們需要對App的功能進行A/BTest,我們該如何進行呢?方式肯定有很多,但是不一定通用。注意我們已經有了路由,結合路由來做A/BTest的話更加方便:

  • 首先我們給路由加一個攔截器,每一條跳轉都會經過這個攔截器的判斷;
  • 通過路由實現介面跳轉,在路由解析過程中我們識別到了需要跳轉的是A模組;
  • 經過攔截器的判斷,如果A/BTest實驗命中的是B模組,則將這個路由進行重定向到B模組;

備註:重定向的好處還有更多,比如緊急情況下的熱修復替換成H5介面等。

6.3、過程監聽

就是監聽開啟Activity的過程,如

  • 開啟前進行資料的準備;
  • 開啟後的回撥;
  • 未匹配到目標Activity的降級等;

本文主要介紹一個Android路由框架誕生過程中的思考,在下篇文章將會具體推薦一個路由框架。

廣告時間

今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都需要,業務增長快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、非頻繁跳槽(如兩年兩跳),歡迎加我的微信詳聊:KOBE8242011

歡迎關注

相關文章