美文网首页
replugin宿主与插件通信小结

replugin宿主与插件通信小结

作者: 神迹12 | 来源:发表于2018-12-09 21:40 被阅读0次

    近来replugin开发中遇到宿主和插件间需要通信的情形,思来只有进程间通信(IPC)才是比较好的宿主与插件的通信方式。而Android进程间通信主要有2种方式:Messenger和AIDL。

    AIDL(Android Interface Definition Language)是Android接口定义语言。AIDL设计用于实现Android进程间通信。Android中每个进程都拥有自己的Dalvik虚拟机(4.4之后是ART虚拟机),以及独立的内存区域,通过AIDL制定的规则,使不同进程可以进行数据交流。

    宿主与插件间通信又可细分为,插件主动向宿主发起通信以及宿主主动向插件发起通信2种情况。为了模拟这2中可能的情况,同时对加深对Messenger和AIDL的理解。在宿主和插件中分别使用这两种方式,宿主作为服务端使用Messenger通信,以及插件作为服务端使用AIDL进行通信,分别查看2种方式的使用效果。

    一、使用Messenger

    1.1 Messenger服务端
    1)服务端定义一个Service
    <service android:name=".PluginComService"    android:enabled="true"    android:exported="true">  
    <intent-filter>        
    <action android:name="com.hk.daijun.messengerclient"></action>        
    <category android:name="android.intent.category.DEFAULT" /> 
    </intent-filter>
    

    在清单文件中配置Service,exported要配置为true,否则别的进程不能绑定该Service,在intent-filter中设置action。

    2) 创建Messenger和Handler实例

    创建Messenger和Handler实例,通过Handler来处理消息。

    private static class MessengerHandler extends Handler {    
    @Override    public void handleMessage(Message msg) {        Log.e(TAG, "handleMessage: what="+msg.what); 
      switch (msg.what) {            case 1001:                
      Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msg"));                
      Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msgJson"));                
      String msgJson = msg.getData().getString("msgJson");               
      Gson gson = new Gson();                
      PushMsgContent pushMsgContent = gson.fromJson(msgJson,PushMsgContent.class);         
      if (pushMsgContent != null) {                    
        Log.e(TAG, "handleMessage: PushMsgContent="+pushMsgContent.sendTime+"  "+pushMsgContent.msgContent.alarmTime); 
      }                
      Messenger mesgerReply = msg.replyTo;                
      Message msgReply = Message.obtain();                
      Bundle bundle = new Bundle();                
      bundle.putString("service_reply","hello,服务端已收到信息,想做什么");                
      msgReply.what = 2001;                
      msgReply.setData(bundle); 
      try {                    
        mesgerReply.send(msgReply);                
      } catch (RemoteException e) {                    
        Log.e(TAG, "handleMessage: 消息发给客户端,发送失败");                   
       e.printStackTrace();                
    }                
    break; 
    }        
    super.handleMessage(msg);    
    }}
    private Messenger messenger = new Messenger(new MessengerHandler());
    
    

    消息传递时可以考虑使用字符串,然后服务端和客户端都用Gson进行解析,因为宿主是第三方开发,插件是自己开发,省去频繁变更接口的麻烦。

    3)重写onBind方法

    在Service的onBind方法中,用Messenger创建一个IBinder,Service将IBinder在onBind方法中返回给客户端。

    1.2 Messenger客户端
    1)创建ServiceConnection,绑定服务

    客户端创建ServiceConnection实例,绑定服务端第1步中的Service,action要与服务端app中AndroidManifest中的一致,包名(服务端app包名,即AndroidManifest中查看到的包名)也要一致。

    private void connectToService(){        
      Intent intent = new Intent();        
      intent.setAction("com.hk.daijun.messengerclient");  
      intent.setPackage("com.hk.daijun.replugintest");//        
      bindService(intent,serviceConnection,BIND_AUTO_CREATE);  
      RePlugin.getHostContext().bindService(intent,serviceConnection,BIND_AUTO_CREATE);    
    }
    

    此处需要主要要用宿主的上下文来绑定服务,如果直接用bindService来启动服务,能启动服务,但解绑时会报错服务未注册。

    2)实例化Messenger

    服务绑定后,在ServiceConnection中的onServiceConnected方法中,用IBinder来实例化Messenger(引用服务端的Handler),用此Messenger来向服务端发送消息。

    3)接收服务端回复

    若客户端还想从服务端那边收到回复,则需要再定义一个Messenger,messengerAccept来,在给服务端发送消息时,在Message的replyTo字段中赋值为messengerAccept。

    public void sendMessageToPlugin(){    
        Log.e(TAG, "sendMessageToHost: ");    
        Message msg = Message.obtain();    
        msg.what = 1001;    
        msg.arg1 = 1001;    
        msg.replyTo = mesgerAccept;    
        Bundle bundle = new Bundle();    
        bundle.putString("msg","我要与宿主通信");          
        bundle.putString("msgJson",getJsonString());      
        msg.setData(bundle);    
        try {        
            mesgerSend.send(msg);    
        } catch (RemoteException e) {        
            e.printStackTrace();        
            Log.e(TAG, "sendMessageToHost:消息发送异常"+e.getMessage());    
        }
    }
    
    

    接收服务端的回复,创建的Messenger实例mesgerAccept处理服务端的回复。

    private void initAcceptMessenger() {    
    mesgerAccept = new Messenger(new Handler(){  
        @Override        
        public void handleMessage(Message msg) {            
            switch (msg.what) {                
            case 2001:                    
            Log.e(TAG, "handleMessage: 收到服务端的回复");                    
            Log.e(TAG, "handleMessage: 
            service_reply="+msg.getData().getString("service_reply"));                    
            break;            
          }            
          super.handleMessage(msg);        
        }    
      });
    }
    
    4) 实现效果

    首先在宿主app中进入插件,然后在插件中绑定宿主Service,然后发送消息。


    messenger效果1.png messenger通信记录.PNG

    二、使用AIDL

    AIDL是一个缩写,全称是Android Interface Definition Language,要使用AIDL,必须创建一个定义编程接口的.aidl文件。AIDL文件可以分为两种。一种是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。

    AIDL支持的数据类型,包括Java中八种基本数据类型:byte、short、int、long、double、boolean、char,以及String、List、Map,集合里边的所有元素也必须是AIDL支持的数据类型(包括支持的Java数据类型以及自定义的数据类型)。List可以使用泛型、Map不支持泛型。

    自定义数据类型,必须实现parcelable接口,还要注意定向tag。定向tag包括三种:in、out、inout。AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。一般通常in就够了。

    2.1 客户端(宿主)
    1)新建自定义数据类型java bean
    java bean.png

    其中writeToParcel方法即表示支持定向tag为in,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel()方法,此方法不在Parcelable接口中,需要自己手写。

    可以看到自定义的数据类型要实现parcelable序列化接口。因为进程是系统进行资源分配和调度的基本单位,不同的进程有不同的内存区域,java中传递对象一般是传递对象在内存堆上的引用,而由于不同进程间不能访问各自的内存区域,所以传递的引用对方进程是访问不到的。所以要将传递的数据进行序列化。在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,还原其中包含的数据,从而实现进程间传递数据的目的。

    2)增加aidl文件目录

    在project视图下,在main目录下新建aidl目录,然后新建package,包名与AlarmMessage的包名路径一致。

    添加aidl文件1.PNG
    3)添加aidl文件
    添加aidl文件2.PNG

    添加后,aidl文件没在bean目录下,aidl的包名要和对应的javabean包名一致,手动移动aidl文件到bean包名下,然后修改包名,声明序列化类AlarmMessage。

    添加aidl文件3.PNG 添加aidl文件5.PNG
    4)增加aidl接口文件

    增加aidl接口文件内容,这个文件里面主要内容是进程间通信的接口。注意要导入自定义数据类型的包。AIDL默认支持一些数据类型,在使用这些数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下。

    添加aidl文件4_1.PNG

    配置完成后,将Module plugin1进行build,就会生成AlarmMessageMgr.java文件。


    aidl接口生成的JAVA文件.PNG
    2.2 服务端(插件)

    以上都是插件工程中的配置,现在要在宿主工程中也进行相应配置。配置过程也是几乎一样,需要将java文件和aidl文件都从插件工程中复制到宿主工程中,并且包路径要一样。

    宿主aidl文件配置.PNG

    在将aidl文件和java bean复制到宿主工程中后。在插件中新建Service类,此次通信以插件作为服务端,而宿主app作为客户端,宿主app主动发起通信。插件中新建Service,其中AlarmMessage.Stub就是aidl文件写完后,对插件工程进行重新编译后产生java文件,在onBind中返回给客户端,客户端据此来进行通信。

    public class PluginAidlService extends Service {
        private static final String TAG = "PluginAidlService";
        private List<AlarmMessage> mMsgList = new ArrayList<>();
        private final AlarmMessageMgr.Stub mAlarmMgr = new AlarmMessageMgr.Stub() {
            @Override
            public List<AlarmMessage> getAlarmMessages() throws RemoteException {
                return mMsgList;
            }
            @Override
            public void addAlarmMessage(AlarmMessage msg) throws RemoteException {
                Log.e(TAG, "plugin service addAlarmMessage: ");
                synchronized (this) {
                    if (mMsgList == null) {
                        mMsgList = new ArrayList<>();
                    }
                    if (msg == null) {
                        Log.e(TAG, "addAlarmMessage: msg to add is null");
                        msg = new AlarmMessage();
                        msg.alarmId = 0;
                        msg.alarmTime = "1997-10-7 12:00:00";
                        msg.alarmTitle = "空报警";
                    }
                    mMsgList.add(msg);
                }
                int size = mMsgList.size();
                for (int i=0;i<size;i++) {
                    AlarmMessage alarmMessage = mMsgList.get(i);
                    Log.e(TAG, "aidl plugin1 : "+String.format("alarmTitle=%s alarmTime=%s",alarmMessage.alarmTitle,alarmMessage.alarmTime));
                }
            }
        };
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            Log.e(TAG, "onBind: ");
            return mAlarmMgr;
        }
    

    而客户端即宿主app通过绑定插件中的PluginAidlService服务来进行通信,需要注意的是,由于这里涉及到宿主app启动插件中的四大组件(Activity、Service、BroadcastReceiver、ContentProvider),所以不能直接使用原生的api来启动插件中的Service,需要调用Replugin中的api来启动。

        private void connectToService(){
            Log.e(TAG, "connectToService: ");
            Intent intent1 = RePlugin.createIntent("plugin1","com.hk.daijun.plugin1.PluginAidlService");
            intent1.setAction("com.hk.daijun.replugintest");
            intent1.setPackage("com.hk.daijun.plugin1");
            try {
                PluginServiceClient.bindService(PluginCommunicateActivity.this, intent1, serviceConnection, BIND_AUTO_CREATE);
            } catch (PluginClientHelper.ShouldCallSystem e) {
                Log.e(TAG, "connectToService: "+e.getMessage());
            }
        }
    

    同样,通过bindService()来启动插件中的服务,取消绑定也需要用Replugin的api来进行Service解绑。

    PluginServiceClient.unbindService(PluginCommunicateActivity.this,serviceConnection);
    

    客户端在Service连接建立后,调用AlarmMessageMgr.Stub.asInterface(service)将服务端返回的Binder对象转换成AIDL接口类型的对象,进而可以调用aidl定义的接口来进行调用服务端的远程方法。

    private void initServiceConnection(){
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.e(TAG, "aidlhost onServiceConnected: ");
                mAlarmMgr = AlarmMessageMgr.Stub.asInterface(service);
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.e(TAG, "aidlhost onServiceDisconnected: ");
            }
        };
    }
    

    插件重新build后,宿主以内置模式接入插件,需要将宿主工程后先clean下,清除build目录,再重新编译安装,否则宿主不先clean的话,插件的修改可能并不生效!

    实现效果如下,在宿主中发送报警信息,插件中收到信息后,将所有报警信息打印出来。宿主app中主要是2个按钮,一个绑定插件服务,一个发送消息。


    aidl宿主界面1.PNG
    宿主aidl发消息插件收到.PNG

    还可以模拟插件Service处理完一些逻辑后,如进行网络请求,获取第三方系统账户信息等操作,执行成功后再启动插件中的Activity,需要注意的是,在Service中启动Activity,必须设置FLAG_ACTIVITY_NEW_TASK标签,但FLAG_ACTIVITY_NEW_TASK标签必须配合taskAffinity属性使用,如果不设置taskAffinity属性值,将不会生成新task。所以不设置taskAffinity就不会生成新的task。

    三、 AIDL原理

    掠下aidl实现进程间通信的主要原理。AIDL的底层是Binder,其本质就是为我们提供了一种快速实现Binder的工具,通过定义AIDL,android studio编译后生成了AlarmMessageMgr.java文件,而原理的关键也就是这个文件,这个文件其实也可以通过手动编写。

    public static com.hk.daijun.plugin1.AlarmMessageMgr asInterface(android.os.IBinder obj)
    {
    if ((obj==null)) {
    return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin!=null)&&(iin instanceof com.hk.daijun.plugin1.AlarmMessageMgr))) {
    return ((com.hk.daijun.plugin1.AlarmMessageMgr)iin);
    }
    return new com.hk.daijun.plugin1.AlarmMessageMgr.Stub.Proxy(obj);
    }
    

    asInteface方法用于将服务器的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么返回服务器的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

    然后看下addAlarmMessage(AlarmMessage msg)方法的调用过程,转到方法的实现。

    @Override public void addAlarmMessage(com.hk.daijun.plugin1.bean.AlarmMessage msg) throws android.os.RemoteException
    {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
    _data.writeInterfaceToken(DESCRIPTOR);
    if ((msg!=null)) {
    _data.writeInt(1);
    msg.writeToParcel(_data, 0);
    }
    else {
    _data.writeInt(0);
    }
    mRemote.transact(Stub.TRANSACTION_addAlarmMessage, _data, _reply, 0);
    _reply.readException();
    }
    finally {
    _reply.recycle();
    _data.recycle();
    }
    }
    

    这个方法由客户端调用,运行在客户端,输入的参数msg定向tag是in,代表数据只能由客户端流向服务端。参数信息写入_data中,然后调用transact方法发起RPC(远程过程调用),mRemote就是Binder对象在binder驱动层对应的引用。transact方法调启后,客户端当前线程被挂起,服务端的onTransact方法会被调用。

    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
    switch (code)
    {
    case INTERFACE_TRANSACTION:
    {
    reply.writeString(DESCRIPTOR);
    return true;
    }
    case TRANSACTION_getAlarmMessages:
    {
    data.enforceInterface(DESCRIPTOR);
    java.util.List<com.hk.daijun.plugin1.bean.AlarmMessage> _result = this.getAlarmMessages();
    reply.writeNoException();
    reply.writeTypedList(_result);
    return true;
    }
    case TRANSACTION_addAlarmMessage:
    {
    data.enforceInterface(DESCRIPTOR);
    com.hk.daijun.plugin1.bean.AlarmMessage _arg0;
    if ((0!=data.readInt())) {
    _arg0 = com.hk.daijun.plugin1.bean.AlarmMessage.CREATOR.createFromParcel(data);
    }
    else {
    _arg0 = null;
    }
    this.addAlarmMessage(_arg0);
    reply.writeNoException();
    return true;
    }
    }
    return super.onTransact(code, data, reply, flags);
    }
    

    onTransact方法中4个参数根据参数code,来确定调用哪个方法,data代表输入参数,reply代表返回值。客户端调用服务端的远程方法跟调用本地方法差不多,客户端发起的请求要挂起直到服务端进程处理完,所以远程方法如果是比较耗时的,客户端最好不要在UI线程中发起这个远程方法调用,而应该放到子线程中进行。而服务端的Binder方法是运行在Binder的线程池中。


    Binder原理图.png

    四、总结

    Messenger是执行进程间通信(IPC)最简单的方式,服务端Messenger通过Handler将客户端的请求放到消息循环中排队,然后逐个取出进行处理,而客户端要接收结果需要2个Messenger,一个用于发送消息,一个用于接收消息。其底层原理也是Binder,客户端得到服务端的Binder对象在binder驱动层对应的mRemote引用,然后给服务器发消息,实现跨进程通信。这种方式缺点很明显,服务端以串行方式处理客户端的消息,消息处理结果反馈并不及时,不适合服务端、客户端经常通信的场景。而用AIDL可以并发处理客户端的远程调用,远程方法调用就像本地调用一样,立即执行后就有结果返回。但Messenger的特点是简单,不需要想AIDL一样,需要定义aidl文件,还需要将aidl文件拷贝到服务端和客户端。由于引入了Replugin,使用Messenger、AIDL启动宿主/插件的Service,启动方式有所区别,使用时需要注意。AIDL的底层是Binder,其本质只是为我们提供了一种快速实现Binder的工具。

    相关文章

      网友评论

          本文标题:replugin宿主与插件通信小结

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