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

好文推薦:
作者:wanderingguy
轉載鏈接:https://juejin.cn/post/6844904034227224589?utm_source=tuicool&utm_medium=referral%3Futm_source%3Dtuicool&utm_medium=referral#comment

關於組件化

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

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

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

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

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

多進程的好處是可以佔用更多的系統資源,並且獨立核心進程可以免受非核心業務出現異常情況導致整個APP崩潰不可用。

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

基於Service的IPC通信

我們回想一下是如何使用Service進行跨進程通信的。

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

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

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

  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架構圖

RemoteTransfer

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

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

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

#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屬於遠程服務所在進程。

這是整個Andromeda工程最最核心的原理,你是不是快看不懂了,沒關係,下面會結合時序圖、關係圖詳細分析實現過程。

本地服務

本地服務沒什麼講的,內部通過維護一個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);
    }
}

遠程服務

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

我們先來看服務的註冊,時序圖如下 ↓

  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當中。

圖中藍色的節點表示註冊服務的當前進程,也就是Server進程,紅色節點表示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());
    }
}

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

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返回給調用方。

同樣圖中藍色的節點表示獲取服務的進程,也就是Client進程,紅色節點表示Dispatcher進程。

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

進程優先級

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

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

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

回到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過程發生在上一章節獲取遠程服務時,流程如下圖:

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

  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核心的原理我們就分析完了,雖然有些問題有待完善,但已經給我們提供了很多優秀的解決問題的思路,無論是繼續優化還是精簡一下本地化都是不錯的選擇。

如果還想了解更多Android 相關的更多知識點,可以點進我的GitHub項目中:https://github.com/733gh/GH-Android-Review-master自行查看,裏面記錄了許多的Android 知識點。

持續更新--請Android的小夥伴關注! 喜歡的話給一個贊Star吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章