Android中的 IPC 方式之使用 AIDL 跨进程通信

Posted on Posted in Android

1. 什么是IPC跨进程通信

IPC 是 Inter-Process Communication 的缩写,含义为进程间通信或跨进程通信。指的是两个进程间进行数据交换的过程。既然要了解进程间通信,那我们就要先了解什么是进程,进程和线程是两个截然不同的概念。线程指 CPU 调度的最小单元,同时线程也是一种有限的系统资源;而进程一般指一个执行单元,也可以理解为一个程序或一个应用。一个进程可以包含多个线程,最最简单的情况一个进行可以只有一个线程,在 Android 中我们称之为主线程,也即 UI 线程,众所周知 只能在 UI 线程才能操作界面元素,并且我们通常会把耗时的操作放到子线程中去执行来避免 UI 的无响应(ANR)。

默认情况下,我们的应用都是运行在单进程的模式下,因此也就无需跨进程通信,只有应用在多进程模式下时,才需要 IPC 机制进行通信。那么什么时候需要开启多进程模式呢?原因可以有很多,例如一些模块由于特殊原因需要运行在单独的进程;又或者通过开启多进程来分配更多的内存。还有一种需要跨进程通信的场景就是一个应用向另一个应用获取数据的时候,由于是两个不同的应用,因此必须通过进程间通信来实现。

在 Android 中开启多进程模式只有一种方法,就是在 AndroidManifest 中为四大组件指定 android:process 属性。除此之外别无他法,因此我们也就不能单独地指定一个线程或者一个实体类运行于哪个进程。另外还有一种特殊的情况是通过 JNI 在native 层去 fork 一个新的进程,但这不是常用的方式,一般不考虑。

下面是在 AndroidManifest.xml 中为 activity和 service 开启多进程的示例

<activity
            android:name=".activity.SecondActivity"
            android:process=":remote"/>

<activity
            android:name=".activity.ThirdActivity"
            android:process="com.chaoyang805.chaptertwo_ipc.remote"/>
<service
            android:name=".service.BookManagerService"
            android:process=":remote"/>

然后我们运行程序,开启相应的组件后,可以在 AndroidStudio 的 AndroidMonitor 中看到相应的进程信息:

也可以通过 adb shell ps | grep com.chaoyang805.chaptertwo命令来查看当前的进程信息:

其中以:remote方式指定的进程,属于当前应用的私有进程,并且这是一种简写的形式 : 前面会附加上完整的包名,才是当前的进程名;而以完整包名命名的方式属于全局进程,其他应用可以通过 ShareUID 的方式和它跑在同一个进程中。

2. Android 中跨进程通信的方式

任何一个操作系统中都有 IPC 机制,如 Windows 中通过剪贴板、管道、油槽来实现 IPC;Linux 上以命名管道、共享内存、信号量 来实现 IPC;虽然 Android 是一种基于 Linux 的操作系统,但在跨进程通信方面却不尽相同。它也有自己的 IPC 机制, Binder 就是 Android 中比较有特色的跨进程通信的方式, AIDL 也是基于这种方式来实现的,除此之外 Messenger、ContentProvider 也是 Android 中基于 Binder 来实现的IPC 方式。除了这几种方式,还可以通过 Bundle、Socket 以及文件共享的方式来实现跨进程通信。

3. 多进程模式存在的问题

为应用开启多进程模式并不仅仅是增加一个 android:process 属性这么简单,同时它还会带来一些意想不到的副作用。我们知道 Android 中会为每一个应用或者说每一个进程分配一个独立的虚拟机,不同的虚拟机的内存分配有不同的地址空间,这就会导致在不同的虚拟机中访问同一个类的对象会产生多个不同的副本,举个例子:假如我们有一个类UserManager,它有一个静态变量默认值为1,我们在应用的主进程给它赋值为2,然后再在第二个进程中打印这个变量,你会发现打印的时候这个变量的值仍然是1。这就是由于多进程模式使得这个类在不同的进程中都有一份副本,我们第一次改变的只是当前进程所在的那份内容,并不会影响到另一个进程中去。
所以运行在多进程的四大组件不可能简单的通过内存来共享数据,而是需要一种中间方式。
一般来说,开启多进程会造成如下的问题:
1. 静态成员和单例模式完全失效(如上面所讲)
2. 线程同步机制完全失效(锁的不是同一个对象)
3. SharedPreferences 可靠性下降(SharedPreferences 不支持多个进程同时去执行写操作)
4. Application 会多次创建(系统要分配新的虚拟机,相当于启动一个应用)

