Andromeda:适用于多进程架构的组件通信框架(上)

引言

其实Android的组件化由来已久,而且已经有了一些不错的方案,特别是在页面跳转这方面,比如阿里的ARouter, 天猫的统跳协议, Airbnb的DeepLinkDispatch, 借助注解来完成页面的注册,从而很巧妙地实现了路由跳转。

但是,尽管像ARouter等方案其实也支持接口的路由,然而令人遗憾的是只支持单进程的接口路由

而目前爱奇艺App中,由于复杂的业务场景,导致既有单进程的通信需求,也有跨进程的通信需求,并且还要支持跨进程通信中的Callback调用,以及全局的事件总线

那能不能设计一个方案,做到满足以上需求呢?

这就是Andromeda的诞生背景,在确定了以上需求之后,分析论证了很多方案,最终选择了目前的这个方案,在满足要求的同时,还做到了整个进程间通信的阻塞式调用,从而避免了非常ugly的异步连接代码。

Andromeda的功能

Andromeda目前已经开源,开源地址为开源地址为https://github.com/iqiyi/Andromeda.

由于页面跳转已经有完整而成熟的方案,所以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'即可。

原文发布于微信公众号 - Android群英传(android_heroes)

原文发表时间:2018-05-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linyb极客之路

工作流引擎之activiti6流程节点自由跳转实现

在中国式的流程需求中,诸如驳回,退回功能需要进行流程跳转,比如领导审批不通过,退回到申请人。这种中国式的流程需求,可以通过以下三种方式实现

1.8K3
来自专栏即时通讯技术

新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

Netty 是一个广受欢迎的异步事件驱动的Java开源网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

2.2K3
来自专栏云计算

Go 微服务,第11部分:Hystrix和Resilience

在Go微服务博客系列的这一部分,我们将探讨如何使用Netflix Hystrix的Go实现和go-resilience重试包,使用断路器模式使我们的服务间通信更...

9253
来自专栏Java架构解析

SpringBoot整合RabbitMQ之典型应用场景实战一

RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务...

4070
来自专栏Linyb极客之路

使用lazyInit缩短Spring Boot启动时间

Spring Boot可以进行有助于相关针对项目的设置,包括最常见的默认设置和随时可用的配置,这无疑是很棒的,因为它节省了宝贵的时间 然而,对于框架的新手来说,...

7357
来自专栏友弟技术工作室

PHP相关概念及配置

php MIME,http,html MIME:MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型 ...

42113
来自专栏Java技术栈

分布式服务防雪崩熔断器,Hystrix理论+实战。

Hystrix是什么? hystrix对应的中文名字是“豪猪”,豪猪周身长满了刺,能保护自己不受天敌的伤害,代表了一种防御机制,这与hystrix本身的功能不谋...

4625
来自专栏SeanCheney的专栏

爬虫框架整理汇总

4096
来自专栏JackieZheng

探秘Tomcat(一)——Myeclipse中导入Tomcat源码

前言:有的时候自己不知道自己是井底之蛙,这并没有什么可怕的,因为你只要蜷缩在方寸之间的井里,无数次的生活轨迹无非最终归结还是一个圆形;但是可怕的是有一天你不得...

2078
来自专栏扎心了老铁

Linux Redis集群搭建与集群客户端实现

硬件环境 本文适用的硬件环境如下 Linux版本:CentOS release 6.7 (Final) Redis版本:3.2.1 Redis已经成功安装,安装...

42512

扫码关注云+社区

领取腾讯云代金券