第二章-IPC机制

IPC基础-Linux 中的进程间通信机制

  • pipe 管道:内核管理的一个缓冲区,只能传递无
  • FIFO 命名管道
  • 共享内存
  • Socket
  • signal
  • Message queues
  • Semaphore

Android 中的多进程方式

Andorid 中使用多进程需要在AndroidMainfest中给4大组件添加android:process属性

  • andorid:process=":remote"
    • 私有进程,别的组件不可以和它跑在一起
  • andorid:process="com.android.remote"
    • 公有进程,别的组件可以通过使用ShareUID的方式和它跑在一起(当然应用签名还必须一样)

多进程会导致如下问题

  • 单例模式,静态变量完全失效
  • Application会创建多次:一个进程分配一个虚拟机,一个应用就是一个进程,启动一个进程就相当于重新启动了一个应用程序
  • SharePreferencess 可靠性下降(多个进程并发读写同一个xml文件很大概率出现问题)
  • 线程同步机制完全失效

序列化与反序列化

实现Serizlizable接口

serialVersionUID 序列化时系统会将该ID也一同写入序列化的文件,在反序列化时会通过比较该id和反序列化的类的id,如果不同会报异常。
如果不指定该值,在反序列化时如果反序列化的类中删除或多出了某些字段就会crash。(不指定,序列化和反序列化系统会计算hash值来赋值到该字段上,所以修改字段或删除后会改变hash从而造成crash)。如果指定了serialVersionUID ,因为serialVersionUID 都是相同的那么在反序列化后系统会尽可能的帮助我们恢复数据

实现Parcelable接口

AIDL 生成的java代码分析

  • aidl中声明了几个方法,就会有几个整形id用来标识这些方法,用于标识在transact过程中客户端使用的是哪个方法。
  • 内部类Stub就是一个Binder。当客户端与服务端位于同一进程时,不会走进程的transact过程,当客户端与服务端位于不同进程时,需要走内部代理类Proxy中的ransact过程
  • Stub中onTransact在服务端执行,该方法运行在服务端的Binder线程池中,当客户端发起跨进程请求系统底层会将请求封装后调用该方法。如果服务端与客户端位于同一进程,那么不会调用该方法(因为客户端获得的Binder是服务端本身,是同一个类)。如果服务端与客户端位于不同进程,由于客户端获得的Binder是一个Proxy,调用方法通过Proxy发送消息,服务端通过onTransact处理消息。
  • Proxy在客户端执行

注:AIDL文件只是系统给我们一个快速生成Binder的工具。

服务端状态的监听

客户端可以通过DeathRecipient接口来监听服务端状态

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    try {
        bookManager = IBookManager.BookManagerImpl.asInterface(service);
        bookManager.asBinder().linkToDeath(deathRecipient,0);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

在绑定服务获取Binder对象时设置监听

private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        bookManager.asBinder().unlinkToDeath(deathRecipient,0);
    }
};

在回调中取消监听

Android中的IPC方式

  • 通过Bundle,四大组件中Activity,Service,Receiver都支持在Intent中传递Bundle数据。Bundle实现了Parcelable接口因此很容易在各进程中传递。
  • 通过共享文件,两个进程通过读/写同一文件进行数据交换
    • 优点
      • 数据格式自定义化程度较高,只要读写双方约定好数据格式即可。
    • 缺点
      • 并发读写可能会出现数据丢失,数据不是最新问题

总结:文件共享方式适合在对数据同步要求不高的进程间进行通信,并且开发人员要妥善处理并发问题

  • 通过Messenger

服务端

private Messenger messenger = new Messenger(new MessageHandle());
@Override
public IBinder onBind(Intent intent) {
    return messenger.getBinder();
}

服务端直接返回Messenger中内部Binder对象

客户端

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mService = new Messenger(service);
}

客户端绑定成功后实例化,该构造函数中会调用IMessenger.Stub.asInterface(target)将IBinder转化成服务端Messenger的代理对象。之后就可以通过mService.send(Message msg)方法向服务端发送消息。

客户端与服务端双向通信

客户端绑定成功后在组装Message消息时需要向message.replyTo赋值接收消息的Messenger

Message message = Message.obtain();
message.setData(bundle);
message.what =1;
message.replyTo = mMessengerClient; //赋值接收消息的Messenger
mService.send(message);

服务端获取该Messenger后即可向客户端发送消息