4. 什么是 AIDL

AIDL 是 Android Interface Definition Language (Android 接口定义语言) 的缩写,它为我们提供了一种简便的语发格式来编写跨进程通信代码的方式。使用这种方式编写跨进程通信接口,系统会自动为我们生成基于 Binder 实现的跨进程通信所需的类,由于都是一些样板代码,因此使用这种方式可以大大提高工作效率。

5. AIDL 的使用方法

使用 AIDL 的一般方法是在服务器端先创建相应的 AIDL 文件;然后系统会根据 AIDL 自动为我们生成对应的 Binder 接口;接着就可以在 Service 的 onBind 方法中返回由这个 Binder 接口的实现了;最后在客户端绑定这个 Service,并在 onServiceConnected 回调方法中将传过来的 IBinder 对象转换成对应的 AIDL 接口所生成的对象。
下面用具体的例子演示一下 AIDL 的使用

1. 编写 AIDL 文件

按上面所说的,第一步就是创建 AIDL 文件;为了方便开发,通常会把所有的 AIDL 文件放到同一个包下面,这么做的好处是当客户端是另一个单独的应用时,可以直接把整个包复制到客户端的工程中去。这也是由于服务器端 AIDL 的包结构需要和客户端中的包结构保持一致,这样客户端在反序列化时才能够成功。
一个好消息是在 AndroidStudio 中,当我们选择新建一个 AIDL 文件时,它会默认帮我们将文件创建在具有相同包结构的 aidl 文件夹下,而不是我们所选择的 java 文件夹,如下图中我选择在 java 文件夹下的 com.chaoyang805.chaptertwo_ipc.avtivity 的包下新建 AIDL 文件:

并且可以看到当前 aidl 文件夹下并没有 activity 这个包,当我输入文件名,点击ok,AndroidStudio 就会自动把我创建的 AIDL 文件移动到 aidl 文件夹下对应的包中,是不是很智能? 然后以 IBookManager.aidl 为例,来看 aidl 文件的书写格式

// IBookManager.aidl
package com.chaoyang805.chaptertwo_ipc;

// Declare any non-default types here with import statements
import com.chaoyang805.chaptertwo_ipc.Book;
import com.chaoyang805.chaptertwo_ipc.IOnNewBookArrivedListener;
interface IBookManager {

    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unregisterListener(IOnNewBookArrivedListener listener);
}

在 AIDL 中并不是所有的数据类型都能够使用的,它只支持以下几种类型:
* 基本数据类型(int、long、char、boolean、double、short 等)
* String 和 CharSequence
* List: 只支持 ArrayList, 里面的每个元素都要被 AIDL 支持
* Map: 只支持 HashMap 里面的每个元素的 key 和 value 都需要被 AIDL 支持
* Parcelable: 实现了 Parcelable 序列化接口的对象
* AIDL: 所有的 AIDL 接口本身都能在 AIDL 接口中使用

其中自定义的类型和 AIDL 都需要显式的 import 导入进来,即使它们处在同一个包中;正如上面的 IBookManager.aidl 中所用到的 Book 类就是一个实现了 Parcelable 的自定义对象;另外除了基本数据类型,其它的都需要手动标上参数的方向:in、out或者inout,分别表示输入型参数、输出型参数和输入输出型参数。
关于 Parcelabel 序列化这里不再多说,可以自行了解。
下面是 Book 类的具体实现

package com.chaoyang805.chaptertwo_ipc;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Created by chaoyang805 on 16/7/25.
 */
public class Book implements Parcelable {
    public int bookId;
    public String bookName;

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    protected Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }


    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flag) {
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    @Override
    public String toString() {
        return bookId + " " + bookName;
    }
}

除此之外,自定义的对象还需要创建同名的 aidl 文件,并在其中声明为它为 Parcelable 类型,下面是 Book.aidl 的内容:

// Book.aidl
package com.chaoyang805.chaptertwo_ipc;

// Declare any non-default types here with import statements

parcelable Book;

然后我们 AndroidStudio 的 Build 菜单中 Make 一下当前的 Project,AndroidStudio 就会自动为我们创建所需的接口,创建好的文件如下图所示:

到此为止,AIDL 接口我们就创建好了,下一步就是实现服务端的service。

2. 实现服务端的service

这一步和创建 普通的 service 类似,不同的是需要为当前的 service 指定运行的进程:

<service android:name=".service.BookManagerService"
            android:process=":remote"/>

这里我们创建了 BookManagerService 并指定了它所在的进程为 :remote。
然后重写 service 的 onBind 方法,并返回对应的binder,这里我们要返回的是 AndroidStudio 自动为我们生成的 IBookManager 接口的对象,在它内部有一个 Stub 抽象类,我们就是要返回这个类的实现:

private IBookManager mBookBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBooks;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBooks.add(book);
        }
    };

mBookBinder 中我们实现了 定义在 IBookManager 中的两个抽象方法,在 getBookList 返回了一个 mBooks 对象, 在 addBook 方法中 调用 mBookadd 方法增加一个元素。
下面是BookManagerService 类中的完整实现

public class BookManagerService extends Service {
    private CopyOnWriteArrayList<Book> mBooks;
    private IBookManager mBookBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBooks;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBooks.add(book);
        }
    };
    
    @Override
    public void onCreate() {
        super.onCreate();
        mBooks = new CopyOnWriteArrayList<>();
        mBooks.add(new Book(1, "Android"));
        mBooks.add(new Book(2, "iOS"));
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBookBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}    

在服务器端,我们用了一个 CopyOnWriteArrayList 的集合,这是为了实现服务器端的并发读写,但之前说过 AIDL 中只支持 ArrayList , 为什么还能正常工作呢?这是因为 AIDL 中支持的是抽象的 List 接口, 虽然这时服务端返回的是一个 CopyOnWriteArrayList, 但是在 Binder 中会按照 List 去读取,并返回一个新的 ArrayList 给客户端,所以在服务器端使用 CopyOnWriteArrayList 是完全可以的, 类似的还有 ConCurrentHashMap
然后其他部分就和普通的 service 的写法基本一致了。下面就来实现客户端的代码。

3. 客户端绑定service

在客户端,为了方便演示,这里在同一个工程下进行,新建一个 BookManagerActivity 并在 AndroidManifest 中注册到默认进程中。然后在这个 Activity 中 调用 bindService 绑定即可:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bookmanager);
        bindService(new Intent(this, BookManagerService.class),
        mConnection, 
        Context.BIND_AUTO_CREATE);
    }

接着我们需要在 mConnectiononServiceConnected 回调中将返回的 Binder 对象转换成 AIDL 接口。通过这个接口就可以去调用服务器端的方法了:

private IBookManager mBookManager;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            IBookManager manager = BookManagerImpl.asInterface(iBinder);
            mBookManager = manager;
            try {
                List<Book> books = manager.getBookList();
                Log.d(TAG, "query book list, list type:" + 
                books.getClass().getCanonicalName());
                Log.d(TAG, "query book list:" + books.get(0).bookName);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };

这里通过调用 IBookManager.StubasInterface 方法就能把返回的 IBinder 对象转换成 AIDL 接口对象, 然后调用服务端的方法。
下面是BookManagerActivity 中的完整代码

public class BookManagerActivity extends AppCompatActivity {
    private static final String TAG = "BookManagerActivity";

