其实 Android 的组件化由来已久,而且已经有了一些不错的方案,特别是在页面跳转这方面,比如阿里的ARouter, 天猫的统跳协议, Airbnb 的DeepinkDispatch, 借助注解来完成页面的注册,从而很巧妙地实现了路由跳转。
但是,尽管像 ARouter 等方案其实也支持接口的路由,然而令人遗憾的是只支持单进程的接口路由。
而目前爱奇艺 App 中,由于复杂的业务场景,导致既有单进程的通信需求,也有跨进程的通信需求,并且还要支持跨进程通信中的 Callback 调用,以及全局的事件总线。
那能不能设计一个方案,做到满足以上需求呢?
这就是 Andromeda 的诞生背景,在确定了以上需求之后,分析论证了很多方案,最终选择了目前的这个方案,在满足要求的同时,还做到了整个进程间通信的阻塞式调用,从而避免了非常 ugly 的异步连接代码。
Andromeda 目前已经开源,开源地址为开源地址为https://github.com/iqiyi/Andromeda.
由于页面跳转已经有完整而成熟的方案,所以 Andromeda 就不再做页面路由的功能了。目前 Andromeda 主要包含以下功能:
注: 这里的服务不是 Android 中四大组件的 Service,而是指提供的接口与实现。为了表示区分,后面的服务均是这个含义,而 Service 则是指 Android 中的组件。
这里为什么需要区分本地服务和远程服务呢?
最重要的一个原因是本地服务的参数和返回值类型不受限制,而远程服务则受 binder 通信的限制。
可以说,Andromeda 的出现为组件化完成了最后一块拼图。
Andromeda 和其他组件间通信方案的对比如下:
这个讨论很有意思,因为有人觉得使用 Event 或 ModuleBean 来作为组件间通信载体的话,就不用每个业务模块定义自己的接口了,调用方式也很统一。
但是这样做的缺陷也很明显:第一,虽然不用定义接口了,但是为了适应各自的业务需求,如果使用 Event 的话,需要定义许多 Event; 如果使用 ModuleBean 的话,需要为每个 ModuleBean 定义许多字段,甚至于即使是让另一方调用一个空方法,也需要创建一个 ModuleBean 对象,这样的消耗是很大的; 而且随着业务增多,这个模块对应的 ModuleBean 中需要定义的字段会越来越多,消耗会越来越大。
第二,代码可读性较差。定义 Event/ModuleBean 的方式不如接口调用那么直观,不利于项目的维护;
第三,正如微信 Android 模块化架构重构实践(上)中说到的那样,"我们理解的协议通信,是指跨平台 /序列化的通信方式,类似终端和服务器间的通信或 restful 这种。现在这种形式在终端内很常见了。协议通信具备一种很强力解耦能力,但也有不可忽视的代价。无论什么形式的通信,所有的协议定义需要让通讯两方都能获知。通常为了方便会在某个公共区域存放所有协议的定义,这情况和 Event 引发的问题有点像。另外,协议如果变化了,两端怎么同步就变得有点复杂,至少要配合一些框架来实现。在一个应用内,这样会不会有点复杂?用起来好像也不那么方便?更何况它究竟解决多少问题呢"。
显然,协议通信用作组件间通信的话太重了,从而导致它应对业务变化时不够灵活。
所以最终决定采用"接口+数据结构"的方式进行组件间通信,对于需要暴露的业务接口和数据结构,放到一个公共的 module 中。
本地服务的路由就不说了,一个 Map 就可以搞定。
比较麻烦的是远程服务,要解决以下难题:
这里最容易想到的就是对传统的 Android IPC 通信方式进行封装,即在 bindService()的基础上进行封装,比如ModularizationArchitecture这个开源库中的 WideRouter 就是这样做的,构架图如下:
这个方案有两个明显的缺陷:
考虑到这几个方面,这个方案 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()实质上是做了以下事情:
整个过程如下所示
所以在这里就需要与 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://github.com/iqiyi/Andromeda.,欢迎大家 star 和 fork,有任何问题也欢迎大家提 issue.
![]() | 1 waruqi 2018-05-30 20:21:48 +08:00 非常不错的框架,赞! |
2 Seasoninthesun OP 谢谢 |
3 PythonAnswer 2018-05-31 01:07:40 +08:00 女神阿。谢谢老板。 |
![]() | 4 caikelun 2018-05-31 17:27:46 +08:00 厉害了!! |