其实Android的组件化由来已久,并且已经有了一些不错的方案,特别是在页面跳转这方面,好比阿里的ARouter, 天猫的统跳协议, Airbnb的DeepLinkDispatch, 借助注解来完成页面的注册,从而很巧妙地实现了路由跳转。java
可是,尽管像ARouter等方案其实也支持接口的路由,然而使人遗憾的是只支持单进程的接口路由。git
而目前爱奇艺App中,因为复杂的业务场景,致使既有单进程的通讯需求,也有跨进程的通讯需求,而且还要支持跨进程通讯中的Callback调用,以及全局的事件总线。github
那能不能设计一个方案,作到知足以上需求呢?服务器
这就是Andromeda的诞生背景,在肯定了以上需求以后,分析论证了不少方案,最终选择了目前的这个方案,在知足要求的同时,还作到了整个进程间通讯的阻塞式调用,从而避免了很是ugly的异步链接代码<!--more-->。微信
Andromeda目前已经开源,开源地址为开源地址为https://gitee.com/bettar/Andromeda.restful
因为页面跳转已经有完整而成熟的方案,因此Andromeda就再也不作页面路由的功能了。目前Andromeda主要包含如下功能:数据结构
本地服务路由,注册本地服务是registerLocalService(Class, Object), 获取本地服务是getLocalService(Class);架构
远程服务路由,注册远程服务是registerRemoteService(Class, Object), 获取远程服务是getRemoteService(Class);app
全局(含全部进程)事件总线, 订阅事件为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就能够搞定。
比较麻烦的是远程服务,要解决如下难题:
让任意两个组件都可以很方便地通讯,即一个组件注册了本身的远程服务,任意一个组件都能轻易调用到
让远程服务的注册和使用像本地服务同样简单,即要实现阻塞调用
不能下降通讯的效率
这里最容易想到的就是对传统的Android IPC通讯方式进行封装,即在bindService()的基础上进行封装,好比ModularizationArchitecture这个开源库中的WideRouter就是这样作的,构架图以下:
这个方案有两个明显的缺陷:
每次IPC都须要通过WideRouter,而后再转发到对应的进程,这样就致使了原本一次IPC能够解决的问题,须要两次IPC解决,而IPC自己就是比较耗时的
因为bindService是异步的,实际上根本作不到真正的阻塞调用
WideConnectService须要存活到最后,这样的话就要求WideConnectService须要在存活周期最长的那个进程中,而如今没法动态配置WideConnectService所在的进程,致使在使用时不方便
考虑到这几个方面,这个方案pass掉。
这是以前一个饿了么同事写的开源框架,它最大的特点就是不须要写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的问题。
设想这样一个场景:在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承担着管理各进程的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呢?
对于耗时操做,咱们直接在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));
在写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目前已经开源,开源地址为https://gitee.com/bettar/Andromeda,欢迎你们star和fork,有任何问题也欢迎你们提issue.