引言
其實Android的元件化由來已久,而且已經有了一些不錯的方案,特別是在頁面跳轉這方面,比如阿里的ARouter, 天貓的統跳協議, Airbnb的DeepLinkDispatch, 藉助註解來完成頁面的註冊,從而很巧妙地實現了路由跳轉。
但是,儘管像ARouter等方案其實也支援介面的路由,然而令人遺憾的是隻支援單程式的介面路由。
而目前愛奇藝App中,由於複雜的業務場景,導致既有單程式的通訊需求,也有跨程式的通訊需求,並且還要支援跨程式通訊中的Callback呼叫,以及全域性的事件匯流排。
那能不能設計一個方案,做到滿足以上需求呢?
這就是Andromeda的誕生背景,在確定了以上需求之後,分析論證了很多方案,最終選擇了目前的這個方案,在滿足要求的同時,還做到了整個程式間通訊的阻塞式呼叫,從而避免了非常ugly的非同步連線程式碼。
Andromeda的功能
Andromeda目前已經開源,開源地址為開源地址為github.com/iqiyi/Andro….
由於頁面跳轉已經有完整而成熟的方案,所以Andromeda就不再做頁面路由的功能了。目前Andromeda主要包含以下功能:
- 本地服務路由,註冊本地服務是registerLocalService(Class, Object), 獲取本地服務是getLocalService(Class);
- 遠端服務路由,註冊遠端服務是registerRemoteService(Class, Object), 獲取遠端服務是getRemoteService(Class);
- 全域性(含所有程式)事件匯流排, 訂閱事件為subscribe(String, EventListener), 釋出事件為publish(Event);
- 遠端方法回撥,如果某個業務介面需要遠端回撥,可以在定義aidl介面時使用IPCCallback;
注: 這裡的服務不是Android中四大元件的Service,而是指提供的介面與實現。為了表示區分,後面的服務均是這個含義,而Service則是指Android中的元件。
這裡為什麼需要區分本地服務和遠端服務呢?
最重要的一個原因是本地服務的引數和返回值型別不受限制,而遠端服務則受binder通訊的限制。
可以說,Andromeda的出現為元件化完成了最後一塊拼圖。
Andromeda和其他元件間通訊方案的對比如下:
易用性 | IPC效能 | 支援IPC | 支援跨程式事件匯流排 | 支援IPC Callback | |
---|---|---|---|---|---|
Andromeda | 好 | 高 | Yes | Yes | Yes |
DDComponentForAndroid | 較差 | -- | No | No | No |
ModularizationArchitecture | 較差 | 低 | Yes | No | No |
介面依賴還是協議依賴
這個討論很有意思,因為有人覺得使用Event或ModuleBean來作為元件間通訊載體的話,就不用每個業務模組定義自己的介面了,呼叫方式也很統一。
但是這樣做的缺陷也很明顯:第一,雖然不用定義介面了,但是為了適應各自的業務需求,如果使用Event的話,需要定義許多Event; 如果使用ModuleBean的話,需要為每個ModuleBean定義許多欄位,甚至於即使是讓另一方呼叫一個空方法,也需要建立一個ModuleBean物件,這樣的消耗是很大的; 而且隨著業務增多,這個模組對應的ModuleBean中需要定義的欄位會越來越多,消耗會越來越大。
第二,程式碼可讀性較差。定義Event/ModuleBean的方式不如介面呼叫那麼直觀,不利於專案的維護;
第三,正如微信Android模組化架構重構實踐(上)中說到的那樣,"我們理解的協議通訊,是指跨平臺/序列化的通訊方式,類似終端和伺服器間的通訊或restful這種。現在這種形式在終端內很常見了。協議通訊具備一種很強力解耦能力,但也有不可忽視的代價。無論什麼形式的通訊,所有的協議定義需要讓通訊兩方都能獲知。通常為了方便會在某個公共區域存放所有協議的定義,這情況和Event引發的問題有點像。另外,協議如果變化了,兩端怎麼同步就變得有點複雜,至少要配合一些框架來實現。在一個應用內,這樣會不會有點複雜?用起來好像也不那麼方便?更何況它究竟解決多少問題呢"。
顯然,協議通訊用作元件間通訊的話太重了,從而導致它應對業務變化時不夠靈活。
所以最終決定採用"介面+資料結構"的方式進行元件間通訊,對於需要暴露的業務介面和資料結構,放到一個公共的module中。
跨程式路由方案的實現
本地服務的路由就不說了,一個Map就可以搞定。
比較麻煩的是遠端服務,要解決以下難題:
- 讓任意兩個元件都能夠很方便地通訊,即一個元件註冊了自己的遠端服務,任意一個元件都能輕易呼叫到
- 讓遠端服務的註冊和使用像本地服務一樣簡單,即要實現阻塞呼叫
- 不能降低通訊的效率
封裝bindService
這裡最容易想到的就是對傳統的Android IPC通訊方式進行封裝,即在bindService()的基礎上進行封裝,比如ModularizationArchitecture這個開源庫中的WideRouter就是這樣做的,構架圖如下:
這個方案有兩個明顯的缺陷:
- 每次IPC都需要經過WideRouter,然後再轉發到對應的程式,這樣就導致了本來一次IPC可以解決的問題,需要兩次IPC解決,而IPC本身就是比較耗時的
- 由於bindService是非同步的,實際上根本做不到真正的阻塞呼叫
- WideConnectService需要存活到最後,這樣的話就要求WideConnectService需要在存活週期最長的那個程式中,而現在無法動態配置WideConnectService所在的程式,導致在使用時不方便
考慮到這幾個方面,這個方案pass掉。
Hermes
這是之前一個餓了麼同事寫的開源框架,它最大的特色就是不需要寫AIDL介面,可以直接像呼叫本地介面一樣呼叫遠端介面。
而它的原理則是利用動態代理+反射的方式來替換AIDL生成的靜態代理,但是它在跨程式這方面本質上採用的仍然是bindService()的方式,如下:
其中Hermes.connect()本質上還是bindService()的方式,那同樣存在上面的那些問題。另外,Hermes目前還不能很方便地配置程式,以及還不支援in, out, inout等IPC修飾符。
不過,儘管有以上缺點,Hermes仍然是一個優秀的開源框架,至少它提供了一種讓IPC通訊和本地通訊一樣簡單的思路。
最終方案
再回過頭來思考前面的方案,其實要呼叫遠端服務,無非就是要獲取到通訊用的IBinder,而前面那兩個方案最大的問題就是把遠端服務IBinder的獲取和Service繫結在了一起,那是不是一定要繫結在一起呢? 有沒有可能不通過Service來獲取IBinder呢?
其實是可以的,我們只需要有一個binder的管理器即可。
核心流程
最終採用了註冊-使用的方式,整體架構如下圖:
這個架構的核心就是Dispatcher和RemoteTransfer, Dispatcher負責管理所有程式的業務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)傳送給與Dispatcher同程式的DispatcherService, DispatcherService收到之後通知Dispatcher, Dispatcher就通過RemoteTransfer的binder將自己反射註冊過去,這樣RemoteTransfer就獲取到了Dispatcher的代理。
這個過程用流程圖表示如下:
這個註冊過程一般發生在子程式初始化的時候,但是其實即使在子程式初始化時沒有註冊也不要緊,其實是可以推遲到需要將自己的遠端服務提供出去,或者需要獲取其他程式的Module的服務時再做這件事也可以,具體原因在下一小節會分析。
遠端服務註冊的流程如下所示:
Dispatcher則持有所有程式的RemoteTransfer的代理binder, 以及所有提供服務的業務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);
}
複製程式碼
Dispatcher提供的服務是由RemoteTransfer來呼叫的,各個方法的命名都很相信大家都能看懂,就不贅述了。
同步獲取binder的問題
前面的方案中有一個問題我們還沒有提到,那就是同步獲取服務binder的問題。
設想這樣一個場景:在Dispatcher反向註冊之前,就有一個Module想要呼叫另外一個程式中的某個服務(這個服務已經註冊到Dispatcher中), 那麼此時如何同步獲取呢?
這個問題的核心其實在於,如何同步獲取IDispatcher的binder?
其實是有辦法的,那就是通過ContentProvider!
有兩種通過ContentProvider直接獲取IBinder的方式,比較容易想到的是利用ContentProviderClient, 其呼叫方式如下:
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);
}
複製程式碼
可以在呼叫結果的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的重任,所以不能讓它輕易狗帶。
對於絕大多數App,主程式是存活時間最長的程式,將Dispatcher置於主程式就可以了。
但是,有些App中存活時間最長的不一定是主程式,比如有的音樂App, 將主程式殺掉之後,播放程式仍然存活,此時顯然將Dispatcher置於播放程式是一個更好的選擇。
為了讓使用Andromeda這個方案的開發者能夠根據自己的需求進行配置,提供了DispatcherExtension這個Extension, 開發者在apply plugin: 'org.qiyi.svg.plugin'之後,可在gradle中進行配置:
dispatcher{
process ":downloader"
}
複製程式碼
當然,如果主程式就是存活時間最長的程式的話,則不需要做任何配置,只需要apply plugin: 'org.qiyi.svg.plugin'即可。
提升服務提供方的程式優先順序
其實本來Andromeda作為一個提供通訊的框架,我並不想做任何提供程式優先順序有關的事情,但是根據一些以往的統計資料,為了儘可能地避免在通訊過程中出現binderDied問題,至少在通訊過程中需要讓服務提供方的程式優先順序與client端的程式優先順序接近,以減少服務提供方程式被殺的概率。
實際上bindService()就做了提升程式優先順序的事情。在我的部落格bindService過程解析中就分析過,bindService()實質上是做了以下事情:
- 獲取服務提供方的binder
- client端通過bind操作,讓Service所在程式的優先順序提高
整個過程如下所示
所以在這裡就需要與Activity/Fragment聯絡起來了,在一個Activity/Fragment中首次使用某個遠端服務時,會進行bind操作,以提升服務提供方的程式優先順序。
而在Activity/Fragment的onDestroy()回撥中,再進行unbind()操作,將連線釋放。
這裡有一個問題,就是雖然bind操作對使用者不可見,但是怎麼知道bind哪個Service呢?
其實很簡單,在編譯時,會為每個程式都插樁一個StubService, 並且在StubServiceMatcher這個類中,插入程式名與StubService的對應關係(編譯時通過javassist插入程式碼),這樣根據程式名就可以獲取對應的StubService.
而IDispatcher的getRemoteService()方法中獲取的BinderBean就包含有程式名資訊。
生命週期管理
上一節提到了在Activity/Fragment的onDestroy()中需要呼叫unbind()操作釋放連線,如果這個unbind()讓開發者來呼叫,就太麻煩了。
所以這裡就要想辦法在Activity/Fragment回撥onDestroy()時我們能夠監聽到,然後自動給它unbind()掉,那麼如何能做到這一點呢?
其實可以借鑑Glide的方式,即利用Fragment/Activity的FragmentManager建立一個監聽用的Fragment, 這樣當Fragment/Activity回撥onDestroy()時,這個監聽用的Fragment也會收到回撥,在這個回撥中進行unbind操作即可。
回撥監聽的原理如下圖所示:
當時其實有考慮過是否藉助Google推出的Arch componentss來處理生命週期問題,但是考慮到還有的團隊沒有接入這一套,加上arch components的方案其實也變過多次,所以就暫時採用了這種方案,後面會視情況決定是否藉助arch components的方案來進行生命週期管理 。
IPCCallback
為什麼需要IPCCallback呢?
對於耗時操作,我們直接在client端的work執行緒呼叫是否可以?
雖然可以,但是server端可能仍然需要把耗時操作放在自己的work執行緒中執行,執行完畢之後再回撥結果,所以這種情況下client端的work執行緒就有點多餘。
所以為了使用方便,就需要一個IPCCallback, 在server端處理耗時操作之後再回撥。
對於需要回撥的AIDL介面,其定義如下:
interface IBuyApple {
int buyAppleInShop(int userId);
void buyAppleOnNet(int userId,IPCCallback callback);
}
複製程式碼
而client端的呼叫如下:
IBinder buyAppleBinder = Andromeda.getRemoteService(IBuyApple.class);
if (null == buyAppleBinder) {
return;
}
IBuyApple buyApple = IBuyApple.Stub.asInterface(buyAppleBinder);
if (null != buyApple) {
try {
buyApple.buyAppleOnNet(10, new IPCCallback.Stub() {
@Override
public void onSuccess(Bundle result) throws RemoteException {
...
}
@Override
public void onFail(String reason) throws RemoteException {
...
}
});
} catch (RemoteException ex) {
ex.printStackTrace();
}
}
複製程式碼
但是考慮到回撥是在Binder執行緒中,而絕大部分情況下呼叫者希望回撥在主執行緒,所以lib封裝了一個BaseCallback給接入方使用,如下:
IBinder buyAppleBinder = Andromeda.getRemoteService(IBuyApple.class);
if (null == buyAppleBinder) {
return;
}
IBuyApple buyApple = IBuyApple.Stub.asInterface(buyAppleBinder);
if (null != buyApple) {
try {
buyApple.buyAppleOnNet(10, new BaseCallback() {
@Override
public void onSucceed(Bundle result) {
...
}
@Override
public void onFailed(String reason) {
...
}
});
} catch (RemoteException ex) {
ex.printStackTrace();
}
}
複製程式碼
開發者可根據自己需求進行選擇。
事件匯流排
由於Dispatcher有了各程式的RemoteTransfer的binder, 所以在此基礎上實現一個事件匯流排就易如反掌了。
簡單地說,事件訂閱時由各RemoteTransfer記錄各自程式中訂閱的事件資訊; 有事件釋出時,由釋出者通知Dispatcher, 然後Dispatcher再通知各程式,各程式的RemoteTransfer再通知到各事件訂閱者。
事件
Andromeda中Event的定義如下:
public class Event implements Parcelable {
private String name;
private Bundle data;
...
}
複製程式碼
即 事件=名稱+資料,通訊時將需要傳遞的資料存放在Bundle中。 其中名稱要求在整個專案中唯一,否則可能出錯。 由於要跨程式傳輸,所以所有資料只能放在Bundle中進行包裝。
事件訂閱
事件訂閱很簡單,首先需要有一個實現了EventListener介面的物件。 然後就可以訂閱自己感興趣的事件了,如下:
Andromeda.subscribe(EventConstants.APPLE_EVENT,MainActivity.this);
複製程式碼
其中MainActivity實現了EventListener介面,此處表示訂閱了名稱為EventConstnts.APPLE_EVENT的事件。
事件釋出
事件釋出很簡單,呼叫publish方法即可,如下:
Bundle bundle = new Bundle();
bundle.putString("Result", "gave u five apples!");
Andromeda.publish(new Event(EventConstants.APPLE_EVENT, bundle));
複製程式碼
InterStellar
在寫Andromeda這個框架的過程中,有兩件事引起了我的注意,第一件事是由於業務binder太多導致SWT異常(即Android Watchdog Timeout).
第二件事是跟同事交流的過程中,思考過能不能不寫AIDL介面, 讓遠端服務真正地像本地服務一樣簡單。
所以就有了InterStellar, 可以簡單地將其理解為Hermes的加強版本,不過實現方式並不一樣,而且InterStellar支援IPC修飾符in, out, inout和oneway.
藉助InterStellar, 可以像定義本地介面一樣定義遠端介面,如下:
public interface IAppleService {
int getApple(int money);
float getAppleCalories(int appleNum);
String getAppleDetails(int appleNum, String manifacture, String tailerName, String userName, int userId);
@oneway
void oneWayTest(Apple apple);
String outTest1(@out Apple apple);
String outTest2(@out int[] appleNum);
String outTest3(@out int[] array1, @out String[] array2);
String outTest4(@out Apple[] apples);
String inoutTest1(@inout Apple apple);
String inoutTest2(@inout Apple[] apples);
}
複製程式碼
而介面的實現也跟本地服務的實現完全一樣,如下:
public class AppleService implements IAppleService {
@Override
public int getApple(int money) {
return money / 2;
}
@Override
public float getAppleCalories(int appleNum) {
return appleNum * 5;
}
@Override
public String getAppleDetails(int appleNum, String manifacture, String tailerName, String userName, int userId) {
manifacture = "IKEA";
tailerName = "muji";
userId = 1024;
if ("Tom".equals(userName)) {
return manifacture + "-->" + tailerName;
} else {
return tailerName + "-->" + manifacture;
}
}
@Override
public synchronized void oneWayTest(Apple apple) {
if(apple==null){
Logger.d("Man can not eat null apple!");
}else{
Logger.d("Start to eat big apple that weighs "+apple.getWeight());
try{
wait(3000);
//Thread.sleep(3000);
}catch(InterruptedException ex){
ex.printStackTrace();
}
Logger.d("End of eating apple!");
}
}
@Override
public String outTest1(Apple apple) {
if (apple == null) {
apple = new Apple(3.2f, "Shanghai");
}
apple.setWeight(apple.getWeight() * 2);
apple.setFrom("Beijing");
return "Have a nice day!";
}
@Override
public String outTest2(int[] appleNum) {
if (null == appleNum) {
return "";
}
for (int i = 0; i < appleNum.length; ++i) {
appleNum[i] = i + 1;
}
return "Have a nice day 02!";
}
@Override
public String outTest3(int[] array1, String[] array2) {
for (int i = 0; i < array1.length; ++i) {
array1[i] = i + 2;
}
for (int i = 0; i < array2.length; ++i) {
array2[i] = "Hello world" + (i + 1);
}
return "outTest3";
}
@Override
public String outTest4(Apple[] apples) {
for (int i = 0; i < apples.length; ++i) {
apples[i] = new Apple(i + 2f, "Shanghai");
}
return "outTest4";
}
@Override
public String inoutTest1(Apple apple) {
Logger.d("AppleService-->inoutTest1,apple:" + apple.toString());
apple.setWeight(3.14159f);
apple.setFrom("Germany");
return "inoutTest1";
}
@Override
public String inoutTest2(Apple[] apples) {
Logger.d("AppleService-->inoutTest2,apples[0]:" + apples[0].toString());
for (int i = 0; i < apples.length; ++i) {
apples[i].setWeight(i * 1.5f);
apples[i].setFrom("Germany" + i);
}
return "inoutTest2";
}
}
複製程式碼
可見整個過程完全不涉及到AIDL.
那它是如何實現的呢?
答案就藏在Transfer中。本質上AIDL編譯之後生成的Proxy其實是提供了介面的靜態代理,那麼我們其實可以改成動態代理來實現,將服務方法名和引數傳遞到服務提供方,然後呼叫相應的方法,最後將結果回傳即可。
InterStellar的分層架構如下:
關於InterStellar的實現詳情,可以到InterStellar github中檢視。
總結
在Andromeda之前,可能是由於業務場景不夠複雜的原因,絕大多數通訊框架都要麼沒有涉及IPC問題,要麼解決方案不優雅,而Andromeda的意義在於同時融合了本地通訊和遠端通訊,只有做到這樣,我覺得才算完整地解決了元件通訊的問題。
其實跨程式通訊都是在binder的基礎上進行封裝,Andromeda的創新之處在於將binder與Service進行剝離,從而使服務的使用更加靈活。
最後,Andromeda目前已經開源,開源地址為開源地址為github.com/iqiyi/Andro….,歡迎大家star和fork,有任何問題也歡迎大家提issue.