Android組件化跨進程通訊框架Andromeda解析

關於組件化

隨着項目結構愈來愈龐大,模塊與模塊間的邊界逐漸變得不清晰,代碼維護愈來愈困難,甚至編譯速度都成爲影響開發效率的瓶頸。java

組件化拆分是比較常見的解決方案,一方面解決模塊間的耦合關係、將通用模塊下沉,另外一方面作到各模塊代碼和資源文件的隔離,這樣即可以放心進行模塊按需編譯、單獨測試等等。android

但隨之而來的問題也越發突出,模塊的精細化拆分不可避免的增長了模塊間的通訊成本。通訊的兩側是一個C/S架構,若是服務端與客戶端同屬一個進程咱們稱之爲本地服務,若是分屬不一樣進程稱之爲遠程服務。注意這裏的服務不只限於Android中的Service組件,而是一種能夠對外提供功能或數據的能力。git

對於同進程的通訊比較簡單,經過註冊本地接口和實現就能夠完成,若是你已經接入ARouter,直接聲明服務類繼承IProvider+Router註解就完成了服務的註冊。github

可是對於跨進程的通訊就比較複雜了,在Android系統中IPC通訊經過Binder實現,對參與通訊的數據格式作了限制,也就是基本數據類型或者實現Parcelable接口的類型。數據庫

多進程的好處是能夠佔用更多的系統資源,而且獨立核心進程能夠免受非核心業務出現異常狀況致使整個APP崩潰不可用。緩存

跨進程通訊業務場景比較複雜,既要保證服務端的可靠性,還須要支持callback,一般Service是首選。bash

基於Service的IPC通訊

咱們回想一下是如何使用Service進行跨進程通訊的。架構

  1. 聲明提供服務的AIDL接口。
  2. 建立Service,並在onBind方法返回實現Stub接口的Binder對象。
  3. Client端經過intent bindService,並傳入ServiceConnection對象,在onServiceConnected回調獲取Service提供的Binder對象。

本質上是將Binder對象(準確的說是代理對象)在進程間進行傳遞,而Service只是一個載體。app

在組件化的大業務背景下,模塊間的通訊接口數量可能不少,按這套方案會有不少問題。框架

  1. 須要書寫AIDL文件和Service類。
  2. bindService是異步操做,須要寫回調,與本地服務調用方式不統一。
  3. 沒用統一的Binder管理者,如何處理Binder Die,如何實現Binder緩存等問題。

這樣咱們能夠總結出一個好的組件化通訊框架須要具有特色或者說要實現的訴求。

組件化跨進程通訊的核心訴求

  • 可不能夠不寫AIDL文件,用聲明普通接口類的方式聲明一個遠程服務接口;可不能夠不寫Service,由於IPC通訊的本質只是傳遞Binder而已。
  • 咱們但願像調用本地服務同樣調用遠程服務,避免回調地獄,即遠程服務的獲取是阻塞式調用。
  • 如何管理各個進程提供的遠程服務,保證高可用。

囉嗦了這麼半天回到咱們今天的主題Andromeda,文章有點長,但願你耐心閱讀,必定有收穫!

Andromeda

Andromeda是愛奇藝開源的組件化IPC通訊解決方案,它解決了上述的問題2和3,同時不須要書寫Service,可是仍須要些AIDL文件。

對於這個問題,餓了嗎早前開源的 Hermes框架 能夠作到,原理是利用動態代理+反射的方式來替換AIDL生成的靜態代理,可是不支持oneway、in、out、inout等修飾符。

再後來,愛奇藝又開源 InterStellar ,實現了不須要書寫AIDL文件,當使用跨進程接口時,聲明@oneway/@in等註解完成IPC修飾符的添加。這樣算是完全的實現了遠程調用像本地調用同樣簡單。但不知爲什麼與Andromeda沒有合併到一個項目中,工程代碼也好久沒有人維護。

此外Andromeda還有一些Feature:

  • 加入了跨進程通訊的事件總線,即跨進程版EventBus。
  • 加入了對加強進程穩定性的考量,經過爲各個進程預先插樁Service,在獲取遠程服務時用前臺UI組件(Activity/Fragment/View)綁定插樁的Service,最終提高後臺服務進程優先級。
  • 支持IPCCallback。
  • 支持配置Binder分發管理中心(Dispatcher)所屬進程。

Andromeda Github地址

咱們先來看一下簡單的使用

//註冊本地服務 第一個參數是接口class未來用做key,第二參數爲接口實現類。
Andromeda.registerLocalService(ICheckApple.class, new CheckApple());
//使用本地服務
ICheckApple checkApple = Andromeda.getLocalService(ICheckApple.class);
------------------------------
//註冊遠程服務 第二個參數爲IBinder類型,未來會在進程間傳遞
Andromeda.registerRemoteService(IBuyApple.class, BuyAppleImpl.getInstance());
//使用遠程服務,傳入UI組件(this)嘗試提高遠程服務進程的優先級
Andromeda.with(this).getRemoteService(IBuyApple.class);
複製代碼

總體API的設計清晰且所有都是同步完成,詳細使用見工程示例,本篇的重點是分析內部原理。

雖然是源碼分析,但我不許備貼過多的源碼,這樣閱讀體驗並很差;我會盡可能剋制,真正有需求的小夥伴請自行查閱源代碼,個人目標是把核心思想講清楚。

架構分析

咱們先理清幾個概念,不管是事件總線仍是服務分發都須要一箇中轉存儲中心,這個中心在Andromeda框架中叫Dispatcher。

Dispatcher

它是一個AIDL接口,各個進程在註冊服務時須要首先拿到DispatcherProxy,而後將本進程服務Binder傳送給DispatcherProxy存儲,當其餘進程須要使用該服務時,也須要先獲取一個DispatcherProxy,而後讀取DispatcherProxy中的緩存Binder,並在本身進程存儲一份緩存,這樣本進程下次獲取相同的服務時就不須要進行IPC調用了。

咱們來看一下Dispatcher提供了哪些功能。

# IDispatcher.aidl
interface IDispatcher {
   //經過服務名稱獲取Binder包裝類BinderBean
   BinderBean getTargetBinder(String serviceCanonicalName);
   //保留接口暫時爲空實現
   IBinder fetchTargetBinder(String uri);
   //註冊本地的RemoteTransfer
   void registerRemoteTransfer(int pid,IBinder remoteTransferBinder);
   //註冊/反註冊遠程服務
   void registerRemoteService(String serviceCanonicalName,String processName,IBinder Binder);
   void unregisterRemoteService(String serviceCanonicalName);

   //發送事件
   void publish(in Event event);
}
複製代碼

Dispatcher所在進程能夠是主進程也能夠用戶自定義的進程,爲何要討論Dispatcher所屬進程呢?由於做爲組件化通訊核心的Center一旦狗帶,將致使以前註冊服務不可用,因此須要將它放在應用生命週期最長的進程中,一般這個進程是主進程,但對於相似音樂播放器相關的app來講,多是一個獨立的播放器進程,因此框架爲咱們提供了一個配置項能夠顯式的聲明Dispatcher所在進程。

#主工程的build.gradle添加聲明
dispatcher{
    process ":downloader"
}
複製代碼

Dispatcher架構圖

arch.jpeg

RemoteTransfer

上面提到各個進程本身自己也須要管理(緩存)從Dispatcher獲取的Binder,防止重複的IPC請求;另外因爲事件總線的需求,各個進程須要向Dispatcher進程註冊本進程組件管理員,這樣當事件pubish後,Dispatcher才能將事件發送給各個進程,這個各個進程管理員就是RemoteTransfer。

IRemoteTransfer是一個AIDL接口,RemoteTransfer是它的實現類,RemoteTransfer還實現了IRemoteServiceTransfer接口。

這裏須要一張類圖來幫你理清思路:

remote_class.jpeg

#IRemoteTransfer.aidl
interface IRemoteTransfer {
	① 將Dispatcher代理返回給RemoteTransfer
    oneway void registerDispatcher(IBinder dispatcherBinder);

    oneway void unregisterRemoteService(String serviceCanonicalName);

    oneway void notify(in Event event);
}

#IRemoteServiceTransfer.java
public interface IRemoteServiceTransfer {
	//②獲取遠程服務包裝
    BinderBean getRemoteServiceBean(String serviceCanonicalName);

    //註冊/反註冊 遠程服務
    void registerStubService(String serviceCanonicalName, IBinder stubBinder);
    void unregisterStubService(String serviceCanonicalName);
}
複製代碼

兩個問題須要注意:

① 方法的調用方在Dispatcher中,這樣就把Dispatcher的遠程代理回傳給了當前進程,以後註冊遠程服務就能夠經過這個DispatcherProxy完成。

② 不管是註冊仍是獲取遠程服務,都是不不能直接傳遞Binder的,由於Binder並無實現Parcelable接口,所以須要將Binder包裝在一個實現了Parcelable接口的類中傳遞,BinderBean就是其中一個包裝類。

主體邏輯已經講清楚了,咱們正式開始分析功能。

  • 經過ContentProvider方式同步的獲取Dispatcher,這個ContentProvider屬於Dispatcher進程,且經過插樁的方式織入manifeset文件。
  • 獲取遠程服務時傳遞當前進程的Activity或Fragment,並bind預先插樁好的StubService,這個StubService屬於遠程服務所在進程。

本地服務

本地服務沒什麼講的,內部經過維護一個Map關係表,來記錄註冊服務的名稱和實現類。

# LocalServiceHub
public class LocalServiceHub implements ILocalServiceHub {
    private Map<String, Object> serviceMap = new ConcurrentHashMap<>();

    @Override
    public Object getLocalService(String module) {
        return serviceMap.get(module);
    }

    @Override
    public void registerService(String module, Object serviceImpl) {
        serviceMap.put(module, serviceImpl);
    }

    @Override
    public void unregisterService(String module) {
        serviceMap.remove(module);
    }
}
複製代碼

遠程服務

遠程服務是框架的核心,對遠程服務的操做就是兩個,一是註冊遠程服務,二是獲取遠程服務。

咱們先來看服務的註冊,時序圖以下 ↓

Andromeda_register (2).jpg

  1. 客戶端經過<T extends IBinder> registerRemoteService(String serviceCanonicalName, T stubBinder)註冊本進程可提供的遠程服務,stubBinder即服務實現類。
  2. 調用RemoteTransfer的registerStubService方法。
  3. registerStubService內部先初始化DispatcherProxy,若是爲空跳轉3.1。
    • 3.1-3.2 要實現服務的同步註冊,本質上是同步獲取DispatcherProxy,這是一次IPC通訊,Andromeda的方案是在Dispatcher進程插樁一個ContentProvider,而後返回一個包含DispatcherProxy的Cursor給客戶端進程,客戶端解析Cursor拿到DispatcherProxy。
  4. RemoteTransfer請求RemoteServiceTransfer幫忙完成真正的註冊。
  5. RemoteServiceTransfer經過第3步獲取的DispatcherProxy,作一次IPC通訊,將Binder傳遞到Dispatcher進程。
  6. Dispatcher進程請求ServiceDispatcher類幫忙完成服務的註冊,其實就是將Binder存儲在一個Map當中。

圖中藍色的節點表示註冊服務的當前進程,紅色節點表示Dispatcher進程。

整個過程重點在第三步,咱們再重點分析一下:

# RemoteTransfer
private void initDispatchProxyLocked() {
    if (null == dispatcherProxy) {
    	//從contentprovider取Binder
        IBinder dispatcherBinder = getIBinderFromProvider();
        if (null != dispatcherBinder) {
        	//取出後asInterface建立遠程代理對象
            dispatcherProxy = IDispatcher.Stub.asInterface(dispatcherBinder);
            registerCurrentTransfer();
        }
    }
    ...
}

private void registerCurrentTransfer() {
	//向Dispatcher註冊本身這個進程的RemoteTransfer Binder
    dispatcherProxy.registerRemoteTransfer(android.os.Process.myPid(), this.asBinder());
    ...
}

private IBinder getIBinderFromProvider() {
    Cursor cursor = null;
    try {
    	//經過contentprovider拿到cursor
        cursor = context.getContentResolver().query(getDispatcherProviderUri(), DispatcherProvider.PROJECTION_MAIN,
                null, null, null);
        if (cursor == null) {
            return null;
        }
        return DispatcherCursor.stripBinder(cursor);
    } finally {
        IOUtils.closeQuietly(cursor);
    }
}

複製代碼

咱們來看這個DispatcherProvider

public class DispatcherProvider extends ContentProvider {
	...
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    	//將Binder封裝到cursor中返回
        return DispatcherCursor.generateCursor(Dispatcher.getInstance().asBinder());
    }
}
複製代碼

接下來咱們看服務的獲取,一樣的先看時序圖 ↓

Android_getRemoteService.jpg

