上次讲到IPC机制的几种通信方式,但是没有讲完,还剩下几种方式没有讲,今天继续。
在上次讲到Service的时候,说过AIDL,但是那种只是非常简单的方式,今天将会详细的讲述AIDL。
一、AIDL 中的数据类型
aidl支持的数据类型:
- 基本数据类型(int,long,char,boolean,double等)
- String和CharSequence
- List:只支持ArrayList,里面的每个元素都必须能够被AIDL支持。
- Map:只支持HashMap,里面的每个元素都必须能够被AIDL支持
- Parcelable:所有实现了Parcelable接口的对象。
- AIDL:所有的AIDL接口本身也可以在AIDL中使用。
注意:
自定义的Parcelable对象和AIDL对象必须要显示import进来,不管他们是否在同一个包中。
package cn.demo.zx_aidl_learn; import cn.demo.zx_aidl_learn.domain.Book; interface IService { List<Book>getBookList(); void addBook(in Book b); }
如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明为parcelable类型。
- 在AIDL中处理基本数据类型,其他类型的参数必须标上方向:in,out或者inout。in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。
- AIDL接口中只支持方法不支持声明静态常量。
- AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化。
案例:
服务端:
public class BookManagerService extends Service {
/**
* 使用CopyOnWriteArrayList的原因:
* 因为CopyOnWriteArrayList支持并发读/写,AIDL方法是在服务端的Binder的线程池中执行的,
* 因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中
* 处理线程同步,而我们这里直接使用CopyOnWriteArrayList来进行自动的线程同步。
*
* AIDL中能够使用的List只有ArrayList,但是这里为什么还使用它呢?
* 因为AIDL中所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,
* 但是在Binder中会按照List的规范去访问数据并最终返回一个新的ArrayList传递给客户端。还有就是类似的是
* ConcurrentHashMap.
*/
CopyOnWriteArrayList<Book>mBookList = new CopyOnWriteArrayList<>();
public BookManagerService() {
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1,"android"));
mBookList.add(new Book(2,"ios学习"));
}
private Binder mBinder = new IService.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book b) throws RemoteException {
mBookList.add(b);
}
};
}
客户端:
public class MainActivity extends AppCompatActivity {
private IService mService = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this,BookManagerService.class);
startService(intent);
bindService(intent,conn,BIND_AUTO_CREATE);
}
public void findBook(View v){
/**
* 注意:服务端的方法有可能是耗时的方法,那么下面的调用就有可能早能ANR异常,需要注意。
*/
try {
List<Book> bookList = mService.getBookList();
if(bookList!=null){
for(Book b : bookList){
System.out.println("书籍::"+b.toString());
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
清单文件AndroidMenifest:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".BookManagerService"
android:enabled="true"
android:exported="true"
android:process=":romote">
</service>
</application>
上述案例只是讲述客户端从服务端获取图书信息,如果是在服务端每添加一本书籍就向客户端发送消息,那么怎么实现呢?
这里就会用到观察者模式。
二、AIDL中的观察者模式
创建一个aidl接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能。
新创建的aidl文件:
import cn.demo.zx_aidl_learn.domain.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook);
}
import cn.demo.zx_aidl_learn.domain.Book;
import cn.demo.zx_aidl_learn.IOnNewBookArrivedListener;
interface IService {
List<Book>getBookList();
void addBook(in Book b);
void registerListener(in IOnNewBookArrivedListener listener);
void unregisterListener(in IOnNewBookArrivedListener listener);
}
在MainActivity中创建一个IOnNewBookArrivedListener对象,然后通过注册的方式将IOnNewBookArrivedListener对象传递给服务端:
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IService.Stub.asInterface(service);
try {
/**
* 注册对新书的监控
*/
mService.registerListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
private IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
System.out.println("新到的图书::"+newBook.toString());
mHandler.obtainMessage(0,newBook).sendToTarget();
}
};
服务端通过获取传入的IOnNewBookArrivedListener对象来调用客户端中的方法来通知客户端:
/**
* 发送新书已到的消息到客户端
* 怎样通知呢?
* 通过客户端在主持监听的时候传入的IOnNewBookArrivedListener对象来通知客户端
* @param newBook
*/
private void sendNewBookArrivedToClient(Book newBook){
mBookList.add(newBook);
for(int i=0;i<mListenerList.size();i++){
IOnNewBookArrivedListener listener = mListenerList.get(i);
try {
listener.onNewBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
详细功能实现请查看源码
三、取消注册功能
当我们实现了注册功能之后,怎样取消注册功能呢?
@Override
protected void onDestroy() {
super.onDestroy();
if(mService!=null || mService.asBinder().isBinderAlive()){
try {
mService.unregisterListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(conn);
}
我们通过上述方法无法取消注册。这是为什么呢?
这是因为在客户端传入的IOnNewBookArrivedListener对象是同一个对象,但是在服务端却又变成了另外的一个对象了。因为对象无法跨进程通信,服务端接收的IOnNewBookArrivedListener对象是通过序列化之后得到对象,所以无法取消注册,这种情况怎么处理呢?
这里我们就会用到RemoteCallbackList类,它是系统专门提供的用于删除跨进程listener的接口,它是一个泛型,支持管理任意的AIDL接口,因为它继承了IInterface接口,aidl接口都继承IInterface。
它的工作原理非常简单,就是在它的内部有一个专门用于保存所有的AIDL回调的Map集合:
ArrayMap<IBinder, Callback> mCallbacks
= new ArrayMap<IBinder, Callback>();
其中Callback中封装了真正的远程listener,当客户端注册listener的时候,它会把这个listener的信息存入mCallback中,其中key和value分别通过下面的方式获得:
private final class Callback implements IBinder.DeathRecipient
。。。。
public boolean register(E callback, Object cookie) {
synchronized (mCallbacks) {
if (mKilled) {
return false;
}
IBinder binder = callback.asBinder();
try {
Callback cb = new Callback(callback, cookie);
binder.linkToDeath(cb, 0);
mCallbacks.put(binder, cb);
return true;
} catch (RemoteException e) {
return false;
}
}
}
到这里就应该知道,虽然在客户端传入的同一对象在服务端会生成了不同的对象,但是它的底层的Binder对象却没有变化,利用这个特性就可以实现上面的功能了。当客户端取消注册的时候,我们会便利所有的listener接口,找到与客户端传入的listener具有相同Binder对象的服务端listener,然后把它删除即可。
服务端的代码:
RemoteCallbackList<IOnNewBookArrivedListener>mListenerList = new RemoteCallbackList<>();
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
// if(!mListenerList.contains(listener)){
// mListenerList.add(listener);
// }
mListenerList.register(listener);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
// if(mListenerList.contains(listener)){
// mListenerList.remove(listener);
// }
mListenerList.unregister(listener);
}
/**
* 发送新书已到的消息到客户端
* 怎样通知呢?
* 通过客户端在主持监听的时候传入的IOnNewBookArrivedListener对象来通知客户端
* @param newBook
*/
private void sendNewBookArrivedToClient(Book newBook){
mBookList.add(newBook);
// for(int i=0;i<mListenerList.size();i++){
// IOnNewBookArrivedListener listener = mListenerList.get(i);
// try {
// listener.onNewBookArrived(newBook);
// } catch (RemoteException e) {
// e.printStackTrace();
// }
// }
int N = mListenerList.beginBroadcast();
for(int i=0;i<N;i++){
IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
try {
listener.onNewBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
mListenerList.finishBroadcast();
}
四、设置死亡代理
关于死亡代理,以前讲过,这里在说明一下,当客户端在与服务端进行跨进程通信的时候,服务端因为异常而被销毁,但是服务端却不知道,那么怎样解决这个问题呢,这里就讲到了重新连接服务的两种方法,第一种就是死亡代理:
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mService.asBinder().unlinkToDeath(mDeathRecipient,0);
mService = null;
//重新连接
bindService(intent,conn,BIND_AUTO_CREATE);
}
};
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IService.Stub.asInterface(service);
try {
service.linkToDeath(mDeathRecipient,0);
} catch (RemoteException e) {
e.printStackTrace();
}
try {
/**
* 注册对新书的监控
*/
mService.registerListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
还有一种方法就是在onServiceDisconnected中重新连接绑定服务。
五、权限验证
我们不想让所有的客户端访问我们的服务端,我们希望具有某些权限的客户端才能访问,怎样实现?
第一种方法,在onBind中进行验证。验证不通过就返回null,造成无法连接服务端。这种验证很多,这里介绍其中一种permission权限验证,在服务端定义一个权限。
AndroidMenifest:
<permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>
服务端:
@Override
public IBinder onBind(Intent intent) {
int check = checkCallingOrSelfPermission("cn.demo.zx_aidl_learn.YANZHENG");
if(check== PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
如果想要访问这个服务端的话,那么客户端就需要设置权限:
<uses-permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>
另一种方法,这种方法就是在服务端的onTransact方法中进行权限验证,如果验证失败返回false。验证的方法也是很多,可以通过permission验证,也可以通过包名验证。permission验证的方法与上述onBind方法中差不多,唯一的区别就是返回false。包名验证就通过getCallinngUid和getCallingPid获取客户端的uid和pid从而获取包名,进行验证。
String[] packageNames = getPackageManager().getPacagesForUid(getCallingUid());
if(!packageNames[0].startsWith("cn.demo")){
return false;
}
注意
当客户端访问服务端的方法时候,客户端会等待跨进程通信,如果服务端中的方法是一个耗时的方法的话,那么客户端就需要在子线程中去访问,如果在UI线程中,容易造成ANR异常。服务端访问客户端的方法也是一样的。
源码下载: