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

关于组件化

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

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

但随之而来的问题也越发突出,模块的精细化拆分不可避免的增长了模块间的通讯成本。通讯的两侧是一个C/S架构,若是服务端与客户端同属一个进程咱们称之为本地服务,若是分属不一样进程称之为远程服务。注意这里的服务不只限于Android中的Service组件,而是一种能够对外提供功能或数据的能力。git

对于同进程的通讯比较简单,经过注册本地接口和实现就能够完成,若是你已经接入ARouter,直接声明服务类继承IProvider+Router注解就完成了服务的注册。github

可是对于跨进程的通讯就比较复杂了,在Android系统中IPC通讯经过Binder实现,对参与通讯的数据格式作了限制,也就是基本数据类型或者实现Parcelable接口的类型。数据库

多进程的好处是能够占用更多的系统资源,而且独立核心进程能够免受非核心业务出现异常状况致使整个APP崩溃不可用。缓存

跨进程通讯业务场景比较复杂,既要保证服务端的可靠性,还须要支持callback,一般Service是首选。bash

基于Service的IPC通讯

咱们回想一下是如何使用Service进行跨进程通讯的。架构

  1. 声明提供服务的AIDL接口。
  2. 建立Service,并在onBind方法返回实现Stub接口的Binder对象。
  3. Client端经过intent bindService,并传入ServiceConnection对象,在onServiceConnected回调获取Service提供的Binder对象。

本质上是将Binder对象(准确的说是代理对象)在进程间进行传递,而Service只是一个载体。app

在组件化的大业务背景下,模块间的通讯接口数量可能不少,按这套方案会有不少问题。框架

  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架构图

arch.jpeg

RemoteTransfer

上面提到各个进程本身自己也须要管理(缓存)从Dispatcher获取的Binder,防止重复的IPC请求;另外因为事件总线的需求,各个进程须要向Dispatcher进程注册本进程组件管理员,这样当事件pubish后,Dispatcher才能将事件发送给各个进程,这个各个进程管理员就是RemoteTransfer。

IRemoteTransfer是一个AIDL接口,RemoteTransfer是它的实现类,RemoteTransfer还实现了IRemoteServiceTransfer接口。

这里须要一张类图来帮你理清思路:

remote_class.jpeg

#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属于远程服务所在进程。

本地服务

本地服务没什么讲的,内部经过维护一个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);
    }
}
复制代码

远程服务

远程服务是框架的核心,对远程服务的操做就是两个,一是注册远程服务,二是获取远程服务。

咱们先来看服务的注册,时序图以下 ↓

Andromeda_register (2).jpg

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

图中蓝色的节点表示注册服务的当前进程,红色节点表示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());
    }
}
复制代码

接下来咱们看服务的获取,一样的先看时序图 ↓

Android_getRemoteService.jpg

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返回给调用方。

一样图中蓝色的节点表示获取服务的进程,红色节点表示Dispatcher进程。

至此,远程服务的注册与获取流程分析结束。

进程优先级

上面提到在获取远程服务时,框架作了提高进程优先级的事情。一般状况下使用远程服务的端(简称Client端)处于前台进程,而Server端进程已经注册完毕,每每处于后台。为了提高Server端的稳定性,最好能将Server端的进程优先级与Client保持接近,不然容易出现被LMK(Low Memory Killer)回收的状况。

那如何提高Server端进程的优先级呢?这里的作法是用前台的UI组件(Activity/Fragment/View)bind一个Server端预先插桩好的Service。

整套流程最终经过AMS的updateOomAdjLocked方法实现。

提高进程Adj.jpeg

回到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过程发生在上一章节获取远程服务时,流程以下图:

提高进程优先级流程.jpg

图中模块根据所在进程分为三部分:

  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核心的原理咱们就分析完了,虽然有些问题有待完善,但已经给咱们提供了不少优秀的解决问题的思路,不管是继续优化仍是精简一下本地化都是不错的选择。

相关文章
相关标签/搜索