1. Andromeda入口經過getRemoteService獲取遠程服務。

2-4. 與提高進程優先級有關,咱們暫且不討論。

5. 向RemoteTransfer請求獲取遠程服務的包裝bean。

6-7. RemoteTransfer請求RemoteServiceTransfer幫忙先從本進程的緩存中查找目標Binder,若是找到直接返回。

7.2. 若是沒有命中緩存調用getAndSaveIBinder方法,經過方法名可知,獲取後會將Binder緩存起來,這就是6-7步讀取的緩存。

8. RemoteServiceTransfer經過DispatcherProxy發起IPC通訊,請求遠程服務Binder。

9-10. Dispatcher請ServiceDispatcher幫忙查找進程中的服務註冊表。

11. 回到客戶端進程將Binder緩存。

12. 將Binder返回給調用方。

一樣圖中藍色的節點表示獲取服務的進程,紅色節點表示Dispatcher進程。

至此,遠程服務的註冊與獲取流程分析結束。

進程優先級

上面提到在獲取遠程服務時,框架作了提高進程優先級的事情。一般狀況下使用遠程服務的端(簡稱Client端)處於前臺進程,而Server端進程已經註冊完畢,每每處於後臺。爲了提高Server端的穩定性,最好能將Server端的進程優先級與Client保持接近,不然容易出現被LMK(Low Memory Killer)回收的狀況。

那如何提高Server端進程的優先級呢?這裏的作法是用前臺的UI組件(Activity/Fragment/View)bind一個Server端預先插樁好的Service。

整套流程最終經過AMS的updateOomAdjLocked方法實現。

提高進程Adj.jpeg

回到Andromeda實現,這個預先插樁的Service以下:

public class CommuStubService extends Service {

    public CommuStubService() {}

    @Override
    public IBinder onBind(Intent intent) {
        return new ICommuStub.Stub() {
            @Override
            public void commu(Bundle args) throws RemoteException {
                //do nothing now
            }
        };
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //這樣可使Service所在進程的保活效果好一點
        return Service.START_STICKY;
    }

    public static class CommuStubService0 extends CommuStubService {}
    public static class CommuStubService1 extends CommuStubService {}
    public static class CommuStubService2 extends CommuStubService {}
    ...
    public static class CommuStubService14 extends CommuStubService {}
}

複製代碼

可見框架預置了15個Service供進程使用,也就是最多支持15個進程,這絕大數場景下足夠了;另外維護了一個進程名和Service名稱的映射表,不然怎麼知道應該bind那個Service,這個映射表也是在編譯階段插樁完成的。

這個service的bind過程發生在上一章節獲取遠程服務時,流程以下圖:

提高進程優先級流程.jpg

圖中模塊根據所在進程分爲三部分:

  1. 藍色表示Client進程,發起獲取遠程服務請求。
  2. 淺灰色表示Server進程,它事先將服務註冊到Dispatcher中。
  3. 紫色表示Dispatcher進程,內部緩存了各個進程的服務的Binder對象。

咱們重點關注的是藍色模塊ConnectionManager部分,實際上當Client向Dispatcher請求遠程服務以後,會當即經過ConnectionManager綁定這個遠程服務所在進程的插樁的StubService,如此一來就提高了Server所在進程的優先級。

至此bind操做已經完成了,那什麼時候unbind呢?顯然是當UI組件銷燬時,由於此時已不在前臺,須要下降進程優先級。

如此一來就須要監聽UI組件的生命週期,在onDestroy時進行unbind操做。

這就是圖中RemoteManager作的事情,它內部維護了前臺組件的生命週期。Andromeda提供了幾種with方法,用於獲取對應RemoteManager:

public static IRemoteManager with(android.app.Fragment fragment) {return getRetriever().get(fragment);}
public static IRemoteManager with(Fragment fragment) {return getRetriever().get(fragment);}
public static IRemoteManager with(FragmentActivity fragmentActivity) {return getRetriever().get(fragmentActivity);}
public static IRemoteManager with(Activity activity) {return getRetriever().get(activity);}
public static IRemoteManager with(Context context) {return getRetriever().get(context);}
public static IRemoteManager with(View view) {return getRetriever().get(view);}
複製代碼

這是借鑑Glide的作法,這些方法最終被轉換爲兩類:

  1. 具有生命週期的UI組件,最終是Activity或Fragment。
  2. ApplicationContext。

