AIDL之进程间共享单例

作者: juexingzhe | 来源:发表于2017-05-28 14:09 被阅读149次

    今天是端午节放假第一天,祝各位小伙伴端午快乐!今天我也给大家送个“粽子”-AIDL之进程间共享单例。关于AIDL的简单使用本文不会涉及太多,前面我们有两篇陆续聊了Android AIDL的相关姿势,有需要的小伙伴可以回头看下:
    AIDL与Binder浅析http://www.jianshu.com/p/4ebd4783d3d9
    IPC之Messengerhttp://www.jianshu.com/p/328605278a93

    本文会涉及到大概下面这些姿势:

    1.进程间通信AIDL的使用步骤
    2.AIDL的实现原理
    3.单例模式
    4.工厂模式
    5.模板模式

    为什么要写这样一个主题?有时候我们项目中会有这样一个需求,一个应用中有两个进程A和B,进程A中有一些配置放在单例Singleton,进程B需要去拿配置,有哪些方式可以实现呢?一种是序列化的方式,比如写到一个文件,或者写到数据库中;另外一种就是通过Binder的方式,也就是我们今天要说的这种方式。序列化的方式操作会繁琐一点,需要一堆的写入和读出的操作,Binder的方式会更灵活一点。

    我们还是通过一个简单的栗子来看下怎么实现的。分成A和B两个进程,A进程和B进程分别有自己的单例Singleton,A进程可以通过B进程的单例调用单例的方法,反过来B进程也可以通过A进程的单例调用单例的方法,操作起来很方便,我们在进程单例中有两个方法和一个成员变量count,方法increment用于递增count,方法getcount用于获取count的值。


    1.png 2.png 3.png

    1.使用方式

    使用起来很简单,先定义两个单例的AIDL文件,因为需要跨进程传输,所以需要AIDL文件。两个单例拥有同样的两个方法,increament中参数是调用方法的当前进程的名称。

    interface ISingletonMain {
        void increment(String currentPorcessName);
        int getCount();
    }
    
    interface ISingletonB {
        void increment(String currentPorcessName);
        int getCount();
    }
    

    当然,为了拿到对方进程的单例,是需要去绑定服务的,在Main进程中绑定B进程的服务:

    bindService(ServiceB.class);
    

    B进程绑定Main进程:

    bindService(MainService.class);
    

    使用起来就直接使用单例的调用方式就可以,比如调用Main进程单例的方法:

    SingletonMainImpl.getInstance().increment(Utils.currentProcessName());
    

    调用B进程单例的方法也是类似:

    SingletonBImpl.getInstance().increment(Utils.currentProcessName());
    

    就是这样,使用起来就是一句代码搞定。

    2. 原理解析

    使用起来很方便,我们来看看背后是怎么实现的。首先应该有个共识,为了共享单例,进程间需要通过Binder通信来拿到对方的实例。为了拿到这个对方的实例,总结起来是这么几个步骤:

    1.定义进程间通信的接口。调用这个接口的方法可以拿到对方的单例;
    2.绑定服务。A绑定B进程的服务,B进程会生成这个单例,封装成一个Binder,通过Binder池回传一个Binder到A进程;
    3.绑定服务成功。成功时,A可以拿到B传送过来的Binder,通过Binder可以取出这个单例;
    4.调用单例方法。拿到单例后调用单例接口aidl文件中的方法时会发起IPC调用,在APP层就跟调用本进程单例一样。

    我们对应着上面的步骤来看看幕后的实现。

    1.定义进程间通信的接口

    先来总体看下有多少AIDL接口,先大体介绍下每个接口:

    1.IInstanceTransfer.aidl就是Main进程和B进程通信的接口,返回的是进程实例的工厂InstanceFactory
    2.实例工厂InstanceFactory为了在AIDL中通过Binder传输也需要定义一个InstanceFactory.aidl文件,并且要实现Parcelable接口;

    public class InstanceFactory implements Parcelable
    

    3.剩下两个就是需要传输的进程实例的aidl文件,里面定义了提供跨进程调用的方法:

    interface ISingletonMain {
        void increment(String currentPorcessName);
        int getCount();
    }
    
    4.png

    我们定义一个获取单例的接口,返回的InstanceFactory是一个“工厂”,这又是什么套路?

    interface IInstanceTransfer {
        InstanceFactory transfer();
    }
    

    我们首先简单讲下工厂模式,比如我现在需要一辆轿车,但是我不可能自己去造一辆车,如果有这么一个车厂Factory,我直接给Factory下个指令“我需要一辆轿车”,Factory就直接造出一辆轿车返回给我,这样是不是很方便,同样另外一个小伙伴B需要SUV,直接给Factory下个指令“我需要一辆SUV”,就能拿到Factory的SUV。
    代码直接看我们下面的栗子,我们现在需要Main进程和B进程的单例(至于什么是单例模式我们后面结合代码具体看),那么我们直接给InstanceFactory这个工厂下个指令就行,这样很方便客户的使用。
    如果是Main进程,InstanceFactory就造一个Mian进程的单例;如果是B进程,InstanceFactory就造一个B进程的单例。writeStrongInterface方法就是将实例封装成Binder,用于AIDL。

    if (Utils.isMainProcess()) {
            dest.writeStrongInterface(SingletonMainImpl.getInstance());
    } else if (Utils.isBProcess()) {
            dest.writeStrongInterface(SingletonBImpl.getInstance());
    }
    

    2.绑定服务

    定义好接口,接着就是绑定服务,APP层其实就是bindService。两个进程都有对应的Service,Main进程对应MainService,B进程对应ServiceB。要实现的东西其实都是一样的,就是返回IInstanceTransfer的实现即可,因此我们将使用另外一种设计模式,模板模式来抽取下代码的实现。我们先拐个弯看下模板模式,讲的会比较简单点,有需要的小伙伴可以去看下设计模式的相关资料。
    模板模式其实和继承有很大的关系,就是我们将多个类共性的方法抽取出来放到父类中,每个子类需要个性化实现的就在父类中留个hook给子类自己去实现,但是方法的执行过程其实是在父类的控制中,简单举个栗子,在hookMethod方法的前后可以实现一些其他的工作,我们这里只是简单的打印log。在外面调用TemplateClass的TemplateMethod就可以实现调用同一个模板方法但是有不同实现的效果。

    public abstract class TemplateClass {
        public void TemplateMethod(){
            Log.d(TemplateClass.class.toString(), "This is before hookMethod");
            hookMethod();
            Log.d(TemplateClass.class.toString(), "This is after hookMethod");
        }
    
        public abstract void hookMethod();
    }
    

    我们看下Main进程绑定服务的过程,两个进程共同的服务我们抽取出来一个BaseService。

    bindService(ServiceB.class);//绑定B进程的服务
    
    public class BaseService extends Service {
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return new InstanceTransferImpl() ;
        }
    }
    

    服务返回的就是AIDL接口IInstanceTransfer的实现,这个我们就不展开讲了,有需要的小伙伴自行参考:http://www.jianshu.com/p/4ebd4783d3d9

    public class InstanceTransferImpl extends IInstanceTransfer.Stub {
        @Override
        public InstanceFactory transfer() throws RemoteException {
            return new InstanceFactory();
        }
    
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            try{
                return super.onTransact(code, data, reply, flags);
            }catch (RuntimeException e){
                Log.i("InstanceTransferImpl", "Unexpected exception" + e);
                throw  e;
            }
        }
    }
    

    可以看出,调用transfer方法就是返回InstanceFactory。

    3.绑定服务成功

    绑定服务成功Main进程就能在ServiceConnection中拿到B进程的Binder,其实B进程在绑定Main进程成功时也是要这个步骤,而我们把实例Binder的工作都封装到了InstanceFactory中,因此我们可以用模板模式抽取一个ServiceConnection。

    public class ServiceConnectionImpl implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                InstanceTransferImpl.asInterface(service).transfer();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }
    

    这样不管Main进程绑定B进程服务还是B进程绑定Main进程服务都可以使用这个ServiceConnection,还是很方便的。
    我们来重点看下绑定服务成功后是怎么拿到单例的。全都浓缩在这句代码里了:

    InstanceTransferImpl.asInterface(service).transfer();
    

    跨进程所以我们是拿到InstanceTransferImpl的代理,调用的自然也是代理的transfer,我们分析下这个流程:

    1.在代理的transfer()方法中会调用

    mRemote.transact(Stub.TRANSACTION_transfer, _data, _reply, 0);
    

    2.来到Stub的onTransact方法中:

    com.example.juexingzhe.processshareinstance.InstanceFactory _result = this.transfer();
    reply.writeNoException();
    if ((_result != null)) {
          reply.writeInt(1);
          _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
    } else {
         reply.writeInt(0);
    }
    

    this当然指的就是B进程,所以调用到B进程的transfer方法,得到InstanceFactor。
    3.调用InstanceFactory:
    接着会调用 InstanceFactory.writeToParcel,判断是B进程,会在reply中写入信息,我们这边主要进行三个工作,写入B进程标识;写入B进程总的方法调用的统计数字;写入B进程的实例。后面这些信息都可以在Main进程中读出。

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            if (Utils.isMainProcess()) {
                dest.writeInt(PROCESS_MAIN);
                dest.writeInt(SingletonMainImpl.COUNT);
                dest.writeStrongInterface(SingletonMainImpl.getInstance());
            } else if (Utils.isBProcess()) {
                dest.writeInt(PROCESS_B);
                dest.writeInt(SingletonBImpl.COUNT);
                dest.writeStrongInterface(SingletonBImpl.getInstance());
            }
        }
    

    4.再回到代理的transfer方法中,会调用InstanceFactory.CREATOR

    _result = com.example.juexingzhe.processshareinstance.InstanceFactory.CREATOR.createFromParcel(_reply);
    

    5.InstanceFactory中读出B进程实例
    根据进程标识赋值 SingletonBImpl.INSTANCE

    public InstanceFactory createFromParcel(Parcel in) {
                return new InstanceFactory(in);
    }
    public InstanceFactory(Parcel in) {
            int processId = in.readInt();
            switch (processId) {
                case PROCESS_B:
                    SingletonBImpl.COUNT = in.readInt();
                    SingletonBImpl.INSTANCE = ISingletonB.Stub.asInterface(in.readStrongBinder());
                    break;
            }
    }
    

    进过上面过程我们就可以在Main进程拿到B的实例了,但是要注意一点,Main进程拿到的SingletonBImpl.INSTANCE和B进程的实例其实不是真正的同一个Object,因为是跨进程通过Binder池进行传送,但是因为每个Binder都有标识符,所以底层能做出正确的识别。为什么说不是同一个Object,我们可以看个截图。在进程Main中获取单例就是ISingletonMainImpl,这个没什么疑问;但是在进程B中获取的进程Main的单例是ISingletonMainImpl$Stub$Proxy,也就是跨进程的单例代理。从两个实例的mObject也可以看出来不是同一个对象。

    5.png 6.png

    4.调用单例方法

    这个其实就很简单了,就跟这个单例就在Main进程一样。

    SingletonBImpl.getInstance().increment(Utils.currentProcessName());
    SingletonBImpl.getInstance().getCount()
    

    原理也是通过代理跨进程进行调用,我们以getCount方法的调用为例:

    1.通过代理调用到getCount方法:

    mRemote.transact(Stub.TRANSACTION_getCount, _data, _reply, 0);
    

    2.调用到B进程单例的getCount方法,在onTransact方法中, 将结果保存到reply中。

     case TRANSACTION_getCount: {
            data.enforceInterface(DESCRIPTOR);
            int _result = this.getCount();
            reply.writeNoException();
            reply.writeInt(_result);
            return true;
    }
    

    3.再回到代理的transact方法中,从reply中读出结果并返回。

    _result = _reply.readInt();
    

    最后我们看看B进程实例的实现,使用的就是单例模式。关键点就是构造方法是私有的,不允许外面实例化这个单例,只能通过静态方法getInstance获取实例。SingletonBImpl两个方法实现都比较简单没什么可说的,主要是getInstance()方法。主要是B进程那么直接就返回SingletonBImpl,如果是A进程会自动重连。

    public class SingletonBImpl extends ISingletonB.Stub {
    
        public static ISingletonB INSTANCE;
    
        public static int COUNT = 0;
    
        private SingletonBImpl() {
        }
    
        public static ISingletonB getInstance() {
    
            if (null == INSTANCE) {
                synchronized (SingletonBImpl.class) {
                    if (null == INSTANCE) {
                        if (Utils.isBProcess()) {
                            INSTANCE = new SingletonBImpl();
                        } else {
                            Context context = MyApplication.getContext();
                            Intent intent = new Intent(context, ServiceB.class);
                            context.bindService(intent, new ServiceConnectionImpl(), Context.BIND_AUTO_CREATE);
                        }
                    }
                }
            }
    
            return INSTANCE;
        }
    
    
        @Override
        public void increment(String currentPorcessName) throws RemoteException {
            COUNT++;
            StringBuilder stringBuilder = new StringBuilder("进程:" + currentPorcessName + "调用" + Utils.currentProcessName() + "的方法increment");
            Utils.showToast(stringBuilder.toString());
        }
    
    
        @Override
        public int getCount() {
            return COUNT;
        }
    }
    

    2.总结

    今天我们这篇文章的AIDL接口会比较多,主要是用到了几个简单的设计模式,可以方便的扩展。主要有进程通信的接口,还有“工厂”的接口,另外两个就是进程单例的接口。为了实现两个进程共享单例,我们是通过AIDL接口IInstanceTransfer获取一个“工厂”,工厂会自动根据当前的进程实例化进程实例,通过Binder传输这个单例,在跨进程拿到这个单例后就可以调用进程单例AIDL接口定义的方法来实现各种操作。要注意的一点就是拿到单例后也是通过跨进程AIDL进行类单例调用的,这就是为什么文章前面需要定义两个进程单例AIDL接口的原因。还是那句话,建议结合前面AIDL入门的文章看会比较好点。
    文章链接:http://www.jianshu.com/p/4ebd4783d3d9
    本文栗子也已经上传Github:https://github.com/juexingzhe/ProcessShareInstance

    完结,谢谢!

    参考链接:
    http://www.jianshu.com/p/4ebd4783d3d9
    https://toutiao.io/posts/6yvvo2/preview

    欢迎关注公众号:JueCode

    相关文章

      网友评论

        本文标题:AIDL之进程间共享单例

        本文链接:https://www.haomeiwen.com/subject/xntcfxtx.html