1. 程式人生 > >我所理解的Android元件化之通訊機制

我所理解的Android元件化之通訊機制

之前寫過一篇關於Android元件化的文章,《Android元件化框架設計與實踐》,之前沒看過的小夥伴可以先點選閱讀。那篇文章是從實戰中進行總結得來,是公司的一個真實專案進行元件化架構改造,粒度會分的更粗些,是對整體架構實踐進行相應的總結,裡面說了要打造一個元件化框架的話,需要從以下7個方面入手:

  1. 程式碼解耦。如何將一個龐大的工程分成有機的整體?
  2. 元件單獨執行。因為每個元件都是高度內聚的,是一個完整的整體,如何讓其單獨執行和除錯?
  3. 元件間通訊。由於每個元件具體實現細節都互相不瞭解,但每個元件都需要給其他呼叫方提供服務,那麼主專案與元件、元件與元件之間如何通訊就變成關鍵?
  4. UI 跳轉。UI 跳轉指的是特殊的資料傳遞,跟元件間通訊區別有什麼不同?
  5. 元件生命週期。這裡的生命週期指的是元件在應用中存在的時間,元件是否可以做到按需、動態使用、因此就會涉及到元件載入、解除安裝等管理問題。
  6. 整合除錯。在開發階段如何做到按需編譯元件?一次除錯中可能有一兩個元件參與整合,這樣編譯時間就會大大降低,提高開發效率。
  7. 程式碼隔離。元件之間的互動如果還是直接引用的話,那麼元件之間根本沒有做到解耦,如何從根本上避免元件之間的直接引用,也就是如何從根本上杜絕耦合的產生?

今天則會從更小細粒度入手,主要講講在元件化架構下元件與元件之間通訊機制是如何、包括所謂的UI跳轉,其實也是元件化通訊,只不過它稍微特殊點,單獨抽取出來而已。學習知識的過程很常見的一個思路就是從整體概況入手,首先對整體有個粗略的印象,然後再深入細節,抽絲剝繭般去挖掘其中的內在原理,一個點一個不斷去突破,這樣就能建立起自己整個知識樹,所以今天我們就從通訊機制這個點入手,看看其中內在玄機有哪些。

思維導圖

同樣,在每寫一篇文章之前,放個思維導圖,這樣做的好處對於想寫的內容有很好的梳理,邏輯和結構上顯得清晰點。

思維導圖

主流方式

總所周知,Android提供了很多不同的資訊的傳遞方式,比如在四大元件中本地廣播、程序間的AIDL、匿名間的記憶體共享、Intent Bundle傳遞等等,那麼在這麼多傳遞方式,哪種型別是比較適合元件與元件直接的傳遞呢。

  • 本地廣播,也就是LoacalBroadcastRecevier。更多是用在同一個應用內的不同系統規定的元件進行通訊,好處在於:傳送的廣播只會在自己的APP內傳播,不會洩漏給其他的APP,其他APP無法向自己的APP傳送廣播,不用被其他APP干擾。本地廣播好比對講通訊,成本低,效率高,但有個缺點就是兩者通訊機制全部委託與系統負責,我們無法干預傳輸途中的任何步驟,不可控制,一般在元件化通訊過程中採用比例不高。
  • 程序間的AIDL。這個粒度在於程序,而我們元件化通訊過程往往是線上程中,況且AIDL通訊也是屬於系統級通訊,底層以Binder機制,雖說Android提供模板供我們實現,但往往使用者不好理解,互動比較複雜,往往也不適用應用於元件化通訊過程中。
  • 匿名的記憶體共享。比如用Sharedpreferences,在處於多執行緒場景下,往往會執行緒不安全,這種更多是儲存一一些變化很少的資訊,比如說元件裡的配置資訊等等。
  • Intent Bundle傳遞。包括顯性和隱性傳遞,顯性傳遞需要明確包名路徑,元件與元件往往是需要互相依賴,這背離元件化中SOP(關注點分離原則),如果走隱性的話,不僅包名路徑不能重複,需要定義一套規則,只有一個包名路徑出錯,排查起來也稍顯麻煩,這個方式往往在元件間內部傳遞會比較合適,元件外與其他元件打交道則使用場景不多。

說了這麼多,那元件化通訊什麼機制比較適合呢?既然元件層中的模組是相互獨立的,它們之間並不存在任何依賴。沒有依賴就無法產生關係,沒有關係,就無法傳遞訊息,那要如何才能完成這種交流?

目前主流做法之一就是引入第三者,比如圖中的Base Module。

基礎元件化架構

元件層的模組都依賴於基礎層,從而產生第三者聯絡,這種第三者聯絡最終會編譯在APP Module中,那時將不會有這種隔閡,那麼其中的Base Module就是跨越元件化層級的關鍵,也是模組間資訊交流的基礎。比較有代表性的元件化開源框架有得到DDComponentForAndroid阿里Arouter聚美Router 等等。

除了這種以通過引入第三者方式,還有一種解決方式是以事件匯流排方式,但這種方式目前開源的框架中使用比例不高,如圖:

事件匯流排

事件匯流排通過記錄物件,使用監聽者模式來通知物件各種事件,比如在現實生活中,我們要去找房子,一般都去看小區的公告欄,因為那邊會經常釋出一些出租資訊,我們去檢視的過程中就形成了訂閱的關係,只不過這種是被動去訂閱,因為只有自己需要找房子了才去看,平時一般不會去看。小區中的公告欄可以想象成一個事件匯流排釋出點,監聽者則是哪些想要找房子的人,當有房東在公告欄上貼上出租房資訊時,如果公告欄有訂閱資訊功能,比如引入門衛保安,已經把之前來這個公告欄要檢視的找房子人一一進行電話登記,那麼一旦有新出租訊息產生,則門衛會把這條訊息一一進行簡訊群發,那麼找房子人則會收到這條訊息進行後續的操作,是馬上過來看,還是延遲過來,則根據自己的實際情況進行處理。在目前開源庫中,有EventBus、RxBus就是採用這種釋出/訂閱模式,優點是簡化了Android元件之間的通訊方式,實現解耦,讓業務程式碼更加簡潔,可以動態設定事件處理執行緒和優先順序,缺點則是每個事件需要維護一個事件類,造成事件類太多,無形中加大了維護成本。那麼在元件化開源框架中有ModuleBusCC 等等。

實現方案

事件匯流排,又可以叫做元件匯流排,路由+介面,則相對好理解點,今天從閱讀它們框架原始碼,我們來對比這兩種實現方案的不同之處。

元件匯流排

這邊選取的是ModuleBus框架,這個方案特別之處在於其借鑑了EventBus的思想,元件的註冊/登出和元件呼叫的事件傳送都跟EventBus類似,能夠傳遞一些基礎型別的資料,而並不需要在Base Moudel中新增額外的類。所以不會影響Base模組的架構,但是無法動態移除資訊接收端的程式碼,而自定義的事件資訊型別還是需要新增到Base Module中才能讓其他功能模組索引。

其中的核心程式碼是在與 ModuleBus 類,其內部維護了兩個ArrayMap鍵對值列表,如下:

/**
     * Object methodClass
     * String methodName;
     * MethodInfo method info
     */
    private static ArrayMap<Object,ArrayMap<String,MethodInfo>> moduleEventMethods = new ArrayMap<>();

    /**
     * Class IBaseClient.class
     * String methodName
     * Object methodClass
     */
    private static ArrayMap<Class<?>,ArrayMap<String,ArrayList<Object>>> moduleMethodClient = new ArrayMap<>();

在使用方法上,在onCreate()和onDestroy()中需要註冊和解綁,比如

ModuleBus.getInstance().register(this);
ModuleBus.getInstance().unregister(this);

最終使用類似EventBus 中 post 方法一樣,進行兩個元件間的通訊。這個框架的封裝的post 方法如下

public void post(Class<?> clientClass,String methodName,Object...args){
        if(clientClass == null || methodName == null ||methodName.length() == 0) return;

        ArrayList<Object> clientList = getClient(clientClass,methodName);

        if(clientList == null) return;

        try{
            for(Object c: clientList){
                try{
                    ArrayMap<String,MethodInfo> methods = moduleEventMethods.get(c);
                    Method method = methods.get(methodName).m;
                    if(method == null){
                        Log.e(TAG,"cannot find client method"+methodName +"for args["+args.length+"]" + Arrays.toString(args));
                        return;
                    }else if(method.getParameterTypes() == null){
                        Log.e(TAG,"cannot find client method param:"+method.getParameterTypes() +"for args["+args.length+"]" + Arrays.toString(args));
                        return;
                    }else if(method.getParameterTypes().length != args.length){
                        Log.e(TAG,"method "+methodName +" param number not matched:method("+method.getParameterTypes().length+"), args(" + args.length+")");
                        return;
                    }
                    method.invoke(c,args);
                }catch (Throwable e){
                    Log.e(TAG,"Notifiy client method invoke error.",e);
                }
            }

        }catch (Throwable e){
            Log.e(TAG,"Notify client error",e);
        }
    }

可以看到,它是通過遍歷之前內部的ArrayMap,把註冊在裡面的方法找出,根據傳入的引數進行匹配,使用反射呼叫。

介面+路由

介面+路由實現方式則相對容易理解點,我之前實踐的一個專案就是通過這種方式實現的。具體地址如下:DemoComponent 實現思路是專門抽取一個LibModule作為路由服務,每個元件宣告自己提供的服務 Service API,這些 Service 都是一些介面,元件負責將這些 Service 實現並註冊到一個統一的路由 Router 中去,如果要使用某個元件的功能,只需要向Router 請求這個 Service 的實現,具體的實現細節我們全然不關心,只要能返回我們需要的結果就可以了。

比如定義兩個路由地址,一個登陸元件,一個設定元件,核心程式碼:

public class RouterPath {

