前言
在网上有很多关于android跨进程通信的文章,但是大多数看了之后还是不知所云,一脸懵逼,最近抽时间整理了一下这方面的知识,为了能让大家从应用层上清楚地了解跨进程通信的实现原理,我会将我所讲的内容分为多进程、Binder和这两个部分,接下来我会按照我的思路给大家一一讲解。
多进程
在讲跨进程通信之前,我们必须先要了解Android中的多进程模式。通过在四大组件中指定android:process
属性,便可以轻易的开启多进程模式,这样似乎看起来很简单,其实并不是这样,多进程远远没有我们想象的这么简单,有时候我们通过多进程得到的好处甚至都不足以弥补使用多进程带来的代码层面的负面影响。下面是一个例子,描述了如何在Android中创建多进程:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".FirstActivity"
android:label="@string/app_name"
android:process=":remote" />
<activity android:name=".SecondActivity"
android:label="@string/app_name"
android:process="com.process.demo.remote"/>
上面的FirstActivity和SecondActivity都指定了process
属性,但是属性值不同,相当于当前应用又新添加了两个进程,而MainActivity没有指定process
属性,那么它是运行在默认的进程中,而系统默认的进程就是当前应用的包名;FirstActivity中的process
属性值为:remote
,这个:
的含义是指在当前进程名前面附加当前的包名,也就是说FirstActivity的完整进程名为com.process.demo.remote;而对于SecondActivity中的process
是一种完整命名方式,不会附加包名信息。另外,进程名以:
开头的进程属于当前应用的私有进程,其它应用的组件不能和它跑在一个进程中,而进程名不以:
开头的进程属于全局进程,其它应用通过ShareUID
的方式可以和它跑在同一个进程中,Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据,关于ShareUID的详细信息大家可以查一下资料,我就不多做解释了。
然后继续,将程序运行起来后,通过adb命令(adb shell ps 包名)查看当前应用下的进程列表,如图所示:
PID
为当前进程的id,可以发现这三个id都不一样,也就是说成功的开启了三个进程。而在前面我们说到使用多进程会产生一些负面影响,接下来验证一下这个问题。
首先我们新建一个Constant类,并声明一个int
类型的静态常量
public class Constant {
public static int id = 1;
}
接着在MainActivity的'onCreate()'方法中修改这个id
值,给它改为2
,最后在MainActivity和FirstActivity中分别去打印这个id
值,打印出来的日志如下:
看了图中的日志,发现结果貌似跟我们想象中的不一样啊,FirstActivity打印出来的值咋还是1
呢,我们的确在MainActivity给id
赋值了,然后在MainActivity又启动了FirstActivity。到这里,想必大家已经明白了这就是多进程所带来的问题。所以说,多进程并非我们想象中的那么简单。
出现以上结果原因就在于MainActivity和FirstActivity是两个不同的进程,而Android会为每个应用分配一个独立的虚拟机,或者说为每个进程分配一个独立的虚拟机,而每个独立的虚拟机都有它自己独享的内存,不同的虚拟机在内存分配上有不同的地址空间,因此不同进程是无法共享内存中的数据的,对于上面例子而言,其实com.process.demo
和com.process.demo.remote
这两个进程中都有一个Constant类,而且这两个类是互不干扰的,所以说在FirstActivity中的id
值并没有发生改变。
最后我们总结一下,使用多进程造成的如下几方面的问题:
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharedPreferences的可靠性下降
- Application会多次创建
第一个问题已经分析;第二个问题其实和第一个本质上是一样的,想想就知道了,进程都不一样了,锁的对象都不是同一个对象了,还怎么去保证线程同步;第三个问题是因为SharedPreferences不支持两个进程去同时执行写的操作,否则会导致一定几率的数据丢失,这是因为SharedPreferences底层是通过读/写XML文件来实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题;第四个问题也是显而易见的,当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动了一个应用的过程,因此会重新创建新的Application,不信的话,大家可以去测试一下。
Binder
上面我们分析了多进程所带来的问题,那么接下来就是讲解如何解决它,其实系统提供了很多跨进程通信的方法,虽然不能共享内存但是通过跨进程通信我们还是可以实现数据交互。其实Android的这四大组件中都可以实现跨进程通信,而AIDL也是处理进程间通信的一种方式,AIDL跨进程的核心实现其实就是Binder,接下来我们就详细分析一下Binder的实现原理。
先写一个简单的例子:
比如我有一个需求,服务端提供给客户端添加书籍和获取所有书籍列表的方法。
1、在包名目录下创建一个bean目录,在bean目录下创建一个Book实体类
然后让Book实现Parcleable接口,代码如下:
public class Book implements Parcelable{
public int bookId;
public String bookName;
protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
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 dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
}
2、 创建Book.aidl、IBookManager.aidl文件。Book.aidl是Book类在AIDL中的声明,IBookManager.aidl是我们定义的一个接口,里面有两个方法:getBookList
和addBook
,其中getBookList
用来获取服务端书籍列表,addBook
用来往服务端添加书籍,这里需要注意的是 Book类和Book.aidl所在的包名要一致,不然AS会编译失败,如图所示:
还需要在IBookManager.aidl中使用import导入Book类
package com.kdp.aidl;
import com.kdp.aidl.bean.Book;
interface IBookManager {
void addBook(in Book book);
List<Book> getBookList();
}
Book.aidl代码如下:
package com.kdp.aidl.bean;
parcelable Book;
最后编译一下,AS会自动帮你生成一个IBookManager.java的一个类,该类所在目录如下:
而这个类就是Binder的核心实现,现在我们需要对这个类进行分析,先看一下整体代码:
package com.kdp.aidl;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.kdp.aidl.IBookManager
{
private static final java.lang.String DESCRIPTOR ="com.kdp.aidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.kdp.aidl.IBookManager interface,
* generating a proxy if needed.
*/
public static com.kdp.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.kdp.aidl.IBookManager))) {
return ((com.kdp.aidl.IBookManager)iin);
}
return new com.kdp.aidl.IBookManager.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@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;
}@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_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.kdp.aidl.bean.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.kdp.aidl.bean.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.kdp.aidl.bean.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.kdp.aidl.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public void addBook(com.kdp.aidl.bean.Book book) 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 ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
@Override public java.util.List<com.kdp.aidl.bean.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.kdp.aidl.bean.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.kdp.aidl.bean.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_addBook =
(android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getBookList =
(android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public void addBook(com.kdp.aidl.bean.Book book) throws android.os.RemoteException;
public java.util.List<com.kdp.aidl.bean.Book> getBookList() throws android.os.RemoteException;
}
粗略的看了一下,发现这里面有很多内部类,我们先看最外层的这个IBookManager,它是一个接口,此接口定义了两个方法,分别是addBook
,getBookList
,另外还有两个常量id,
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
这两个常量id是服务端用来区分这两个方法的,当客户端发起一个请求时,服务端会根据这个id来判断客户端请求的是哪个方法;此外IBookManager继承了 android.os.IInterface
接口,该接口中只定义了一个方法,如图:
这个方法表示返回当前Binder对象。
接下来我们继续往下看,在IBookManager内部有一个Stub类,而且是抽象的,这个Stub你有没有感觉很熟悉,其实它就是我们在Service中需要实例化的Binder对象,我们一般会在Service中这样做
而这个Stub继承了android.os.Binder
并且实现了IBookManager接口类,我们看Stub的构造方法,里面有一个attachInterface
方法
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
第一个参数不用多说,第二个参数DESCRIPTOR
是Binder的唯一标识,一般用当前Binder的类名表示,比如本例中的com.kdp.aidl.IBookManager"
。点开进入此方法内部,发现这个方法在Binder类中做了如下赋值:
将Binder类的唯一标识赋值给了mDescriptor
变量,现在大家肯定有疑问,这个mDescriptor
变量到底有什么用?当然有用了,接下来我们会用到它。
下面接着看IBookManager.java这个类,在Stub构造方法下面有一个asInterface
的静态方法,
这个方法想必大家都不会陌生,这个就是我们在客户端需要调用的方法,我们一般在客户端这样写:
IBookManager binder = IBookManager.Stub.asInterface(binder);
此方法传入的是服务端的Binder对象,而这个方法就是用来将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。
在这个方法中我们发现有一个queryLocalInterface
方法,此方法就是用来判断客户端和服务端是否位于同一进程,该方法传入当前Binder类的唯一标识,进入此方法内部来看看里面做了什么?
我们发现传入的DESCRIPTOR
要跟mDescroptor
变量做对比,等等,这个mDescroptor
不就是我们在attachInterface
方法中赋值的那个变量吗?没错,也就是说,当客户端和服务端位于同一个进程时,mDescroptor
变量和我们传入的DESCRIPTOR
是相同的,所以直接就会返回当前的Binder对象本身;如果客户端和服务端不在同一个进程,那么客户端所在进程中的mDescriptor
变量就会为空,由于我们只在服务端通过new的方式来实例化了Stub对象并给mDescriptor
赋值,也就是说服务端所在的进程中mDescriptor
变量是有值的,但是在客户端只是通过asInterface
方法来获取Stub对象,而并没有给mDescriptor
变量赋值,所以客户端所在进程中的mDescriptor
变量是空的,回想上面我们讲多进程数据共享的时候举过的一个例子,内存中的数据只有在同一进程才会被共享。
当客户端和服务端不在同一个进程时,asInterface
方法则会返回系统封装后的Stub.Proxy对象,将服务端的Binder对象通过构造方法传了进去,并且这个Proxy是Stub中的一个内部类,该类也实现了IBookManager接口,并实现了addBook
和getBookList
这两个方法,而这两个方法是我们在客户端需要调用的,比如当客户端调用getBookList
方法时,如下图所示:
此方法内部首先会创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;
然后把该方法的参数信息写入_data中(如果有参数的话),接着会调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的onTarnsact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。
在调用transact方法发起RPC请求时,服务端的onTarnsact
方法会被调用,我们进入transact
方法的内部会发现
此方法中调用了onTransact
方法,这个方法也是定义在系统的Binder类中,我们之前讲了Stub类继承自Binder,那么在我们的Stub类中重写了Binder类中的onTransact
方法,而此方法是运行在服务端的Binder线程池中,当客户端发起请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法原型为`public Boolean onTransact(int code,android.os.Parcel data,android.os.Parcel.reply,int flags)。
看一下该方法内部做了什么
@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;
}@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_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.kdp.aidl.bean.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.kdp.aidl.bean.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.kdp.aidl.bean.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
在该方法中,服务端会根据传入的code来判断当前客户端请求的是哪个方法,这个code就是这两个方法的id,然后服务端就会调用相应的目标方法,比如当客户端调用getBookList
方法时,最终实现此方法是在Sercvice中(因为我们是在Service中new Stub然后实现IBookManager接口中的这两个方法的),而调用是在服务端的onTransact
方法中,服务端会先判断code,然后再调用getBooList
方法拿到数据,之后便将List<Book>
序列化后写入到reply这个Parcel对象中,接着我们再回到客户端的getBookList
方法
@Override public java.util.List<com.kdp.aidl.bean.Book> getBookList()
throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.kdp.aidl.bean.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.kdp.aidl.bean.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
在调用Binder的transact
方法之后,此时的_reply
中已经有了客户端所需要的数据,然后将_reply
反序列化取出List<Book>
,返回给客户端,这样一个完整的远程请求就结束了
最后给大家看一下Binder的工作机制图
Binder的工作机制图.png到这里,Binder机制就讲完了,如果还有什么问题请留言。
网友评论