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

以前寫過一篇關於Android組件化的文章,《Android組件化框架設計與實踐》,以前沒看過的小夥伴能夠先點擊閱讀。那篇文章是從實戰中進行總結得來,是公司的一個真實項目進行組件化架構改造,粒度會分的更粗些,是對總體架構實踐進行相應的總結,裏面說了要打造一個組件化框架的話,須要從如下7個方面入手:java

  1. 代碼解耦。如何將一個龐大的工程分紅有機的總體?
  2. 組件單獨運行。由於每一個組件都是高度內聚的,是一個完整的總體,如何讓其單獨運行和調試?
  3. 組件間通訊。因爲每一個組件具體實現細節都互相不瞭解,但每一個組件都須要給其餘調用方提供服務,那麼主項目與組件、組件與組件之間如何通訊就變成關鍵?
  4. UI 跳轉。UI 跳轉指的是特殊的數據傳遞,跟組件間通訊區別有什麼不一樣?
  5. 組件生命週期。這裏的生命週期指的是組件在應用中存在的時間,組件是否能夠作到按需、動態使用、所以就會涉及到組件加載、卸載等管理問題。
  6. 集成調試。在開發階段如何作到按需編譯組件?一次調試中可能有一兩個組件參與集成,這樣編譯時間就會大大下降,提升開發效率。
  7. 代碼隔離。組件之間的交互若是仍是直接引用的話,那麼組件之間根本沒有作到解耦,如何從根本上避免組件之間的直接引用,也就是如何從根本上杜絕耦合的產生?

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

思惟導圖

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

思惟導圖

主流方式

總所周知,Android提供了不少不一樣的信息的傳遞方式,好比在四大組件中本地廣播、進程間的AIDL、匿名間的內存共享、Intent Bundle傳遞等等,那麼在這麼多傳遞方式,哪一種類型是比較適合組件與組件直接的傳遞呢。github

  • 本地廣播,也就是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 等等。架構

這二者模式更詳細的對比,能夠查看這篇文章多個維度對比一些有表明性的開源android組件化開發方案app

實現方案

事件總線,又能夠叫作組件總線,路由+接口,則相對好理解點,今天從閱讀它們框架源碼,咱們來對比這兩種實現方案的不一樣之處。

組件總線

這邊選取的是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

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

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/

相關文章
相關標籤/搜索