【Android】一篇足矣:全面方位详解跨进程通信(IPC机制)

前言:IPC(inter-Process-Communication)进程间通信,用于两个进程之间进行数据交互的过程,任何操作系统都有IPC机制,但不同的操作系统有着不同的通信方式,Android系统是一种基于Linux内核的移动操作系统,Linux的跨进程通信主要通过管道、共享内存、内存映射等实现的,但Android有自己的进程间通信机制,最具代表性的就是Binder。

本文篇幅虽长,但可以复习到的知识却有很多,通过阅读你能了解什么是IPC机制(Linux&Android)、多进程模式的用法、跨进城通讯的方式、序列化反序列化、Binder的作用、Bundle的使用、ContentProvider、Messenger、Socket等知识点。

尊重原著,参考资料如下:任志强《Android开发艺术探索》、Serializable和ParcelableServiceManagerAIDL的使用详解AIDL的工作原理ContentProvider、Binder原理大量参考写给 Android 应用工程师的 Binder 原理剖析,写的很好请去支持!

码字不易,耗时三天学习和理解最后归纳出了这篇文章,转载请标明出处。

简单介绍一下IPC机制

  • 含义:进程间通信、跨进程通信,进程是资源分配的最小单位,操作系统的一个执行单元,即一个程序或者应用。一个进程至少包含一个线程,线程是CPU调度的最小单位,是一种有限的系统资源。
  • 方式:Binder、AIDL、Socket、文件共享、ContentProvide等。
  • 场景:多进程、一种是一个应用由于特殊原因需要多进程,特殊原因比如模块需要运行在单独的进程中,或者应用为了获取更大的内存空间(系统给单个应用分配的空间有限,不同手机设备大小不一,但相差不大)。另一种是当前应用需要向其他应用获取数据。

Android中的多进程模式

指的是指定应用内部开启多进程模式,只有一中方法,给四大组件在AndroidMenifest中指定android:process属性。如下图方式:
项目实例图

上图可以看到process属性有两种写法,这两种写法是有区别的,以”:“开头的相当于当前应用的私有进程,其他应用的组件不可以和他跑在同一进程中,而另一种则属于全局进程,其他应用通过ShareUID方式和它跑在同一进程,那UID是什么呢?UID是Android系统为每一个应用分配的用户身份证明来区分不同的应用。

  • 具有相同UID的应用能共享数据(可以互相访问对方的私有数据data目录、组件信息等)
  • 跑在同一进程需要相同的UID+相同的签名(还可以访问共享内存数据)
    相同UID的应用实现资源共享: 首先需要在两个应用的AndroidManifest.xml中都定义相同的sharedUserId,如:android:sharedUserId="com.test"。
    uid共享res资源实例
    假设我们有这样一个需求,A和B是两个应用,现在要求在A中获取B的一张名字为send_bg的图片资源,那么先将A和B的注册文件的AndroidManifest.xml节点添加sharedUserId,并且赋值相同,然后在A中可以用如下方式实现:
    Context thdContext = null;
    try {
        thdContext = createPackageContext(
                "com.example.testdatabase",
                Context.CONTEXT_IGNORE_SECURITY);
        Resources res = thdContext.getResources();  
        int menuIconId = res.getIdentifier("send_bg", "drawable",  
                "com.example.testdatabase");  
        Drawable drawable = res.getDrawable(menuIconId);  
        mButton.setBackgroundDrawable(drawable);
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }

uid共享database实例
假设我们有这样一个需求,A和B是两个应用,现在要求在A中要获取B的数据库,那么先将A和B的注册文件的AndroidManifest.xml节点添加sharedUserId,并且赋值相同,然后在A中可以用如下方式实现:

    Context thdContext = null;
    try {
        thdContext = createPackageContext(
                "com.example.testdatabase",
                Context.CONTEXT_IGNORE_SECURITY);
        String dbPath = thdContext.getDatabasePath("BookStore.db")
                .getAbsolutePath();
        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath,
                null, SQLiteDatabase.OPEN_READWRITE);
        Cursor cursor = db.query("Book", null, null, null, null,
                null, null);
        if (cursor.moveToFirst()) {
            do {
                String name = cursor.getString(cursor
                        .getColumnIndex("name"));
                Log.d(TAG, "name: " + name);
            } while (cursor.moveToNext());
        }
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }

同应用开启多进程后,就运行在不同的虚拟机里了,不同的虚拟机在内存分配上有不同的地址空间,造成的问题主要是:

  • 静态成员和单例模式失效
  • 线程同步机制失效
  • SharePreferences可靠性下降,底层是通过读写XML文件来实现的,不支持两个进程同时读写操作,会丢失数据。
  • Application会多次创建,因为系统要为新建的进程分配独立的虚拟机,这个过程就是启动一个新应用的过程,相当于系统又把这个应用重新启动了一遍导致重建新的Application。运行在同一进程中的组件属于同一个虚拟机和Application,反之则反。

利用跨进程通信解决上述问题

简单说就是:通过使数据持久化存储在设备上来传输数据。
需要通过Serializable接口/Parcelable接口来完成对象的序列化,再通过Intent和Binder传输数据或者通过Serializable将对象持久化到存储设备上或者网络传输给其他应用。

对比 Parcelable Serializable
实现方式 实现Parcelable接口 实现Serializable接口
属于 android 专用,基于DVM Java自带,基于JVM
内存消耗 优秀 一般,相对开销大
读写数据 内存中直接进行读写 通过使用IO流的形式将数据读写入在硬盘上
持久化 不可以 可以
速度 优秀 一般
Android中使用场景 跨进城数据的传输 数据持久化存储设备、网络传输数据

Serializable接口,基于JVM上的持久化数据

Java提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化。Serializable使用IO读写存储在硬盘上。序列化过程使用了反射技术,并且期间产生临时对象。Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。优点代码少。
使用方法:在需要序列化的类里面实现Serializable接口并声明一个标识serialVersionUID 如

public class User implements Serializable {
    //可以不写,系统根据结构计算当前类的hash值自动分配,增加或删除结构从新计算会hash值会不一致,导致反序列化失败程序crash。
    //手动指定,最大程度反序列化成功,建议用插件自动生成。
    private static final long serialVersionUID =463442362345654634L;
    ... ...
}

剩下的工作系统会自动完成,进行序列化和反序列化的方法:采用ObjectOutputStream和ObjectInputStream。

//序列化过程
User user = new User("chou",27,man);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.tex"));
out.writeObject(user);
out.close;

//反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.tex"));
User newUser = (User)in.readObject();
in.close;

Parcelable接口,Android专用,基于DVM上的持久化数据,Parcelable 的读取顺序必须一致,不然有的机型会出错闪退(遇到过这种bug)

Parcelable是直接在内存中读写,我们知道内存的读写速度肯定优于硬盘读写速度,所以Parcelable序列化方式性能上要优于Serializable方式很多。但是代码写起来相比Serializable方式麻烦一些。
代码示例:User(Parcel source) 参数的顺序一定需要与 writeToParcel(Parcel dest, int flags)参数顺序

public class User implements Parcelable {
      private String name;
      private int age;
      private String sex;
  
      public User(String name, int age, String sex ) {
          this.name = name;
          this.age = age;
          this.sex = sex;
      }

      //返回当前对象的内容描述
      @Override
      public int describeContents() {
          return 0;
      }

      //序列化操作,将对象写入序列化结构中
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeString(name);
          dest.writeInt(age);
          dest.writeString(sex);
      }

      //反序列化操作
      public static final Creator<User> CREATOR = new Creator<User>() {
          //从序列化后的对象中创建原始对象
          @Override
          public User createFromParcel(Parcel in) {
              return new User(in);
          }
          //创建指定长度的原始对象数组
          @Override
          public User[] newArray(int size) {
              return new User[size];
          }
      };
      //从序列化后的对象中创建原始对象
      public User(Parcel source) {
          this.name = source.readString();
          this.age = source.readInt();
          this.sex = source.readString();
      }
  }

这里先说一下Parcel,Parcel内部包装了可序列化的数据,可以在Binder中自由传输。从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的;反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程;内容描述功能由describeContents方法来完成,几乎在所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。需要注意的是,在User(Parcel in)方法中,由于book是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。详细的方法说明请参看下面表格