    //注意路由的命名,路徑第一個開頭需要不一致,保證唯一性
    //Login Service
    public static final String ROUTER_PATH_TO_LOGIN_SERVICE = "/login/service";

    //Setting Service
    public static final String ROUTER_PATH_TO_SETTING_SERVICE = "/setting/service";
}

那麼就相應著就有兩個介面API,如下:

public interface ILoginProvider extends IProvider {

    void goToLogin(Activity activity);
}

public interface ISettingProvider extends IProvider {
    
    void goToSetting(Activity activity);
}

這兩個介面API對應著是向外暴露這兩個元件的能提供的通訊能力,然後每個元件對介面進行實現,如下:

@Route(path = RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE, name = "登陸頁面")
public class LoginService implements ILoginProvider {
    @Override
    public void init(Context context) {}


    @Override
    public void goToLogin(Activity activity) {
        Intent loginIntent = new Intent(activity, LoginActivity.class);
        activity.startActivity(loginIntent);
    }
}

這其中使用的到了阿里的ARouter頁面跳轉方式,內部本質也是介面+實現方式進行元件間通訊。

呼叫則很簡單了,如下:

 ILoginProvider loginService = (ILoginProvider) ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).navigation();
if(loginService != null){
    loginService.goToLogin(MainActivity.this);
}

還有一個元件化框架,就是ModularizationArchitecture ,它本質實現方式也是介面+實現,但是封裝形式稍微不一樣點,它是每個功能模組中需要使用註解建立Action事件,每個Action完成一個事件動作。invoke只是方法名為反射,並未用到反射,而是使用介面方式呼叫,引數是通過HashMap<String,String>傳遞的,無法傳遞物件。具體詳解可以看這篇文章 Android架構思考(模組化、多程序)

頁面跳轉

頁面跳轉也算是一種元件間的通訊,只不過它相對粒度更細化點,之前我們描述的元件間通訊粒度會更抽象點,頁面跳轉則是定位到某個元件的某個頁面,可能是某個Activity,或者某個Fragment,要跳轉到另外一個元件的Activity或Fragment,是這兩者之間的通訊。甚至在一般沒有進行元件化架構的工程專案中,往往也會封裝頁面之間的跳轉程式碼類,往往也會有路由中心的概念。不過一般 UI 跳轉基本都會單獨處理,一般通過短鏈的方式來跳轉到具體的 Activity。每個元件可以註冊自己所能處理的短鏈的 Scheme 和 Host,並定義傳輸資料的格式,然後註冊到統一的 UIRouter 中,UIRouter 通過 Scheme 和 Host 的匹配關係負責分發路由。但目前比較主流的做法是通過在每個 Activity 上添加註解,然後通過 APT 形成具體的邏輯程式碼。

下面簡單介紹目前比較主流的兩個框架核心實現思路:

ARouter 核心實現思路是,我們在程式碼里加入的@Route註解,會在編譯時期通過apt生成一些儲存path和activityClass對映關係的類檔案,然後app程序啟動的時候會拿到這些類檔案,把儲存這些對映關係的資料讀到記憶體裡(儲存在map裡),然後在進行路由跳轉的時候,通過build()方法傳入要到達頁面的路由地址,ARouter會通過它自己儲存的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),然後new Intent(),當呼叫ARouter的withString()方法它的內部會呼叫intent.putExtra(String name, String value),呼叫navigation()方法,它的內部會呼叫startActivity(intent)進行跳轉,這樣便可以實現兩個相互沒有依賴的module順利的啟動對方的Activity了。

ActivityRouter 核心實現思路是,它是通過路由 + 靜態方法來實現,在靜態方法上加註解來暴露服務,但不支援返回值,且引數固定位(context, bundle),基於apt技術,通過註解方式來實現URL開啟Activity功能,並支援在WebView和外部瀏覽器使用,支援多級Activity跳轉,支援Bundle、Uri引數注入並轉換引數型別。它實現相對簡單點,也是比較早期比較流行的做法,不過學習它也是很有參考意義的。

小結

總的來說,元件間的通訊機制在元件化程式設計和元件化架構中是很重要的一個環節,可能在每個元件獨自開發階段,不需要與其他元件進行通訊,只需要在內部通訊即可,當處於元件整合階段,那就需要大量元件進行互相通訊,體現在每個業務互相協作,如果元件間設計的不好,開啟一個頁面或呼叫一個方法,想當耗時或響應慢,那麼體現的則是這個APP使用比較卡頓,僅僅開啟一個頁面就是需要好幾秒才能開啟,則嚴重影響使用者的體驗了,甚至一些大型APP,可能元件分化更小,種類更多,那麼元件間的通訊則至關重要了。所以,要打造一個良好的元件化框架,如何設計一個更適合自己本身的業務型別的通訊機制,就需要多多進行思考了。

參考文章:

1,https://github.com/luckybilly/AndroidComponentizeLibs

2,http://blog.spinytech.com/2016/12/28/android_modularization/