對於第一種狀況,框架會爲當前Activity或Fragment添加一個不可見的RemoteManagerFragment以監聽生命週期。

對於使用ApplicationContext,獲取遠程服務的場景不作unbind操做。

事實上用Jetpack lifecycle組件也能夠方便的監聽Activity/Fragment的生命週期,可是這有個前提,那就是Activity必須繼承android.support.v4.app.FragmentActvity,而Fragment必須繼承 android.support.v4.app.Fragment,且v4庫的版本必須大於等於26.1.0,從這個版本開始支持了Lifecycle。

事件總線

在上述通訊框架基礎之上,實現事件總線簡直易如反指。

咱們來看一下使用

//訂閱事件,這裏MainActivity實現了EventListener接口
Andromeda.subscribe(EventConstants.APPLE_EVENT,MainActivity.this);

//發佈事件
Bundle bundle = new Bundle();
bundle.putString("Result", "gave u five apples!");
Andromeda.publish(new Event(EventConstants.APPLE_EVENT, bundle));
複製代碼

這裏的Event是事件傳遞的載體。

public class Event implements Parcelable {
    private String name;
    private Bundle data;
    ...
}
複製代碼

至於原理,回想一下咱們在註冊遠程服務的過程當中,同時將本進程的RemoteTransfer的Binder也註冊到了Dispatcher中。

當咱們訂閱一個事件時,只是將Event名稱和監聽器存儲在了本進程的RemoteTransfer中,當另外一個進程發佈事件時,會經過一次IPC調用將Event對象發送到Dispatcher,Dispatcher收到事件後,會向註冊過的RemoteTransfer依次發送回調信息,也就是說這一步可能進行屢次IPC調用,效率問題需diss一下。

事件到達訂閱進程後會根據事件名稱,提取全部關於此名稱的監聽器,最終發送給監聽者。

注意:這裏的監聽器經常使用的是Activity,但顯然RemoteTransfer是屬於進程生命週期的,所以保存監聽器時需使用弱引用。

插樁

上面分析原理過程當中反覆提到了插樁,總結一下共有幾處:

  1. 將屬於Dispatcher進程的DispatcherProvider和DispatcherService插入到manifest中(StubServiceGenerator)。
  2. 將各個進程的預置StubService插入到manifest中(StubServiceGenerator)。
  3. 將進程名與StubService的關係表插入到StubServiceMatcher類的map中(StubServiceMatchInjector)。

對於manifest的操做,框架內提供了很多工具方法,好比獲取全部聲明的進程,值得好好學習一下;對於class的操做使用的是javasisst,這在以前的AOP文章中也介紹過,感興趣的同窗自行查閱。


在讀源碼過程當中發現兩個值得關注的問題:

一是DispatcherProvider僞造的DispatcherCursor繼承MatrixCursor,它一般用於返回幾條固定的已知記錄,不須要從數據庫查詢這種場景。

二是跨進程傳遞bundle對象時,若是bundle中存放了parcelable對象須要手動設置setClassLoader。

#DispatcherCursor
public static IBinder stripBinder(Cursor cursor) {
    if (null == cursor) {
        return null;
    }
    Bundle bundle = cursor.getExtras();
    //從cursor中取出bundle須要設置classLoader
    bundle.setClassLoader(BinderWrapper.class.getClassLoader());
    BinderWrapper BinderWrapper = bundle.getParcelable(KEY_Binder_WRAPPER);
    return null != BinderWrapper ? BinderWrapper.getBinder() : null;
}
複製代碼

由於默認狀況下bundle傳輸使用的ClassLoader是BootClassLoader,而BootClassLoader只能加載系統類,咱們本工程的class須要使用PathClassLoader進行加載,所以須要額外的調用bundle的setClassLoader方法設置類加載器,詳見Bundle.setClassLoader()方法解析

缺點

  • 服務須要手動註冊,這個時機很差把握。最好能提供一個自動註冊服務的開關,上層不須要關注服務的註冊。
  • 發送一次事件須要屢次IPC調用效率低,有優化空間。
  • 仍須要書寫AIDL文件。

至此,Andromeda核心的原理咱們就分析完了,雖然有些問題有待完善,但已經給咱們提供了不少優秀的解決問題的思路,不管是繼續優化仍是精簡一下本地化都是不錯的選擇。

相關文章
相關標籤/搜索