转载请以链接形式标明出处:
本文出自:103style的博客
《Android开发艺术探索》 学习记录
base on AndroidStudio 3.5.1
目录
- 前言
- AIDL接口创建
- AIDL支持的数据格式
- 服务端实现
- 创建 BookManagerService.java
- 处理并发情况
- 客户端实现
- 创建 BookManagerActivity.java
- 运行程序查看日志
- AIDL添加和解除回调
- 添加服务端新增数据的回调
- 解除回调失败?RemoteCallbackList ?
- AIDL添加权限验证
- 小结
前言
前面我们介绍了 进程间通信基础介绍 、 通过AIDL介绍Binder的工作机制 ,以及 通过 Bundle、文件共享、Messenger实现进程间通信 , 不了解的可以先看下。
通过之前对 Messenger 的介绍,我们知道 Messenger 是以串行的方式处理消息的,所以当有 大量消息并发请求 时,Messenger 可能就不太合适了。
同时 Messenger 主要是用来传递消息,很多时候我们可能需要 跨进程调用其他进程的方法 ,这个是 Messenger 做不到的。
这时候就轮到 AIDL 展示自己的实力了。
Messenger 也是基于 AIDL 的,是系统对 AIDL 的封装,方便上层调用。
我们在 通过AIDL介绍Binder的工作机制 中介绍了 Binder 的概念,大家对 Binder 应该有了一定的了解。
这里我们先介绍下AIDL 来进行进程间通信的流程,包括 AIDL接口创建、服务端、客户端。
AIDL接口创建
tips: 为了方便开发,建议把 AIDL 相关的类和文件放到统一的目录,这样当客户端和服务端是不同应用时,可以把整个包复制过去。
注意: 客户端和服务端的 AIDL 包结构必须保持一致,否则会运行报错。
创建 IBookManager.aidl
:
//IBookManager.aidl:
package aidl;
import aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
然后我们先来介绍下AIDL支持的数据格式。
AIDL支持的数据格式
AIDL 支持的大部分数据格式,不过也不是所有的数据类型都能使用的,可以用如下类型:
- 基本数据类型(int、long、char、boolean、double 等)
- String 和 CharSequence
- List : 只能是 ArrayList,而且其中的元素的格式都要能被 AIDL 支持。
- Map : 只能是 HashMap,而且其中的元素的格式都要能被 AIDL 支持。
- AIDL:所有 AIDL 接口也可以在 AIDL 中使用。需要import导入。
- Parcelable:所有实现该接口的对象。需要import导入,该对象还需创建 类名.aidl 文件,然后添加如下内容,以上述示例中的
Book
为例://Book.aidl package aidl; parcelable Book;
除了基本类型之外,其他的类型在作为参数的时候必须标上方向:in、out、inout。
in:表示输入型参数
out:表示输出型参数
inout:表示输入输出型参数
而且不能一概使用 inout,因为底层性能是有开销的,所以要按需使用。
例如上述示例中的 void addBook(in Book book);
服务端实现
首先我们在服务端创建一个 Service 来处理客户端的连接请求,然后在 Service 中实现在 AIDL 中的声明暴露给客户端的接口。
创建 BookManagerService.java
:
//BookManagerService.java
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
private CopyOnWriteArrayList<Book> bookList = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return bookList;
}
@Override
public void addBook(Book book) throws RemoteException {
bookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
bookList.add(new Book(1, "Android艺术开发探索"));
bookList.add(new Book(2, "Java并发编程指南"));
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
上述示例中主要是创建 实现 AIDL 中声明的方法的 BInder 类,并在 Service 的 onBind 中返回。
然后前面提到是在服务端的 Binder 线程池中执行的,所以会存在多个线程同时访问的情况。所以我们要在 AIDL 方法中处理线程同步,因为 CopyOnWriteArrayList 是支持并发读写的,这里我们直接用 CopyOnWriteArrayList 来进行线程自动同步。
但是在上面介绍 AIDL支持的数据格式 时,我们知道 List 只支持 ArrayList,而 CopyOnWriteArrayList 也不是 ArrayList 的子类,那为什么能供支持工作呢?
这是因为 AIDL 中所支持的是抽象的 List,而 List 是一个接口,因此虽然服务端返回的是 CopyOnWriteArrayList,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 传给客户端。
然后在 AndroidManifest.xml
中声明所在的进程 :remote
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ipc">
<application>
...
<service
android:name="test.BookManagerService"
android:process=":remote" />
</application>
</manifest>
客户端实现
客户端首先要绑定服务端的 Service, 绑定成功后用服务端返回的 Binder 对象转成 AIDL 接口所属的类型,然后就可以调用 AIDL 的方法了。
创建 BookManagerActivity.java
:
//BookManagerActivity.java
public class BookManagerActivity extends AppCompatActivity {
private static final String TAG = "BookManagerActivity";
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager iBookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = iBookManager.getBookList();
Log.e(TAG, "query book list, type = " + list.getClass().getCanonicalName());
Log.e(TAG, list.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
}
运行程序,看到日志信息如下:
BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android艺术开发探索'}, Book{bookId=2, bookName='Java并发编程指南'}]
AIDL添加和解除回调
我们在上面的代码中实现以下功能,当服务端有新的书添加时,通知客户端。
来,直接开撸。
因为 AIDL 中无法使用普通接口,所以我们得创建一个 AIDL接口 IBookAddListener.aidl。
//IBookAddListener.aidl
package aidl;
import aidl.Book;
interface IBookAddListener{
void onBookArrived(in Book newBook);
}
然后在之前的 IBookManager.aidl 中添加接口的添加和删除方法。
//IBookManager.aidl
package aidl;
import aidl.Book;
import aidl.IBookAddListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IBookAddListener listener);
void unregisterListener(IBookAddListener listener);
}
然后在修改上面的服务端代码 BookManagerService 中的 mBinder 实现 新增的两个方法,并且创建一个 Worker 定时往服务端的 bookList 中添加数据。
//BookManagerService.java
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
//服务是否已经销毁
private AtomicBoolean destroyed = new AtomicBoolean(false);
private CopyOnWriteArrayList<Book> bookList = new CopyOnWriteArrayList<>();
private CopyOnWriteArrayList<IBookAddListener> listeners = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
...
@Override
public void registerListener(IBookAddListener listener) throws RemoteException {
if (!listeners.contains(listener)) {
listeners.add(listener);
} else {
Log.e(TAG, "lister is already exist");
}
Log.e(TAG, "registerListener: listeners.size = " + listeners.size());
}
@Override
public void unregisterListener(IBookAddListener listener) throws RemoteException {
if (listeners.contains(listener)) {
listeners.remove(listener);
} else {
Log.e(TAG, "lister not found, can not unregister");
}
Log.e(TAG, "unregisterListener: listeners.size = " + listeners.size());
}
};
@Override
public void onCreate() {
super.onCreate();
bookList.add(new Book(1, "Android艺术开发探索"));
bookList.add(new Book(2, "Java并发编程指南"));
new Thread(new Worker()).start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
destroyed.set(true);
super.onDestroy();
}
private void onBookAdd(Book book) throws RemoteException {
bookList.add(book);
Log.e(TAG, "onBookAdd: notify listeners.size = " + listeners.size());
for (IBookAddListener listener : listeners) {
listener.onBookArrived(book);
}
}
private class Worker implements Runnable {
@Override
public void run() {
while (!destroyed.get()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int bookId = bookList.size() + 1;
Book book = new Book(bookId, "new book#" + bookId);
try {
onBookAdd(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
}
然后修改客户端的 BookManagerActivity 添加服务端的监听。
//BookManagerActivity.java
public class BookManagerActivity extends AppCompatActivity {
private static final String TAG = "BookManagerActivity";
private static final int BOOK_ADD_MSG = 0x001;
private IBookManager remoteBookManager;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case BOOK_ADD_MSG:
Log.e(TAG, "a new book add :" + msg.obj);
break;
default:
super.handleMessage(msg);
}
}
};
//监听服务端的回调
private IBookAddListener bookAddListener = new IBookAddListener.Stub() {
@Override
public void onBookArrived(Book newBook) throws RemoteException {
//运行在客户端的Binder线程池,不能执行访问UI
handler.obtainMessage(BOOK_ADD_MSG, newBook).sendToTarget();
}
};
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager iBookManager = IBookManager.Stub.asInterface(service);
try {
remoteBookManager = iBookManager;
List<Book> list = iBookManager.getBookList();
Log.e(TAG, "query book list, type = " + list.getClass().getCanonicalName());
Log.e(TAG, list.toString());
Book book = new Book(3, "Android软件安全指南");
remoteBookManager.addBook(book);
remoteBookManager.registerListener(bookAddListener);//添加回调监听
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
remoteBookManager = null;
Log.e(TAG, "binder died ");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);//绑定服务
}
@Override
protected void onDestroy() {
unregisterListener();//解除注册
unbindService(connection);//解绑服务
super.onDestroy();
}
private void unregisterListener() {
if (remoteBookManager != null && remoteBookManager.asBinder().isBinderAlive()) {
try {
remoteBookManager.unregisterListener(bookAddListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
然后运行程序,打印如下日志。
//客户端进程
BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android艺术开发探索'}, Book{bookId=2, bookName='Java并发编程指南'}]
BookManagerActivity: a new book add :Book{bookId=4, bookName='new book#4'}
BookManagerActivity: a new book add :Book{bookId=5, bookName='new book#5'}
BookManagerActivity: a new book add :Book{bookId=6, bookName='new book#6'}
//服务端 :remote进程
BookManagerService: registerListener: listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
→ BookManagerService: lister not found, can not unregister
BookManagerService: unregisterListener: listeners.size = 1
BookManagerService: onBookAdd: notify listeners.size = 1
我们从日志中可以看到,确实有监听到每隔5s就新增一条数据。
但是我们发现一个问题: 解除注册的时候提示 lister not found, can not unregister。说明解除注册失败了,这是为什么呢?
这是因为 Binder 的机制的问题,Binder会把客户端传递过来的对象重新转化并生成一个新的对象。
因为对象是不能直接跨进程传输的,对象传输的本质都是反序列化的过程,这就是为什么 AIDL 中的对象都得实现 Parcelabe 接口的原因。
那我们怎么才能解注册呢? 就得使用系统提供的 RemoteCallbackList,专门提供用于删除跨进程的 回调接口,从它的泛型我们可以看到,它是支持管理任意的 AIDL 接口。
public class RemoteCallbackList<E extends IInterface> {}
接下来我们来修改我们之前的 BookManagerService:
//BookManagerService.java 只贴了要修改的地方
public class BookManagerService extends Service {
...
private RemoteCallbackList<IBookAddListener> listeners = new RemoteCallbackList<>();
private Binder mBinder = new IBookManager.Stub() {
...
@Override
public void registerListener(IBookAddListener listener) throws RemoteException {
listeners.register(listener);
final int N = listeners.beginBroadcast();
Log.e(TAG, "registerListener: size = " + N);
listeners.finishBroadcast();
}
@Override
public void unregisterListener(IBookAddListener listener) throws RemoteException {
listeners.unregister(listener);
final int N = listeners.beginBroadcast();
Log.e(TAG, "unregisterListener: size = " + N);
listeners.finishBroadcast();
}
};
private void onBookAdd(Book book) throws RemoteException {
bookList.add(book);
final int N = listeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IBookAddListener listener = listeners.getBroadcastItem(i);
if (listener != null) {
listener.onBookArrived(book);
}
}
listeners.finishBroadcast();
}
}
运行程序,从日志我们可以看到解注册成功了。
//客户端进程
BookManagerActivity: query book list, type = java.util.ArrayList
BookManagerActivity: [Book{bookId=1, bookName='Android艺术开发探索'}, Book{bookId=2, bookName='Java并发编程指南'}]
BookManagerActivity: a new book add :Book{bookId=4, bookName='new book#4'}
//服务端:remote进程
E/BookManagerService: registerListener: size = 1
E/BookManagerService: unregisterListener: size = 0
使用 RemoteCallbackList
,有一点需要注意,虽然名字中有 List,但是我们不能像 List 一样去操作它。
遍历其数据 或者 获取其大小,我们必须配对使用 beginBroadcast 和 finishBroadcast,参考上面代码中回调的注册和解注册的方法。
至此,AIDL 的基本使用方法已经介绍完了,但是还有几点需要再强调以下:
- 客户端调用远程服务的方法是运行在服务端的 Binder 线程池中的,客户端会被挂起直到方法执行完成,如果方法比较耗时的话,客户端如果在 UI线程 中直接调用则会出现 ANR。所以在知道方法耗时时,我们不能直接在UI线程中调用,需要通过子线程去处理,如示例中客户端 BookManagerActivity 中的 ServiceConnection 的两个方法
onServiceConnected
和onServiceDisconnected
都是运行在UI线程的。 - 另外就是客服端中的回调,即示例 BookManagerActivity 中的
bookAddListener
,是运行在客户端的 Binder 线程池的,所以不能直接访问UI内容的,如需访问UI,则需要通过 Handler 等切换线程。
另外,为了程序的健壮性,我们还的防止 Binder 意外死亡,这往往是由于服务端进程意外停止了,这是我们需要重连服务。有两种方法:
- 给Binder设置 DeathRecipient 监听,当 Binder死亡时,我们会收到 binderDied 回调,这个我们已经在 Binder的工作机制 这里介绍过了 。
- 在 onServiceDisconnected 中去重连远程服务。
AIDL添加权限验证
默认情况下,我们的远程服务任何人都可以连接,但这是我们不想要的,所以我们要在AIDL中添加权限验证。这里介绍两种方法:
1.在 obBinder 中验证
验证不通过时直接返回 null,这样验证失败的客户端直接无法绑定服务。至于验证方式有多种,比如 permission验证,使用这种验证,我们需要在 AndroidManifest.xml 中声明所需要的权限,示例如下:
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.okhttptesst">
//声明权限
<permission android:name="com.aidl.test.permission.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
...
</manifest>
//BookManagerService.java
public class BookManagerService extends Service {
...
@Nullable
@Override
public IBinder onBind(Intent intent) {
int check = checkCallingOrSelfPermission("com.aidl.test.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return null;
}
return mBinder;
}
...
}
然后再我们要绑定服务的应用内声明权限即可。
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.okhttptesst">
//注册权限
<uses-permission android:name="com.aidl.test.permission.ACCESS_BOOK_SERVICE"/>
...
</manifest>
2.在服务端的 onTransact 中验证
在 onTransact 中验证失败即返回 false,这样服务端就终止执行AIDL中的方法从而达到保护服务端的效果。具体验证方法也有很多。
可以采用第一种验证中的 permission 验证,具体实现也一样。
也可以 Uid 和 Pid 来做验证,通过 getCallingUid 和 getCallingPid 可以获取客户端所属应用的 Uid 和 Pid,通过这两个参数我们做 包名验证 等。
示例如下,我们重写 BookManagerService 中 mBinder 的 onTransact 方法,添加权限和包名验证:
//BookManagerService.java
public class BookManagerService extends Service {
...
private Binder mBinder = new IBookManager.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("com.aidl.test.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
Log.e(TAG, "PERMISSION_DENIED");
return false;
}
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
if (!packageName.startsWith("com.aidl")) {
Log.e(TAG, "packageName is illeagl = " + packageName);
return false;
}
return super.onTransact(code, data, reply, flags);
}
...
};
...
}
启动程序,查看日志:
BookManagerService: PERMISSION_DENIED
申明权限之后,再运行:
BookManagerService: packageName is illeagl = com.example.aidltest
BookManagerService: packageName is illeagl = com.example.aidltest
BookManagerService: packageName is illeagl = com.example.aidltest
除了上面两个验证方法之外,我们还可以通过 给 Service 指定 android:permission
属性等。
小结
我们再来回顾下本文的内容:
- 介绍了 AIDL 的基本使用方法,以及AIDL支持的数据格式。
- 通过 RemoteCallbackList 给 AIDL 添加和删除回调,遍历数据或者获取大小 必须配对使用 beginBroadcast 和 finishBroadcast。
- 以及介绍了 通过 permission验证 和 包名验证 给AIDL做权限验证。
下一节我们介绍通过 ContentProvider 来进行IPC.
如果觉得本文不错的话,请帮忙点个赞呗。
以上
扫描下面的二维码,关注我的公众号 Android1024, 点关注,不迷路。