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

提升服务提供方的进程优先级

其实本来Andromeda作为一个提供通信的框架,我并不想做任何提供进程优先级有关的事情,但是根据一些以往的统计数据,为了尽可能地避免在通信过程中出现binderDied问题,至少在通信过程中需要让服务提供方的进程优先级与client端的进程优先级接近,以减少服务提供方进程被杀的概率。

实际上bindService()就做了提升进程优先级的事情。在我的博客bindService过程解析中就分析过,bindService()实质上是做了以下事情:

  • 获取服务提供方的binder
  • client端通过bind操作,让Service所在进程的优先级提高

整个过程如下所示

所以在这里就需要与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

为什么需要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));

InterStellar

在写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.

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

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

Lighttpd1.4.20源码分析之状态机(1)---状态机总览

http://www.cnblogs.com/kernel_hcy/archive/2010/03/24/1694203.html

871
来自专栏ImportSource

像Spring Boot那样创建一个你自己的Starter

如果你所在的公司要开发一个共享的lib,或者如果你想要为开源世界做点贡献,你也许想要开发你自己的自定义的自动配置类以及你自己的starter pom。这些自动配...

3489
来自专栏CSDN技术头条

学 Spring Boot 看这个就够了!

在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。Spring Boot 应用本质上就是一个基于 Spring 框架的应用...

9314
来自专栏JAVA高级架构

Java面试分享(题目+答案)

2563
来自专栏Java成神之路

Spring_总结_02_依赖注入

在上一节中,我们了解了Spring的最根本使命、四大原则、六大模块以及Spring的生态。

814
来自专栏冷冷

Spring 必知概念(二)

13、Spring框架中的单例Beans是线程安全的么? Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要...

2089
来自专栏子勰随笔

Instrumentation框架介绍-Android自动化测试系列(三)

2608
来自专栏扎心了老铁

zookeeper curator使用caches实现各种监听

1、篇首语 curator是zookeeper的一个高级api开发包。封装了zookeeper众多的recipes,并且实现了一些新的recipes原语,最重要...

5325
来自专栏小鄧子的技术博客专栏

【译】加载进阶

首先,从Android的资源文件中加载。你需要提供一个int类型的资源ID,而不是一个String类型的字符串指向网络URL。

943
来自专栏CSDN技术头条

给你一份超详细 Spring Boot 知识清单

在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。Spring Boot 应用本质上就是一个基于 Spring 框架的应用...

5162

扫码关注云+社区

领取腾讯云代金券