private static class MessageHandle extends Handler{·
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case 1:
                try {
                    Messenger replyClient = msg.replyTo;
                    Bundle reciverBundle = msg.getData();
                    reciverBundle.setClassLoader(Book.class.getClassLoader());
                   com.hdingmin.dev.Book book = reciverBundle.getParcelable("data");
                    if (replyClient != null) {
                        Message replyMsg = Message.obtain();
                        replyMsg.what = 1001;
                        Bundle bundle = new Bundle();
                        bundle.putString("reply", "我已经收到消息了,这是我收到的消息:" + book.toString());
                        replyMsg.setData(bundle);
                        replyClient.send(replyMsg);
                    }
                }catch (RemoteException e){
                    e.printStackTrace();
                }
                break;
            default:
                    break;
        }
    }
}

注意:通过Messenger向服务端发送包含实现Parcelable接口数据的Bundle时由于是跨进程调用导致Bundle中ClassLoder丢失,所以在getXXX数据前必须先setClassLoader否则会报错。同时Messenger是串行处理的请求不适合存在大量并发请求,同时也不支持调用服务端方法。

  • 通过AIDL
    • AIDL支持的数据类型
      • 基本数据类型(int、long、char、boolean、double等)
      • String和CharSequence
      • List:只支持ArrayList,里面每个元素都必须能够被AIDL支持
      • Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和vale
      • Parcelable:所有实现了Parcelable接口的对象
      • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

注意

  • Parcelable对象或AIDL对象必须显示的import出来
  • 自定义的Parcelable对象必须有同名的AIDL文件,如XXX.java,那么必须有XXX.aidl
  • 除了基本数据类型(默认是in),其他类型参数必须标上方向:in,out,inout
    • in :数据只能由客户端流向服务端
    • out :数据只能由服务端流向客户端
    • inout :数据可在服务端与客户端之间双向流通
      客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的参数为空的对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。
  • 通过ContentProvider

使用步骤:

  • 继承ContentProvider抽象类实现抽象方法
  • 在AndroidManifest中配置provider和authorities
    ContentProvider 中方法参数的含义
    /**
     * 查询
     * @param uri 标识、定位任何资源的字符串
     * @param projection 需要筛选的列字段
     * @param selection 筛选条件相当于WHERE后的内容“name=?”或“name=张三”
     * @param selectionArgs  如果selection中使用了占位符“name=?”,那么该参数不能为空
     * @param sortOrder 排序
     * @return
     * Cursor cursor = getContentResolver().query(uri,new String[]{"name"},"name=?",new String[]{"张三"}, null);
     */
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }
  • 通过Socket
    服务端
serverSocket = new ServerSocket(8688);
//阻塞方法,调用该方法后线程进入阻塞状态直到接收到客户端的连接
final Socket client = serverSocket.accept();

客户端

socket = new Socket("192.168.12.1",8688);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while (!SocketActivity.this.isFinishing()){
        //阻塞方法,调用该方法后线程进入阻塞状态直到接收到服务端返回的消息
        String msg = br.readLine();
        mHandler.obtainMessage(HAS_RECEIVE_MSG,msg).sendToTarget();
}

客户端发送消息

private void sendMessage (){
      String content = msgEidtText.getText().toString();
      if(!TextUtils.isEmpty(content)){
          try {
                if(!socket.isOutputShutdown()) {
                    //加上\n是因为服务端读取客户端的时候使用了readLine(),所以加上换行符才能被服务端读取
                    bw.write(content+"\n");
                    bw.flush();
                    Toast.makeText(this, "已发送:" + content, Toast.LENGTH_SHORT).show();
              }
          }catch (IOException ex){
                ex.printStackTrace();
          }
     }else {
            Toast.makeText(this,"发送消息不能为空",Toast.LENGTH_SHORT).show();
     }
}

IPC 方式的优缺点和适用场景

名称 优点 缺点 适用场景
Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件间的进程通信
文件共享 简单易用 不支持高并发,无法做到进程间即时通信 无并发访问,交换简单的数据实时性不高的场景
AIDL 功能强大,支持一对多并发通信,支持实时通信 使用复杂,需要处理线程同步 一对多通信,有RPC需求
Messenger 支持一对多串行通信,支持实时通信 不能很好处理高并发的情况,不支持RPC,数据通过Messenger传输只能传Bundle支持的数据类型 低并发,一对多即时通信,无需RPC或无需返回结果的RPC需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发操作,可通过Call方法扩展其他操作 可理解为收约束的AIDL,主要提供数据源的CRUD操作 一对多的进程间的数据共享
Socket 功能强大,可通过网络传输字节流,支持一对多并发实时通信 使用起来复杂,不支持直接的RPC 网络数据交换

你可能感兴趣的:(第二章-IPC机制)