方法 功能 标记位
createFromParcel(Parcel in) 从序列化后的对象中创建原始对象
newArray(int size) 创建指定长度的原始对象数组
User(Parcel in) 从序列化后的对象中创建原始对象
writeToParce(Parcel out, int flags) 将当前对象写入序列化结构中,其中flags标识有两种值: 0或者1 (参见右侧标记位)。为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0 PARCELABLE_WRITE_RETURN_VALUE
describeContents 返回当前对象的内容描述。如果含有文件描述符,返回1(参见右侧标记位),否则返回0, 几乎所有情况都返回0 ONTENTS_FILE_DESCRIPTOR
  • 将这个数据放到 intent 或者 Bundle 中完成传递
 Intent intent = new Intent(MainActivity.this, ServerService.class);

  intent.setExtrasClassLoader(User.class.getClassLoader());

  Bundle bundle = new Bundle();
  bundle.putParcelable("User", new User("chou",27,"sex"));
  intent.putExtras(bundle); 

  bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
  • 在需要接收目标组件中完成接收操作
  Bundle bundle = intent.getExtras();
  User user = bundle.getParcelable("User");

Binder重头戏

binder是Android中的一种跨进程通信方式,是Android的一个类,它实现了IBinder接口。

  1. 从IPC角度讲:也可以把它当成一种虚拟的物理设备,它的驱动是/dev/binder,该通讯方式在Linux中没有,所以是Android特有的跨进程通信方式。
  2. 从Android Framework层面上讲:Binder是连接ServiceManager(用来管理系统的service)连接各种Manager(ActivityManager、WindowManager等等)和相应ManagerService的桥梁。
  3. 从应用层讲:Binder是客户端和服务端进行通信的媒介,当bindService时,服务端会返回一个包含服务端业务调用的Binder对象,客户端通过这个对象来获取服务端提供的服务或数据。
Android是基于Linux系统上的,我们先了解一下Linux中的IPC通信原理

在Linux中,进程之间是隔离的,内存也是不共享的,想要进程之间通信必须先了解一下进程空间,进程空间分为用户空间和内核空间,用户空间是用户程序运行的空间,内核空间则是内核运行的空间,为了防止用户空间随便干扰,用户空间是独立的,内核空间是共享的,但为了安全性考虑,内核空间跟用户空间也是隔离的,它们之间的仅可以通过系统调用来通信。至此我们知道IPC的大致方案是A进程的数据通过系统调用把数据传递到内核空间,内核空间再利用系统调用把数据传递到B空间,其中会有两次数据的拷贝如下图:


Linux下的IPC

缺点显而易见,数据传递通过内存缓存—>内核缓存—>内存缓存2次拷贝性能低,其次是传递数据后,接收方不知道用多大内存存放,所以尽可能大的开辟内存空间导致内存空间浪费。

再来看一下Android中的IPC通信-Binder

字面意思,粘合剂、胶水的意思,顾名思义就是粘合不同的进程,使之实现通信。
你肯定有疑问,既然基于Linux系统,为什么Android不沿用Linux的IPC而要使用Binder呢?因为综合考虑其性能、稳定性和安全性。

  • 性能 :数据拷贝次数,共享内存0次、Binder 1次、Socket/管道/消息队列2次。
  • 稳定性:Binder基于C/S架构,客户端有需求丢给服务端完成,架构清晰,职责明确独立,共享内存需要控制负责、难以使用,Binder机制更优。
  • 安全性:Android开放性平台,不免会有很多恶意APP、流氓软件等,传统IPC没有任何安全防护,无法获取对方进程ID,Android为每个进程分配UID,传统的IPC机制只能在数据包中添加,可靠的身份标识只能由IPC机制在内核添加才安全,其次传统的IPC访问接入点是开放的,恶意软件通过猜测接入点可以获得连接,所以不安全。Binder支持实名和匿名Binder,安全性更高。

先看看了解Binder可以给我们带来什么帮助?

  • 为什么Activity间传递对象需要序列化?
  • 帮助我们理解Activity的启动流程
  • 四大组件底层的通讯机制是怎样的?
  • 理解AIDL内部实现机制
  • 帮助理解插件化编程等等~
    Android应用程序是由四大组件中的一个或多个组成,有时这些组件运行在同一进程,有时运行在不同进程,这些进程间通信需要依赖Binder IPC机制,不光于此,Android系统对应用层提供的各种服务如AMS、PMS等都是基于Binder IPC机制来实现的。
了解了这些,我们再来看Binder是怎样不利用传统的IPC来实现通信的

IPC基于内核空间,但Binder不是Linux系统内核的一部分,但Linux有动态内核可加载模块(LKM)的机制,模块是具有独立功能的程序,可以被单独编译但不能独立运行,运行时被了链接到内核座位内核的一部分运行,至此Android系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信,我们把这个内核模块叫做Binder驱动(Binder Driver)。它是怎样实现进程间通信的呢?通过内存映射!,Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

Binder IPC 原理

Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。举个栗子:进程中的用户区域不能直接访问物理设备,如果想访问磁盘数据,需要通过磁盘—>内核空间—>用户空间这两次拷贝,我们通过mmap()在两者之间建立映射,减少数据的拷贝,用内存读取来取代I/O读写提高效率。而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
过程如下图文:
1.首先 Binder 驱动在内核空间创建一个数据接收缓存区;
2.接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
3.发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

Android下的IPC

Client/Server/ServiceManager/驱动

Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。

  • binder 驱动 -> 路由器
  • ServiceManager -> DNS
  • Binder Client -> 客户端
  • Binder Server -> 服务器
    如下图比较:



    Binder通讯整个下来的过程
    1.首先,一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
    2.Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
    3.Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。


Binder 通信中的代理模式

我们已经解释清楚 Client、Server 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。
前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。

当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。

各 Java 类职责描述

在正式编码实现跨进程调用之前,先介绍下实现过程中用到的一些类。了解了这些类的职责,有助于我们更好的理解和实现跨进程通信。

  • IBinder : IBinder 是一个接口,代表了一种跨进程通信的能力。只要实现了这个接口,这个对象就能跨进程传输。

  • IInterface : IInterface 代表的就是 Server 进程对象具备什么样的能力(能提供哪些方法,其实对应的就是 AIDL 文件中定义的接口)

  • Binder : Java 层的 Binder 类,代表的其实就是 Binder 本地对象。BinderProxy 类是 Binder 类的一个内部类,它代表远程进程的 Binder 对象的本地代理;这两个类都继承自 IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder 驱动会自动完成这两个对象的转换。

  • Stub : AIDL 的时候,编译工具会给我们生成一个名为 Stub 的静态内部类;这个类继承了 Binder, 说明它是一个 Binder 本地对象,它实现了 IInterface 接口,表明它具有 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要开发者自己实现。

实现过程

一次跨进程通信必然会涉及到两个进程,在这个例子中 RemoteService 作为服务端进程,提供服务;ClientActivity 作为客户端进程,使用 RemoteService 提供的服务。如下图:

那么服务端进程具备什么样的能力?能为客户端提供什么样的服务呢?还记得我们前面介绍过的 IInterface 吗,它代表的就是服务端进程具体什么样的能力。因此我们需要定义一个 BookManager 接口,BookManager 继承自 IIterface,表明服务端具备什么样的能力。

/**
 * 这个类用来定义服务端 RemoteService 具备什么样的能力
 */
public interface BookManager extends IInterface {

    void addBook(Book book) throws RemoteException;
}

只定义服务端具备什么要的能力是不够的,既然是跨进程调用,那么接下来我们得实现一个跨进程调用对象 Stub。Stub 继承 Binder, 说明它是一个 Binder 本地对象;实现 IInterface 接口,表明具有 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要调用方自己实现。

public abstract class Stub extends Binder implements BookManager {

    ...

    public static BookManager asInterface(IBinder binder) {
        if (binder == null)
            return null;
        IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
        if (iin != null && iin instanceof BookManager)
            return (BookManager) iin;
        return new Proxy(binder);
    }

    ...

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {

            case INTERFACE_TRANSACTION:
                reply.writeString(DESCRIPTOR);
                return true;

            case TRANSAVTION_addBook:
                data.enforceInterface(DESCRIPTOR);
                Book arg0 = null;
                if (data.readInt() != 0) {
                    arg0 = Book.CREATOR.createFromParcel(data);
                }
                this.addBook(arg0);
                reply.writeNoException();
                return true;

        }
        return super.onTransact(code, data, reply, flags);
    }

    ...
}

