Android组件化跨进程通信框架Andromeda解析

好文推荐:

作者:wanderingguy

关于组件化


随着项目结构越来越庞大,模块与模块间的边界逐渐变得不清晰,代码维护越来越困难,甚至编译速度都成为影响开发效率的瓶颈。

组件化拆分是比较常见的解决方案,一方面解决模块间的耦合关系、将通用模块下沉,另一方面做到各模块代码和资源文件的隔离,这样便可以放心进行模块按需编译、单独测试等等。

但随之而来的问题也愈加突出,模块的精细化拆分不可避免的增加了模块间的通信成本。通信的两侧是一个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。
  1. RemoteTransfer请求RemoteServiceTransfer帮忙完成真正的注册。

  2. RemoteServiceTransfer通过第3步获取的DispatcherProxy,做一次IPC通信,将Binder传递到Dispatcher进程。

  3. 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() {}

总结

笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题

觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
(https://img-blog.csdnimg.cn/img_convert/bce539b10451faec3f398488a4381d64.png)

回到Andromeda实现,这个预先插桩的Service如下:

public class CommuStubService extends Service {

public CommuStubService() {}

总结

笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题

[外链图片转存中…(img-XFID8IxW-1714563650541)]

[外链图片转存中…(img-vxBzCVRm-1714563650542)]

[外链图片转存中…(img-BuHXyJZR-1714563650543)]

[外链图片转存中…(img-w8e0tMwk-1714563650544)]

觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值