    private IBookManager mBookManager;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            IBookManager manager = BookManagerImpl.asInterface(iBinder);
            mBookManager = manager;
            try {
                mBookManager.registerListener(mListener);
                List<Book> books = manager.getBookList();
                Log.d(TAG, "query book list, list type:" + 
                books.getClass().getCanonicalName());
                Log.d(TAG, "query book list:" + books.get(0).bookName);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bookmanager);
        bindService(new Intent(this, BookManagerService.class),
        mConnection, 
        Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

运行程序,就可以看到打印的如下log,说明我们成功连接上了服务器端,并成功调用了服务器端的方法:

4. 通过回调监听服务器端

有时候我们不仅要能够调用服务端的方法,还想监听服务器端的数据变化,针对这个功能,首先想到的就是使用回调。在 AIDL 中使用回调,和普通情况下的回调类似,同样是添加 Listener 接口与注册、注销监听的方法,只不过在 AIDL 中,回调接口需要用 AIDL 来实现。下面我们就给上面的例子增加一个 OnNewBookArrivedListener 的接口,来监听新增 book 的变化。下面是 IOnNewBookArrivedListner.aidl 的具体实现:

// IOnNewBokkArrivedListener.aidl
package com.chaoyang805.chaptertwo_ipc;

// Declare any non-default types here with import statements
import com.chaoyang805.chaptertwo_ipc.Book;

interface IOnNewBookArrivedListener {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void onNewBookArrived(in Book book);
}

和声明普通的 Listener 接口没什么两样,唯一的区别是指定参数的类型为传入参数。
然后在原来的 IBookManager.aidl 中添加两个用来注册、注销监听的方法,同样需要显式导入 IOnNewBookArrivedListener 所在的包:

// IBookManager.aidl
package com.chaoyang805.chaptertwo_ipc;

// Declare any non-default types here with import statements
import com.chaoyang805.chaptertwo_ipc.Book;
import com.chaoyang805.chaptertwo_ipc.IOnNewBookArrivedListener;
interface IBookManager {

    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unregisterListener(IOnNewBookArrivedListener listener);
}

然后 Make Project 生成所需的 Java 类。
重新构建完工程后还需要在 BookManagerService 中为 IBookBinder实现刚添加的这两个方法:

private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = 
        new CopyOnWriteArrayList<>();

private IBookManager mBookBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBooks;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBooks.add(book);
        }
        
        @Override
        public void registerListener(IOnNew BookArrivedListener listener) throws RemoteException {
             if (!mListenerList.contains(listener)) {
                    mListenerList.add(listener);
                    Log.d(TAG, "add listener" + mListenerList.size());
             } else {
                    Log.d(TAG, "listener already exists");
             }
        }
        
        @Override
           public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
                 if (mListenerList.contains(listener) {
                        mListenerList.remove(listener);
                        Log.d(TAG, "listener unregister success");
                 } else {
                        Log.d(TAG, "listener " + listener + "not found unregister failed");
                 }
           }
    };

为了演示,这里开启一个线程用来循环向 mBooks 中添加新的元素,下面是修改后的 BookManagerService 中的代码:

public class BookManagerService extends Service {

    private CopyOnWriteArrayList<Book> mBooks;
    private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<>();

    private BookManagerImpl mBinder;
    private AtomicBoolean mIsServiceDead = new AtomicBoolean(false);

    @Override
    public void onCreate() {
        super.onCreate();
        mBooks = new CopyOnWriteArrayList<>();
        mBooks.add(new Book(1, "Android"));
        mBooks.add(new Book(2, "iOS"));
        new Thread(new ServiceWorker()).start();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        mBinder = new BookManagerImpl(mBooks, mListenerList);
        return mBinder;
    }

    @Override
    public void onDestroy() {
        mIsServiceDead.set(true);
        super.onDestroy();
    }

