InterStellar

Introduction: InterStellar 是一个能够适用于多进程的组件通信框架
More: Author   ReportBugs   
Tags:

InterStellar_license InterStellar_core_tag InterStellar_plugin_tag

InterStellar 是一个基于接口的组件间通信方案,包括同进程的本地接口调用和跨进程接口调用。 在 InterStellar 的世界里,不需要任何 aidl 接口及 Service,IPC 通信就和本地通信一样简单、方便。

注:之所以分成本地服务和远程服务这两种,是由于本地服务的接口可以传递各种类型的参数和返回值,而远程接口则受 binder 通信数据类型的限制,参数和返回值只能是基本类型或者实现了 Parcelable 接口的自定义类型。

特色

  • 无需开发者进行 bindService()操作,也不用定义 Service,不需要定义任何 aidl 接口即可实现 IPC 通信

  • 同步获取远程服务。抛弃了 bindService()这种异步获取的方式,改造成了同步获取

  • 生命周期自动管理。可根据 Fragment 或 Activity 的生命周期进行提高或降低服务提供方进程的操作

  • 支持 IPC 的 Callback,并且支持跨进程的事件总线

  • 采用"接口+数据结构"的方式来实现组件间通信,这种方式相比协议的方式在于实现简单,维护方便

注意这里的服务不是 Android 中四大组件的 Service,而是指提供的接口与实现。为了表示区分,后面的服务均是这个含义,而 Service 则是指 Android 中的组件。

InterSteallar 和其他组件间通信方案的对比如下:

使用方便性 代码侵入性 互操作性 是否支持 IPC 是否支持跨进程事件总线 是否支持页面跳转
InterSteallar 较小 Yes Yes No
DDComponentForAndroid 较差 较大 No No Yes
ARouter 较好 较大 No No Yes

接入方式

首先在 buildscript 中添加 classpath:

    classpath "org.qiyi.video.mcg.arch:plugin:$version"

这两个分别是核心代码库和 gradle 插件库的路径。 在 Application 或 library Module 中使用核心库:

    implementation "org.qiyi.video.mcg.arch:core:$version"

在 application Module 中使用 gradle 插件:

    apply plugin: 'org.qiyi.video.mcg.arch.plugin'

使用方式

为 Dispatcher 配置进程

由于 Dispatcher 负责管理所有进程信息,所以它应该运行在存活时间最长的进程中。 如果不进行配置,Dispatcher 默认运行在主进程中。 但是考虑到在有些 App 中,主进程不一定是存活时间最长的(比如音乐播放 App 中往往是播放进程的存活时间最长), 所以出现这种情况时开发者应该在 application module 的 build.gradle 中为 Dispatcher 配置进程名,如下:

    dispatcher{
        process ":downloader"
    }

在这里,":downloader"进程是存活时间最长的.

初始化

最好是在自己进程的 Application 中进行初始化(每个进程都有自己的 InterStellar 对象),代码如下:

    InterStellar.init(Context);

本地服务的注册与使用

本地服务注册

本地服务的注册有两种方法,一种是直接调用接口的全路径名和接口的实现,如下:

    InterStellar.registerLocalService(ICheckApple.class.getCanonicalName(),new CheckApple());

还有一种是调用接口 class 和接口的实现,其实在内部也是获取了它的全路径名,如下:

    InterStellar.registerLocalService(ICheckApple.class,new CheckApple());

其中 ICheckApple.class 为接口,虽然也可以采用下面这种方式注册:

    InterStellar.registerLocalService("wang.imallen.blog.moduleexportlib.apple.ICheckApple",CheckAppleImpl.getInstance());

但是考虑到混淆问题,非常不推荐使用这种方式进行注册,除非双方能够协商一致使用这个 key(因为实际上 InterStellar 只需要保证有一个唯一的 key 与该服务对应即可).

本地服务使用

注册完之后,与服务提供方同进程的任何模块都可以调用该服务,获取服务的方式与注册对应,也有两种方式,一种是通过接口的 class 获取,如下:

    ICheckApple checkApple = InterStellar.getLocalService(ICheckApple.class);

还有一种方法是通过接口的全路径名获取,如下:

    ICheckApple checkApple =  InterStellar.getLocalService(ICheckApple.class.getCanonicalName());

与注册类似,仍然不推荐使用如下方式来获取,除非双方始终协商好使用一个唯一的 key(但是这样对于新的调用方或者新加入的开发者不友好,容易入坑):

    ICheckApple checkApple = InterStellar.getLocalService("wang.imallen.blog.moduleexportlib.apple.ICheckApple");

具体使用,可以察看 applemodule 中 LocalServiceDemo 这个 Activity。

本地接口的 Callback 问题

如果是耗时操作,由本地接口自己定义 Callback 接口,调用方在调用接口时传入 Callback 对象即可。

远程服务的注册与使用

远程服务的注册与使用略微麻烦一点,因为需要像实现 AIDL Service 那样定义 aidl 接口。

远程接口的定义与实现

依靠@in,@out,@inout,@oneway 这 4 个注解定义可进行 IPC 通信的接口,这四个注解分别对应于 aidl 中的 in,out,inout 和 oneway 修饰符。 函数参数不添加注解的话,默认为@in. 如下是一个典型的接口定义:

   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);

   }

而接口的实现跟普通接口基本一样,除了要为@out 和@inout 的参数赋值之外:

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";
    }
}

远程服务的注册

与本地接口的注册略有不同,远程接口注册的是继承了 Stub 类的 IBinder 部分,注册方式有传递接口 Class 和接口全路径名两种,如下:

    InterStellar.registerRemoteService(IAppleService.class,new AppleService());

远程服务的使用

  • 由于 InterStellar 利用 bindService()来提升通信过程中的优先级,对于在 Fragment 或者 Activity 中使用的情形,可在 onDestroy()时自动释放连接,所以需要调用先调用 with();
  • 由于跨进程只能传递 IBinder,所以只能获取到远程服务的 IBinder 之后,再调用 XX.Stub.asInterface()获取到它的代理.

以 FragmentActivity 中使用为例,如下的 this 是指 FragmentActivity:

        IAppleService appleService = InterStellar.with(BananaActivity.this).getRemoteService(IAppleService.class);
        if (appleService != null) {
            int appleNum = appleService.getApple(120);
            Logger.d("appleNum:" + appleNum);
            float calories = appleService.getAppleCalories(30);
            Logger.d("calories:" + calories);

            Apple apple = new Apple(1.3f, "Guangzhou");
            String desc = appleService.outTest1(apple);
            Logger.d("now apple:" + apple.toString());

        }

其他的在 android.app.Fragment,android.support.v4.app.Fragment,以及普通的 Activity 中远程服务的使用类似,可查看 app module 中的 CustomFragment,CustomSupportFragment,FragActivity 等,不再赘述。

值得注意的是,远程服务其实既可在其他进程中调用,也可以在同进程中被调用,当在同一进程时,虽然调用方式一样,但其实会自动降级为进程内普通的接口调用,这个 binder 会自动处理.

生命周期自动管理的问题

对于 IPC,为了提高对方进程的优先极,在使用 InterStellar.with().getRemoteService()时会进行 bindService()操作。 既然进行了 bind 操作,那自然要进行 unbind 操作以释放连接了,目前有如下两种情形。

  • 对于在 Fragment 或者 Activity 中,并且是在主线程中调用的情形,只要在获取远程服务时利用 with()传递 Fragment 或者 Activity 对象进去,InterStellar 就会在 onDestroy()时自动释放连接,不需要开发者做任何 unbind()操作。

  • 对于在子线程或者不是 Fragment/Activity 中的情形,只能传递 Application Context 去获取远程服务。在使用完毕后,需要手动调用 InterStellar 的 unbind()释放连接:

    public static void unbind(Class<?> serviceClass);
    public static void unbind(Set<Class<?>> serviceClasses);

如果只获取了一个远程服务,那么就使用前一个 unbind()方法;否则使用后一个。

License

BSD-3-Clause. See the BSD-3-Clause file for details.

TODO List

  • 远程服务的 Callback
  • 事件总线

支持

  1. Sample 代码
  2. 阅读 Wiki 或者 FAQ
  3. 联系 bettarwang@gmail.com
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools