Andromeda:適用于多進程架構(gòu)的組件通信框架(上)

轉(zhuǎn)載自:https://mp.weixin.qq.com/s/7SfRnt_jJLU0nrHPeuwomg

引言

其實Android的組件化由來已久常侣,而且已經(jīng)有了一些不錯的方案庭瑰,特別是在頁面跳轉(zhuǎn)這方面堪置,比如阿里的ARouter, 天貓的統(tǒng)跳協(xié)議, Airbnb的DeepLinkDispatch, 借助注解來完成頁面的注冊蔗彤,從而很巧妙地實現(xiàn)了路由跳轉(zhuǎn)剖毯。

但是洲鸠,盡管像ARouter等方案其實也支持接口的路由堂淡,然而令人遺憾的是只支持單進程的接口路由

Andromeda的功能

  • 本地服務路由坛怪,注冊本地服務是registerLocalService(Class, Object), 獲取本地服務是getLocalService(Class);
  • 遠程服務路由淤齐,注冊遠程服務是registerRemoteService(Class, Object), 獲取遠程服務是getRemoteService(Class);
  • 全局(含所有進程)事件總線, 訂閱事件為subscribe(String, EventListener), 發(fā)布事件為publish(Event);
  • 遠程方法回調(diào),如果某個業(yè)務接口需要遠程回調(diào)袜匿,可以在定義aidl接口時使用IPCCallback;

注: 這里的服務不是Android中四大組件的Service,而是指提供的接口與實現(xiàn)更啄。為了表示區(qū)分,后面的服務均是這個含義居灯,而Service則是指Android中的組件祭务。

為什么需要區(qū)分本地服務和遠程服務
最重要的一個原因是本地服務的參數(shù)和返回值類型不受限制,而遠程服務則受binder通信的限制怪嫌。Andromeda的出現(xiàn)為組件化完成了最后一塊拼圖义锥。

Andromeda和其他組件間通信方案的對比如下:

易用性 IPC性能 支持IPC 支持跨進程事件總線 支持IPC Callback
Andromeda yes yes yes
DDComponentForAndroid 較差 - no no no
ModularizationArchitecture 較差 yes no no

接口依賴還是協(xié)議依賴

有人覺得使用Event或ModuleBean來作為組件間通信載體的話,就不用每個業(yè)務模塊定義自己的接口了岩灭,調(diào)用方式也很統(tǒng)一???
但是也有如下缺點:

  • 雖然不用定義接口了拌倍,但是為了適應各自的業(yè)務需求,如果使用Event的話噪径,需要定義許多Event; 如果使用ModuleBean的話柱恤,需要為每個ModuleBean定義許多字段,甚至于即使是讓另一方調(diào)用一個空方法找爱,也需要創(chuàng)建一個ModuleBean對象梗顺,這樣的消耗是很大的; 而且隨著業(yè)務增多,這個模塊對應的ModuleBean中需要定義的字段會越來越多车摄,消耗會越來越大
  • 代碼可讀性較差寺谤。定義Event/ModuleBean的方式不如接口調(diào)用那么直觀仑鸥,不利于項目的維護
  • 我們理解的協(xié)議通信,是指跨平臺/序列化的通信方式变屁,類似終端和服務器間的通信或restful這種⊙劭。現(xiàn)在這種形式在終端內(nèi)很常見了。協(xié)議通信具備一種很強力解耦能力敞贡,但是協(xié)議如果變化了泵琳,兩端怎么同步就變得有點復雜,至少要配合一些框架來實現(xiàn)誊役。在一個應用內(nèi)获列,這樣會不會有點復雜?協(xié)議通信用作組件間通信的話太重了蛔垢,從而導致它應對業(yè)務變化時不夠靈活击孩。

所以最終決定采用接口+數(shù)據(jù)結(jié)構(gòu)的方式進行組件間通信,對于需要暴露的業(yè)務接口和數(shù)據(jù)結(jié)構(gòu)鹏漆,放到一個公共的module中巩梢。

跨進程路由方案的實現(xiàn)

本地服務的路由就不說了,一個Map就可以搞定艺玲。
遠程服務括蝠,需要解決一下難題:

  • 讓任意兩個組件都能夠很方便地通信,即一個組件注冊了自己的遠程服務饭聚,任意一個組件都能輕易調(diào)用到
  • 讓遠程服務的注冊和使用像本地服務一樣簡單忌警,即要實現(xiàn)阻塞調(diào)用
  • 不能降低通信的效率

最終方案:

建立一個一個binder的管理器,如下圖:

核心流程.png

詳細分析如下:

這個架構(gòu)的核心就是Dispatcher和RemoteTransfer秒梳, Dispatcher負責管理所有進程的業(yè)務binder以及各進程中RemoteTransfer的binder; 而RemoteTransfer負責管理它所在進程所有Module的服務binder.

  • 每個進程有一個RemoteTransfer,它負責管理這個進程中所有Module的遠程服務法绵,包含遠程服務的注冊、注銷以及獲取酪碘,RemoteTransfer提供的遠程服務接口為:
interface IRemoteTransfer {
    oneway void registerDispatcher(IBinder dispatcherBinder);
   
    oneway void unregisterRemoteService(String serviceCanonicalName);

    oneway void notify(in Event event);}

這個接口是給binder管理者Dispatcher使用的朋譬,其中registerDispatcher()是Dispatcher將自己的binder反向注冊到RemoteTransfer中,之后RemoteTransfer就可以使用Dispatcher的代理進行服務的注冊和注銷了兴垦。

  • 在進程初始化時徙赢,RemoteTransfer將自己的信息(其實就是自身的binder)發(fā)送給與Dispatcher同進程的DispatcherService, DispatcherService收到之后通知Dispatcher, Dispatcher就通過RemoteTransfer的binder將自己反射注冊過去,這樣RemoteTransfer就獲取到了Dispatcher的代理探越。流程圖如下:


    調(diào)用流程簡圖.png

    這個注冊過程一般發(fā)生在子進程初始化的時候狡赐,但是其實即使在子進程初始化時沒有注冊也不要緊,其實是可以推遲到需要將自己的遠程服務提供出去扶关,或者需要獲取其他進程的Module的服務時再做這件事也可以阴汇。

遠程服務注冊流程簡圖:

遠程服務注冊流程簡圖.png

Dispatcher則持有所有進程的RemoteTransfer的代理binder, 以及所有提供服務的業(yè)務binder, Dispatcher提供的遠程服務接口是IDispatcher,其定義如下:

interface IDispatcher {

   BinderBean getTargetBinder(String serviceCanonicalName);
   
   IBinder fetchTargetBinder(String uri);

   void registerRemoteTransfer(int pid,IBinder remoteTransferBinder);

   void registerRemoteService(String serviceCanonicalName,String processName,IBinder binder);

   void unregisterRemoteService(String serviceCanonicalName);

   void publish(in Event event);}

同步獲取binder的問題

設想這樣一個場景:在Dispatcher反向注冊之前数冬,就有一個Module想要調(diào)用另外一個進程中的某個服務(這個服務已經(jīng)注冊到Dispatcher中), 那么此時如何同步獲取呢节槐?

其實是有辦法的搀庶,那就是通過ContentProvider!

利用ContentProviderClient直接獲取IBinder,其調(diào)用方式如下:

 public static Bundle call(Context context, Uri uri, String method, String arg, Bundle extras) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return context.getContentResolver().call(uri, method, arg, extras);
        }
        ContentProviderClient client = tryGetContentProviderClient(context, uri);
        Bundle result = null;
        if (null == client) {
            Logger.i("Attention!ContentProviderClient is null");
        }
        try {
            result = client.call(method, arg, extras);
        } catch (RemoteException ex) {
            ex.printStackTrace();
        } finally {
            releaseQuietly(client);
        }
        return result;
    }

    private static ContentProviderClient tryGetContentProviderClient(Context context, Uri uri) {
        int retry = 0;
        ContentProviderClient client = null;
        while (retry <= RETRY_COUNT) {
            SystemClock.sleep(100);
            retry++;
            client = getContentProviderClient(context, uri);
            if (client != null) {
                return client;
            }
            //SystemClock.sleep(100);
        }
        return client;
    }

    private static ContentProviderClient getContentProviderClient(Context context, Uri uri) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return context.getContentResolver().acquireUnstableContentProviderClient(uri);
        }
        return context.getContentResolver().acquireContentProviderClient(uri);
    }
}

可以在調(diào)用結(jié)果的Bundle中攜帶IBinder即可,但是這個方案的問題在于ContentProviderClient兼容性較差铜异,在有些手機上第一次運行時會crash哥倔,這樣顯然無法接受。另外一種方式則是借助ContentResolver的query()方法揍庄,將binder放在Cursor中咆蒿,DispatcherCursor的定義如下,其中蚂子,generateCursor()方法用于將binder放入Cursor中沃测,而stripBinder()方法則用于將binder從Cursor中取出。

public class DispatcherCursor extends MatrixCursor {

    public static final String KEY_BINDER_WRAPPER = "KeyBinderWrapper";

    private static Map<String, DispatcherCursor> cursorMap = new ConcurrentHashMap<>();

    public static final String[] DEFAULT_COLUMNS = {"col"};

    private Bundle binderExtras = new Bundle();

    public DispatcherCursor(String[] columnNames, IBinder binder) {
        super(columnNames);
        binderExtras.putParcelable(KEY_BINDER_WRAPPER, new BinderWrapper(binder));
    }

    @Override
    public Bundle getExtras() {
        return binderExtras;
    }

    public static DispatcherCursor generateCursor(IBinder binder) {
        try {
            DispatcherCursor cursor;
            cursor = cursorMap.get(binder.getInterfaceDescriptor());
            if (cursor != null) {
                return cursor;
            }
            cursor = new DispatcherCursor(DEFAULT_COLUMNS, binder);
            cursorMap.put(binder.getInterfaceDescriptor(), cursor);
            return cursor;
        } catch (RemoteException ex) {
            return null;
        }
    }

    public static IBinder stripBinder(Cursor cursor) {
        if (null == cursor) {
            return null;
        }
        Bundle bundle = cursor.getExtras();
        bundle.setClassLoader(BinderWrapper.class.getClassLoader());
        BinderWrapper binderWrapper = bundle.getParcelable(KEY_BINDER_WRAPPER);
        return null != binderWrapper ? binderWrapper.getBinder() : null;
    }}

其中BinderWrapper是binder的包裝類食茎,其定義如下:

public class BinderWrapper implements Parcelable {

    private final IBinder binder;

    public BinderWrapper(IBinder binder) {
        this.binder = binder;
    }

    public BinderWrapper(Parcel in) {
        this.binder = in.readStrongBinder();
    }

    public IBinder getBinder() {
        return binder;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeStrongBinder(binder);
    }

    public static final Creator<BinderWrapper> CREATOR = new Creator<BinderWrapper>() {
        @Override
        public BinderWrapper createFromParcel(Parcel source) {
            return new BinderWrapper(source);
        }

        @Override
        public BinderWrapper[] newArray(int size) {
            return new BinderWrapper[size];
        }
    };}

再回到我們的問題蒂破,其實只需要設置一個與Dispatcher在同一個進程的ContentProvider,那么這個問題就解決了。

Dispatcher的進程設置

由于Dispatcher承擔著管理各進程的binder的重任别渔,所以不能讓它輕易狗帶附迷。對于絕大多數(shù)App,主進程是存活時間最長的進程哎媚,將Dispatcher置于主進程就可以了喇伯。但是,有些App中存活時間最長的不一定是主進程拨与,比如有的音樂App, 將主進程殺掉之后稻据,播放進程仍然存活,此時顯然將Dispatcher置于播放進程是一個更好的選擇截珍。
為了讓使用Andromeda這個方案的開發(fā)者能夠根據(jù)自己的需求進行配置攀甚,提供了DispatcherExtension這個Extension, 開發(fā)者在apply plugin: ‘org.qiyi.svg.plugin'之后,可在gradle中進行配置:

dispatcher{
    process ":downloader"}
}

當然岗喉,如果主進程就是存活時間最長的進程的話秋度,則不需要做任何配置,只需要apply plugin: 'org.qiyi.svg.plugin'即可钱床。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荚斯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子查牌,更是在濱河造成了極大的恐慌事期,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纸颜,死亡現(xiàn)場離奇詭異兽泣,居然都是意外死亡,警方通過查閱死者的電腦和手機胁孙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門唠倦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來称鳞,“玉大人,你說我怎么就攤上這事稠鼻「灾梗” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵候齿,是天一觀的道長熙暴。 經(jīng)常有香客問我,道長慌盯,這世上最難降的妖魔是什么周霉? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮亚皂,結(jié)果婚禮上诗眨,老公的妹妹穿的比我還像新娘。我一直安慰自己孕讳,他們只是感情好匠楚,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厂财,像睡著了一般芋簿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璃饱,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天与斤,我揣著相機與錄音,去河邊找鬼荚恶。 笑死撩穿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的谒撼。 我是一名探鬼主播食寡,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼廓潜!你這毒婦竟也來了抵皱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤辩蛋,失蹤者是張志新(化名)和其女友劉穎呻畸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悼院,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡伤为,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了据途。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绞愚。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡剑鞍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出爽醋,到底是詐尸還是另有隱情,我是刑警寧澤便脊,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布蚂四,位于F島的核電站,受9級特大地震影響哪痰,放射性物質(zhì)發(fā)生泄漏遂赠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一晌杰、第九天 我趴在偏房一處隱蔽的房頂上張望跷睦。 院中可真熱鬧,春花似錦肋演、人聲如沸抑诸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜕乡。三九已至,卻和暖如春梗夸,著一層夾襖步出監(jiān)牢的瞬間层玲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工反症, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辛块,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓铅碍,卻偏偏與公主長得像润绵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胞谈,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理授药,服務發(fā)現(xiàn),斷路器呜魄,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • Android跨進程通信IPC整體內(nèi)容如下 1悔叽、Android跨進程通信IPC之1——Linux基礎(chǔ)2、Andro...
    隔壁老李頭閱讀 11,888評論 11 56
  • 3.5 Android進程間通信 3.5.1 背景知識 傳統(tǒng)IPC Linux傳統(tǒng)的IPC機制分為如下幾種:管道爵嗅、...
    jianhuih閱讀 5,539評論 1 5
  • Android跨進程通信IPC整體內(nèi)容如下 1娇澎、Android跨進程通信IPC之1——Linux基礎(chǔ)2、Andro...
    隔壁老李頭閱讀 10,417評論 11 44
  • 膩煩一成不變的生活 沉迷于黑色的世界 想換穿衣風格真的煎熬 需要新的突破與沖動 我漸漸意識到服飾 少而精的重要性 ...
    韓鯊魚閱讀 187評論 0 0