Android IPC 简介:
IPC是Inter-Process Communication的缩写,就是进程间通信或者跨进程通信的意思,指的是两个进程之间进行数据交换的过程。这里简单讲一下进程和线程的区别:进程指的是一个程序,在Android中指的就是一个app;线程是cpu调度的最小单元,我的理解是线程是执行单线任务的,一般来说,每个app都有主线程,主线程相当于主线剧情,不论发生的事件还是执行流程都是围着他进行的。其他线程相当于支线任务,主要是丰富和扩展主线的。因此,一个进程可包含多个线程。
在Android想要进行线程间的通信,大家都很熟悉Handler,Asynctask,线程池等。但是说到进程间的通信,大家可能了解的不多。事实上,android中实现多进程的方式也是多种多样,他们每个都有自己优缺点,今天主要介绍一下AIDL的通信方式。
Android AIDL 实现:
在正式实现之前,我们需要搞懂几个基础概念。首先我们知道Aidl分为服务端和客户端。
1.服务端:
服务端就是你要连接的进程。他提供给客户端一个Service,在这个Service中监听客户端的连接请求,然后创建一个AIDL接口文件,里面是将要实现的方法,注意这个方法是暴露给客户端的的。最后在Service中实现这个AIDL接口即可(这里是接口的具体实现)。服务端的职责是提供连接和自身
2.客户端:
客户端首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转换成AIDL接口所属的类型,最后调用AIDL的方法就可以了。可以看到,客户端还是比较简单的,负责连接和调用。
3.AIDL所支持的数据类型
在AIDL中,并非支持所有数据类型,他支持的数据类型如下所示:
● 基本数据类型(int、long、char、boolean、double、float、byte、short)
● String和CharSequence
● List:只支持ArrayList,并且里面的每个元素必须被AIDL支持
● Map: 只支持HashMap, 同样的,里面的元素都必须被AIDL支持,包括key和value
● Parcelable:所有实现了Parcelable接口的对象
● AIDL: 所有的AIDL接口本身也可以在AIDL 文件中使用
以上就是AIDL所支持的所有类型,其中自定义的Parce对象和AIDL对象必须要显式的import进来,不管它们是否和当前的AIDL文件在同一个包中。另外需要注意的一点是,如果AIDL文件用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。除此之外,AIDL除了基本类型,其他类型的参数都必须标上方向:in、out或者inout,in标上输入型参数,out表示输出型参数,inout表示输入输出型参数。
好的,准备工作完成。接下来正式开始了。假设XX资讯公司某天接了个业务,公司领导决定和XX招聘合作。需求是这样的,用户在浏览资讯的时候,会不时的插播一条招聘广告(万恶的广告啊)。就这么个简单的需求,用AIDL怎么实现。
服务端实现:
首先是招聘广告对象,这个类是一个招聘的具体内容:
```
package com.example.aykon.aidltest.AD;
import android.os.Parcel;
import android.os.Parcelable;
public class Advert implements Parcelable{
//职位
private String position;
//薪资
private int salary;
//具体内容
private String content;
public Advert(String position, int salary, String content) {
this.position = position;
this.salary = salary;
this.content = content;
}
protected Advert(Parcel in) {
position = in.readString();
salary = in.readInt();
content = in.readString();
}
public static final Creator CREATOR = new Creator() {
@Override
public Advert createFromParcel(Parcel in) {
return new Advert(in);
}
@Override
public Advert[] newArray(int size) {
return new Advert[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(position);
dest.writeInt(salary);
dest.writeString(content);
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
```
这个类就是需要用的实体类,因为是跨进程,所以实现了Parcelable接口,这个是Android官方提供的,它里面主要是靠Parcel来传递数据,Parcel内部包装了可序列化的数据,能够在Binder中自由传输数据。剩下代码十分简单,声明了职位、工资、具体内容3个字段。提供相关的构造方法,getter()和setter()方法。接着就是需要重写的方法,大致是提供一个读一个写两个方法,具体的含义这里不深究。
之前说过,如果用到了自定义Parcelable对象,就需要创建一个同名的AIDL文件。
// Advert.aidl
package com.example.aykon.aidltest;
parcelable Advert;
数据有了保障,然后就是给客户端提供获取数据的方法。在这里就是创建AIDL接口,具体就是招聘广告的AIDL文件,这个接口里暂时提供2个方法,为什么说暂时,因为需求从来没确定过。诶!一个是获取所有的广告,再一个就是添加一条广告。
// IAdvertManager.aidl
package com.example.aykon.aidltest;
import com.example.aykon.aidltest.Advert;
interface IAdvertManager {
List getAdvertList();
void addAdvert(in Advert ad);
}
好了,接口有了,服务端最后一步,提供给客户端连接的service,并实现广告接口。
public class AdvertManagerService extends Service{
private CopyOnWriteArrayList mAdvertList = new CopyOnWriteArrayList<>();
//核心,Stub里面的方法运行的binder池中。
private Binder mBinder = new IAdvertManager.Stub(){
@Override
public List getAdvertList() throws RemoteException {
return mAdvertList;
}
@Override
public void addAdvert(Advert ad) throws RemoteException {
mAdvertList.add(ad);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
mAdvertList.add(new Advert("Android", 10, "app开发"));
mAdvertList.add(new Advert("ios", 10, "ios开发"));
}
}
可以看到,在onCteate()方法里添加了两条假数据,关于CopyOnWriteArrayList 集合,这里简单介绍下,CopyOnWriteArrayList 支持并发读/写,AIDL的发放是运行在服务端的Binder池中,因此当多个客户端同时连接的时候,存在多个线程同时访问的情况,因此这里用CopyOnWriteArrayList 来进行自动的线程同步。另外,细心的小伙伴可能注意到了,我们前面说过,AIDL中支持的List只有ArrayList,那么为什么CopyOnWriteArrayList (并非继承自ArrayList)可以呢?这是因为AIDL支持的是抽象的List,而List是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList ,但是在Binder线程池中,也就是Stub()中,它会形成一个新的ArrayList传递给客户端。
在我们重写的onBinde()方法中返回Binder对象,这个Binder对象指向IAdvertManager.Stub(),这个Stub类并非我们自己创建的,而是AIDL自动生成的。系统会为每个AIDL接口在build/source/aidl下生成一个文件夹,它的名称跟你命名的AIDL文件夹一样,里面的类也一样。如下图:
这个IAdvertManager.java就是系统为我们生成的相应java文件,简单说下这个类。它声明了两个方法getAdvertList和addAdvert,分明就是我们AIDL接口中的两个方法。同时他声明了2个id用来标识这两个方法,这两个id用于标识在transact过程中客户端请求的到底是哪个方法。接着就是我们的Stub,可以看到它是一个内部类,他本质上是一个Binder类。当服务端和客户端位于同一个进程时,方法调用不会走跨进程的transact过程,当两者处于不同晋城市,方法调用走transact过程,这个逻辑由Stub的内部代理类Proxy完成。
这个Stub对象之所以里面有我们AIDL的接口,正是因为官方替我们做好了,我们只要在这里具体实现就好了。这两个方法,我在这里做了简单的处理,一个是返回我们之前的集合,一个是向集合里面添加一条广告数据。这里只做演示用,项目中记得活学活用。
至此服务端的代码都实现了,然后在看看客户端的实现。
客户端:
package com.example.aykon.aidltest;
public class AdvertActivity extends AppCompatActivity {
public static final String TAG = "AdvertActivity";
private IAdvertManager mAdvertManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_advert);
Intent intent = new Intent(this, AdvertManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//这里将binder对象转换为aidl对象,从而能够调用aidl方法。
IAdvertManager iAdvertManager = IAdvertManager.Stub.asInterface(service);
try {
mAdvertManager = iAdvertManager;
List advertList = mAdvertManager.getAdvertList();
//得到广告列表之后就可以为所欲为了。。。。
Log.i(TAG,advertList.toString());
Advert advert = new Advert("java", 10, "后台");
mAdvertManager.addAdvert(advert);
Log.i(TAG,iAdvertManager.getAdvertList().toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onDestroy() {
//最后解注册
unbindService(mConnection);
super.onDestroy();
}
}
客户端也非常简单,首先我们连接到服务端Service,在连接成功时,也就是onServiceConnected方法里,通过asInterface(service)方法可以将服务端的Binder对象转换成客户端所需的AIDL的接口的对象。这种转换是区分进程的,如果是同一进程,那么此方法返回的就是Stub本身,否则返回的就是系统Stub.proxy对象。拿到接口对象之后,我们就能够调用相应方法进行自己的处理(为所欲为之为所欲为)。
上面就是一整个AIDL跨进程的方法,同时我们也分析了Binder的工作机制。但是,这里有两点需要额外说明一下:第一个,当客户端发起远程请求时,客户端会挂起,一直等到服务端处理完并返回数据,所以远程通信是很耗时的,所以不能在UI线程发起访问。第二个,由于服务端的Binder方法运行在Binder线程池中,所以应采取同步的方式去实现,因为它已经运行在一个线程中了。
Binder是会意外死亡的。如果服务端的进程由于某种原因异常终止,会导致远程调用失败,如果我们不知道Binder连接已经断裂, 那么客户端就会受到影响。不用担心,Android贴心的为我们提供了连个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。
同时在onServiceConnected连接成功时设置死亡代理binder.linkToDeath(mDeathRecipient, 0);第二个参数是一个标记,我们自己定义的。
AIDL注册和解注册:
因为跨进程传输客户端的同一个对象会在服务端生成不同的对象,所以如果我们解注册的时候还是用这个接口,就会报一个unregister listener的错。事实上,这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个。当客户端解注册的时候,我们只要便利服务端所有的listener,找出那个和解注册listener具有相同Bidner对象的服务端listener并把它删掉就可以了。RemoteCallbackList已经为我们做好了这些事情。RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。它是一个泛型,支持管理任意的AIDL接口。它的工作原理很简单,在它内部有一个Map专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。其中Callback封装了真正的远程listener。当客户端注册listener的时候,它会把这个listener的信息存入mCallbacks中。同时,当客户端进程终止后,它能够自动移除客户端所注册的listener。除此之外,RemoteCallbackList内部实现了线程同步,我们使用它来注册和解注册时,不需要做额外的线程同步工作。
RemoteCallbackList的用法也很简单,你只需在注册和解注册的地方调用mRemoteCallbackList.register(listener)和mRemoteCallbackList.unregister(listener)即可。还有要注意的一点是,RemoteCallbackList并不是一个List,遍历RemoteCallbackList时,必须要配对使用mRemoteCallbackList.beginBroadcast()和mRemoteCallbackList.finishBroadCast()。beginBroadcast返回RemoteCallbackList的size,finishBroadCast结束RemoteCallbackList的遍历,通过mRemoteCallbackList.getBroadcastItem(i)来获取每个注册的接口。
AIDL权限验证:
我们的远程服务自然是不想任意的人调用的,所以我们给服务加入权限验证功能。在AIDL进程权限验证,这里介绍两种常用的方法。
第一种:在onBind中验证,验证不通过就返回null。
第二种,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果,具体的实现方式和第一种一样。另外还可以采用Uid和Pid来进行验证。
本篇文章如果有什么纰漏,还请不吝指出。
文章参考自《Android开发艺术探索》。这真的是一本神器啊,谁用谁知道。