Stub 类中我们重点介绍下 asInterfaceonTransact

先说说 asInterface,当 Client 端在创建和服务端的连接,调用 bindService 时需要创建一个 ServiceConnection 对象作为入参。在 ServiceConnection 的回调方法 onServiceConnected 中 会通过这个 asInterface(IBinder binder) 拿到 BookManager 对象,这个 IBinder 类型的入参 binder 是驱动传给我们的,正如你在代码中看到的一样,方法中会去调用 binder.queryLocalInterface() 去查找 Binder 本地对象,如果找到了就说明 Client 和 Server 在同一进程,那么这个 binder 本身就是 Binder 本地对象,可以直接使用。否则说明是 binder 是个远程对象,也就是 BinderProxy。因此需要我们创建一个代理对象 Proxy,通过这个代理对象来是实现远程访问。

接下来我们就要实现这个代理类 Proxy 了,既然是代理类自然需要实现 BookManager 接口。

public class Proxy implements BookManager {

    ...

    public Proxy(IBinder remote) {
        this.remote = remote;
    }

    @Override
    public void addBook(Book book) throws RemoteException {

        Parcel data = Parcel.obtain();
        Parcel replay = Parcel.obtain();
        try {
            data.writeInterfaceToken(DESCRIPTOR);
            if (book != null) {
                data.writeInt(1);
                book.writeToParcel(data, 0);
            } else {
                data.writeInt(0);
            }
            remote.transact(Stub.TRANSAVTION_addBook, data, replay, 0);
            replay.readException();
        } finally {
            replay.recycle();
            data.recycle();
        }
    }

    ...
}

我们看看 addBook() 的实现;在 Stub 类中,addBook(Book book) 是一个抽象方法,Client 端需要继承并实现它。

  • 如果 Client 和 Server 在同一个进程,那么直接就是调用这个方法。
  • 如果是远程调用,Client 想要调用 Server 的方法就需要通过 Binder 代理来完成,也就是上面的 Proxy。

在 Proxy 中的 addBook() 方法中首先通过 Parcel 将数据序列化,然后调用 remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中创建,能走到创建 Proxy 这一步就说明 Proxy 构造函数的入参是 BinderProxy,即这里的 remote 是个 BinderProxy 对象。最终通过一系列的函数调用,Client 进程通过系统调用陷入内核态,Client 进程中执行 addBook() 的线程挂起等待返回;驱动完成一系列的操作之后唤醒 Server 进程,调用 Server 进程本地对象的 onTransact()。最终又走到了 Stub 中的 onTransact() 中,onTransact() 根据函数编号调用相关函数(在 Stub 类中为 BookManager 接口中的每个函数中定义了一个编号,只不过上面的源码中我们简化掉了;在跨进程调用的时候,不会传递函数而是传递编号来指明要调用哪个函数);我们这个例子里面,调用了 Binder 本地对象的 addBook() 并将结果返回给驱动,驱动唤醒 Client 进程里刚刚挂起的线程并将结果返回。

这样一次跨进程调用就完成了。

多种多样的跨进程方式

上面介绍了Binder、序列化,我们再来了解一下其它的方式,比如通过Intent中附加的extra来传递信息、通过文件共享的方式共享数据、ContentProvider这种天生就支持跨进城访问的方式、通过网络通讯Socket来完成通讯等。

1.使用Bundle:
四大组件都支持在Intent中传递Bundle数据,Intent传递数据的特点上面说过需要序列化,Bundle实现了Pracelable接口,所以当我们在一个进程中启动另一个进程的Activity、Service等,就可以利用Bundle附加数据来传递信息,Bindle传递自定义类型需要序列化,但局限性是Bundle传递的数据有限制。
2.使用文件共享:
把一个对象序列化存储写入到SD卡上的一个文件里,另一个进程再去读取这个文件获得通信。
3.使用Messenger:
Messenger顾名思义,信使的意思,通过它我们可以在不同的进程中传递Message对象,Message中存放着需要传递的对象。它是一种轻量级的IPC方案,底层实现是AIDL,它对AIDL做了封装,使我们使用起来更简单,由于它一次处理一个请求,所以我们不需要考虑线程同步问题。
使用方法:
1.服务端进程:创建Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind方法中返回这个messenger对象底层的Binder。
2.客户端进程:首先需要绑定服务端的Service,绑定成功后在服务端返回的IBinder对象中创建一个Messenger,通过它向服务端发Message类型的消息。如果服务端回应客户端,和服务端,我们还需要创建一个Handler并创建一个新的Messenger并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过replyTo参数就可以回应客户端。
代码示例:

  • 构建一个运行在独立进程中的服务端Service:
public class MessengerService extends Service {
    private static final String TAG = "MessagerService";

    /**
     * 处理来自客户端的消息,并用于构建Messenger
     */
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message message) {
            switch (message.what) {
                case MESSAGE_FROM_CLIENT:
                    Log.e(TAG, "receive message from client:" + message.getData().getString("msg"));
                    break;
                default:
                    super.handleMessage(message);
                    break;
            }
        }
    }

    /**
     * 构建Messenger对象
     */
    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //将Messenger对象的Binder返回给客户端
        return mMessenger.getBinder();
    }
}
  • 注册Service,在不同进程
<service
    android:name="com.xxq2dream.service.MessengerService"
    android:process=":remote" />
  • 然后客户端是通过绑定服务端返回的binder来创建Messenger对象,并通过这个Messenger对象来向服务端发送消息
public class MessengerActivity extends AppCompatActivity {
    private static final String TAG = "MessengerActivity";

    private Messenger mService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.e(TAG, "ServiceConnection-->" + System.currentTimeMillis());
            //通过服务端返回的Binder创建Messenger
            mService = new Messenger(iBinder);
            //创建消息,通过Bundle传递数据
            Message message = Message.obtain(null, MESSAGE_FROM_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "hello service,this is client");
            message.setData(bundle);
            try {
                //向服务端发送消息
                mService.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            Log.e(TAG, "onServiceDisconnected-->binder died");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        //绑定服务
        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        //解绑服务
        unbindService(mConnection);
        super.onDestroy();
    }
}
  • 服务端如果要回复消息给客户端,那就要用到Message的replyTo参数了
  • 服务端改造:
private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case Constant.MESSAGE_FROM_CLIENT:
                Log.e(TAG, "receive message from client:" + message.getData().getString("msg"));
                //获取客户端传递过来的Messenger,通过这个Messenger回传消息给客户端
                Messenger client = message.replyTo;
                //当然,回传消息还是要通过message
                Message msg = Message.obtain(null, Constant.MESSAGE_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("msg", "hello client, I have received your message!");
                msg.setData(bundle);
                try {
                    client.send(msg);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(message);
                break;
        }
    }
}
  • 客户端改造:
/**
 * 用于构建客户端的Messenger对象,并处理服务端的消息
 */
private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case Constant.MESSAGE_FROM_SERVICE:
                Log.e(TAG, "receive message from service:" + message.getData().getString("msg"));
                break;
            default:
                super.handleMessage(message);
                break;
        }
    }
}

/**
 * 客户端Messenger对象
 */
private Messenger mClientMessenger = new Messenger(new MessengerHandler());

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        Log.e(TAG, "ServiceConnection-->" + System.currentTimeMillis());
        mService = new Messenger(iBinder);
        Message message = Message.obtain(null, MESSAGE_FROM_CLIENT);
        Bundle bundle = new Bundle();
        bundle.putString("msg", "hello service,this is client");
        message.setData(bundle);
        //将客户端的Messenger对象传递给服务端
        message.replyTo = mClientMessenger;
        try {
            mService.send(message);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        Log.e(TAG, "onServiceDisconnected-->binder died");
    }
};

Messenger工作原理图示

4.使用AIDL:
Messenger是串行处理消息的,服务端需要一个个来处理,不适合大量的消息同时发送给服务端。其次Messenger作用是传递消息,有时候我们还需要调用服务端方法,这种情景AIDL更适合。步骤如下:
1.服务端:创建Service用来监听客户端连接请求
2.客户端:绑定服务端的Service,将服务端返回的Binder对象转换成AIDL接口所属的类型后就可以调用AIDL中的方法了。
3.AIDL接口创建,声明接口、方法、同步项目,具体使用如下图:

创建,输入aidl文件名:IMyAidlInterface.aidl

// IMyAidlInterface.aidl
interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     *
     * 此方法作用是告诉我们aidl中可以使用的基本类型,可以删除无视。
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

在AIDL中定义提供给其他app使用的方法

interface IMyAidlInterface {
   String getName();
}

sycn project同步后,编译工具会自动帮我们生成一个Stub类,创建一个service并在里面创建一个继承刚才接口的Stub类内部类,实现接口方法,并在onBind方法中返回内部类的实例:

public class MyService extends Service
{

    public MyService()
    {
      ...
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return new MyBinder();
    }

    class MyBinder extends IMyAidlInterface.Stub
    {

        @Override
        public String getName() throws RemoteException
        {
            return "test";
        }
    }
}

接下来我们将AIDL文件拷贝到客户端(包名,文件名必须完全一致),然后在Activity中绑定服务。

public class MainActivity extends AppCompatActivity
{


    private IMyAidlInterface iMyAidlInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent("cc.abto.server"), new ServiceConnection()
        {

            @Override
            public void onServiceConnected(ComponentName name, IBinder service)
            {

                iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
            }

            @Override
            public void onServiceDisconnected(ComponentName name)
            {

            }
        }, BIND_AUTO_CREATE);
    }

    public void onClick(View view)
    {
        try
        {
            Toast.makeText(MainActivity.this, iMyAidlInterface.getName(), Toast.LENGTH_SHORT).show();
        }
        catch (RemoteException e)
        {
            e.printStackTrace();
        }
    }
}

这边我们通过隐式意图来绑定service,在onServiceConnected方法中通过IMyAidlInterface.Stub.asInterface(service)获取iMyAidlInterface对象,然后在onClick中调用iMyAidlInterface.getName()。
使用自定义数据类型是需要把使用的对象序列化实现Parcelable接口,在aidl中导入该类型再使用,注意包名文件名。

5.使用ContentProvider
ContentProvider是Android提供的专门用于应用间进行数据共享的方式,天生适合进程间通信,和Messenger一样底层同样实现了Binder(可以理解几乎所有跨进程都是基于Binder的封装。来实现的)。

ContentProvider模型

ContentProvider是一个抽象类,如果我们需要开发自己的内容提供者我们就需要继承这个类并复写其方法,需要实现的主要方法如下:
public boolean onCreate()
在创建ContentProvider时使用
public Cursor query()
用于查询指定uri的数据返回一个Cursor
public Uri insert()
用于向指定uri的ContentProvider中添加数据
public int delete()
用于删除指定uri的数据
public int update()
用户更新指定uri的数据
public String getType()
用于返回指定的Uri中的数据MIME类型
数据访问的方法insert,delete和update可能被多个线程同时调用,此时必须是线程安全
其它应用可以通过ContentResolver来访问ContentProvider提供的数据,而ContentResolver通过uri来定位自己要访问的数据。
为什么要通过再加一层ContentResolver而不是直接访问ContentProvider?
原因是:一台手机中可不是只有一个Provider内容,它可能安装了很多含有Provider的应用,比如联系人应用,日历应用,字典应用等等。有如此多的Provider,如果你开发一款应用要使用其中多个,如果让你去了解每个ContentProvider的不同实现,岂不是要头都大了。所以Android为我们提供了ContentResolver来统一管理与不同ContentProvider间的操作。怎样区别不同的Provider则是通过URI!
ContentResolver工作原理

扩展:URI(Universal Resource Identifier)统一资源定位符
格式:[scheme:][//host:port][path][?query]
URI:http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack

  • scheme:根据格式我们很容易看出来scheme为http
  • host:www.baidu.com
  • port:就是主机名后面path前面的部分为8080
  • path:在port后面?的前面为wenku/jiatiao.html
  • query:?之后的都是query部分为 id=123456$name=jack
    uri的各个部分在安卓中都是可以通过代码获取的,下面我们就以上面这个uri为例来说下获取各个部分的方法:
  • getScheme() :获取Uri中的scheme字符串部分,在这里是http
  • getHost():获取Authority中的Host字符串,即 www.baidu.com
  • getPost():获取Authority中的Port字符串,即 8080
  • getPath():获取Uri中path部分,即 wenku/jiatiao.html
  • getQuery():获取Uri中的query部分,即 id=15&name=du

接下来我们看一下如何使用,创建了两个工程,进程一自定义了contentprovider,进程二通过ContentResolver来访问进程一中的contentprovider的数据(对进程一中自定义的contentprovider的数据库进行增删改查操作)
先写个数据库的工具类用来创建数据库(进程二就是跨进程操作此数据库的)

public class DBHelper extends SQLiteOpenHelper {

    // 数据库名
    private static final String DATABASE_NAME = "finch.db";

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";

    private static final int DATABASE_VERSION = 1;
    //数据库版本号

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        // 创建两个表格:用户表 和职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");

        db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {

    }
}

再自定义一个自己的contentprovider。。DBHelper中有两张表, UriMatcher是用来根据进程二的ContentResolver调用的uri判断进程二到底是需要操作哪张表的数据的。

public class MyProvider extends ContentProvider {

    private Context mContext;
    DBHelper mDbHelper = null;
    SQLiteDatabase db = null;
    public static final String AUTOHORITY = "com.example.zhaoziliang";
    // 设置ContentProvider的唯一标识

    public static final int User_Code = 1;
    public static final int Job_Code = 2;

    // UriMatcher类使用:在ContentProvider 中注册URI
    private static final UriMatcher mMatcher;
    static{
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mMatcher.addURI(AUTOHORITY,"user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
        // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
        // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
    }

    // 以下是ContentProvider的6个方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {

        mContext = getContext();
        // 在ContentProvider创建时对数据库进行初始化
        // 运行在主线程,故不能做耗时操作,此处仅作展示
        mDbHelper = new DBHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        // 初始化两个表的数据(先清空两个表,再各加入一个记录)
        db.execSQL("delete from user");
        db.execSQL("insert into user values(1,'Carson');");
        db.execSQL("insert into user values(2,'Kobe');");

        db.execSQL("delete from job");
        db.execSQL("insert into job values(1,'Android');");
        db.execSQL("insert into job values(2,'iOS');");

        return true;
    }

    /**
     * 添加数据
     */

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

        // 向该表添加数据
        db.insert(table, null, values);

        // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
        mContext.getContentResolver().notifyChange(uri, null);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        return uri;
        }

    /**
     * 查询数据
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        // 查询数据
        return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    /**
     * 更新数据
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    /**
     * 删除数据
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    @Override
    public String getType(Uri uri) {

        // 由于不展示,此处不作展开
        return null;
    }

    /**
     * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = DBHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = DBHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
        }
    }

进程二:
ContentResolver通过对应匹配的uri去调用对应的进程一的contentprovider的不同的表进行增删改查操作


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /**
         * 对user表进行操作
         */

        // 设置URI
        Uri uri_user = Uri.parse("content://com.example.zhaoziliang/user");

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("_id", 4);
        values.put("name", "zzl");


        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(uri_user,values);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标

        /**
         * 对job表进行操作
         */
        // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
        Uri uri_job = Uri.parse("content://com.example.zhaoziliang/job");

        // 插入表中数据
        ContentValues values2 = new ContentValues();
        values2.put("_id", 4);
        values2.put("job", "LOL Player");

        // 获取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver2.insert(uri_job,values2);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 将表中数据全部输出
        }
        cursor2.close();
        // 关闭游标
    }
}

6.使用Socket(套接字)
socket实现进程间通信,分为:

  • TCP协议(传输控制/流式套接字):面向连接的协议,提供稳定的双向通信功能,三次握手四次挥手,这个次数是保证安全又高效。
  • UDP协议(用户数据报套接字):面向无连接,不稳定,不安全,不保证数据一定能传输到,但效率高。
    具体就是我们平常使用的网络请求,有兴趣的话自己写个简单的服务器聊天室。。。。。。
Binder连接池

随着项目越来越大,很多业务模块都需要使用AIDL来通信,我们还需要了解一下Binder连接池的原理,但我真的写吐了,以后再写吧。。。。

最后总结一下他们的使用场景

比较使用场景

结尾 :终于结束了,Don’t Say So Much。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350