    private void onNewBookArrived(Book book) throws RemoteException {
        mBooks.add(book);
        Log.d(TAG, "onNewBookArrived, notify listeners");
        for (int i = 0; i < mListenerList.size(); i++) {
                    mListenerList.get(i).onNewBookArrived();
        }
    }

    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            while (!mIsServiceDead.get()) {

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                int bookId = mBooks.size() + 1;

                Book newBook = new Book(bookId, "bookName#" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后在客户端绑定服务时注册 listener, 就能收到服务端的回调了:

private IOnNewBookArrivedListenerImpl mListener = new IOnNewBookArrivedListenerImpl() {
        @Override
        public void onNewBookArrived(Book book) throws RemoteException {
            Log.d(TAG, "onNewBookArrived: " + book.bookName);
        }
    };

private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            IBookManager manager = BookManagerImpl.asInterface(iBinder);
            mBookManager = manager;
            try {
                mBookManager.registerListener(mListener);
                List<Book> books = manager.getBookList();
                Log.d(TAG, "query book list, list type:" + books.getClass().getCanonicalName());
                Log.d(TAG, "query book list:" + books.get(0).bookName);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
};

@Override
protected void onDestroy() {
   if (mBookManager != null && mBookManager.asBinder().isBinderAlive()) {
       try {
           Log.d(TAG, "unregisterlistener:" + mListener);
           mBookManager.unregisterListener(mListener);
       } catch (RemoteException e) {
           e.printStackTrace();
       }
   }
   unbindService(mConnection);
   super.onDestroy();
}

这时候运行程序,就可以收到服务器每隔2s新增元素的回调了。但是这里还有一个问题,当退出客户端的activity时,你会发现 unregisterListener 方法执行后并没有打印出 Log.d(TAG, "listener unregister success"); 而是移除失败了。这时为什么呢?这也比较容易解释,因为之前说过,不同的进程是运行在不同的虚拟机中的,而跨进程通信时序列化后的数据反序列化时只是创建了一个内容相同的对象,并不是原来那个对象,因此客户端注册时的 listener 对象和服务端注销时的 listener 就不可能是同一个对象了,这时再调用 mListenerList.contains 方法肯定会返回 false。那么这个时候就不能再使用CopyOnWriteArrayList 或者 ArrayList 了。那么该用什么来实现呢?

**5. 使用 RemoteCallbackList **

其实 Android 已经为我们设计好了相应的类供我们使用,这就是 RemoteCallbackList 这是一个专门用来实现可删除的跨进程 listener 的接口:

public class RemoteCallbackList<E extends IInterface>

它也是一个泛型集合,而且它管理的元素都是 AIDL 类型的接口,因为所有的 AIDL 接口都继承自 IInterface 这点在以后会说到。
然后我们只需要把原来的 CopyOnWriteArrayList 替换成 RemoteCallbackList 就行了,对应的还需要使用 RemoteCallbackList 的用法重新实现如下方法:

    @Override
    public void registerListener(IOnNewBookArrivedListener listener) {
        mListenerList.register(listener);
    }

    @Override
    public void unregisterListener(IOnNewBookArrivedListener listener) {
        mListenerList.unregister(listener);
    }
    // BookManagerService 中的onNewBookArrived 方法
    private void onNewBookArrived(Book book) throws RemoteException {
        mBooks.add(book);
        final int N = mListenerList.beginBroadcast();

        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener broadcastItem = 
            mListenerList.getBroadcastItem(i);
            if (broadcastItem != null) {
                broadcastItem.onNewBookArrived(book);
            }
        }

        mListenerList.finishBroadcast();
    }

注意:使用 RemoteCallbackList 不能像 List 一样去操作它,因为其实它并不是一个 List,使用的时候 beginBroadcastfinishBroadcast 必须配对使用。

然后再运行程序,这一次就能够正确的注销了。

6. 为客户端设置 DeathRecipient

由于服务端的 Binder 可能会意外死亡,例如服务端所在的进程意外终止了,这个时候我们就需要重连服务,Android 为我们提供了 DeathRecipient 来实现客户端对服务端状态的监听,当 Binder 死亡时,就会收到 binderDied 的回调,然后我们就可以在这里重连服务。
下面是 DeathRecipient 的实现,我们在 binderDied 的回调中断开死亡监听,并重新绑定服务:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
bindService(new Intent(BookManagerActivity.this, BookManagerService.class),mConnection, Context.BIND_AUTO_CREATE);
}
};

然后在 onServiceConnected 的回调里添加死亡监听就行了:

iBinder.linkToDeath(mDeathRecipient, 0);

以上就是 Android 中使用 AIDL 进行 IPC 跨进程通信的使用方法,最后还有一点:由于客户端调用的远程服务的方法是运行在服务端的 Binder 线程池中的,这时客户端线程会被挂起,因此如果服务端方法执行了比较耗时的操作,就会导致客户端这边长时间的阻塞,如果又刚好是 UI 线程,就容易出现 ANR,所以就要避免在 UI 线程中去执行远程方法,并且由于客户端的 onServiceConnected 和 onServiceDisconnected 都是在 UI 线程回调的,所以也不能在这里直接执行服务端的远程方法,最好是把远程方法的调用放到子线程中去,然后再通过 handler 来通知 UI 线程。

发表评论

电子邮件地址不会被公开。 必填项已用*标注