IPC之AIDL进阶用法
接上篇笔记 AIDL入门用法
主要包括下边四点:
- 跨进程的接口回调【观察者】
- 线程问题
- 断开重连问题
- 权限校验问题
一:跨进程的观察者
Step1. 首先定义一个通知客户端的回调接口
// OnNewBookInsertListener.aidl
package com.zhu.aidldemo;
import com.zhu.aidldemo.Book;
interface OnNewBookInsertListener {
void onNewBookInsert(in Book book);
}
因为在AIDL文件中不支持接口,所以定义成aidl文件。
Step2. 增加IBookManager.aidl方法
// IBookManager.aidl
package com.zhu.aidldemo;
import com.zhu.aidldemo.Book;
import com.zhu.aidldemo.OnNewBookInsertListener;
interface IBookManager {
void addBook(in Book book);
List<Book> getBookList();
void registerBookInsertListener(OnNewBookInsertListener listener);
void unregisterBookInsertListener(OnNewBookInsertListener listener);
}
新增注册与注销观察者的方法。然后Make Project.
Step3. 在服务端增加相关逻辑代码。
public class BookManagerService extends Service {
private final static String TAG = "server";
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private CopyOnWriteArrayList<OnNewBookInsertListener> mListenerList = new CopyOnWriteArrayList<>();
private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1, "人类简史"));
mBookList.add(new Book(2, "三体"));
new Thread(new BookAutoInsertWorker()).start(); //每个5s自动向bookList中插入一条数据
}
private Binder mBinder = new IBookManager.Stub() {
@Override
public void addBook(Book book) throws RemoteException {
Log.d(TAG, "receive a request from client, add book to list...");
mBookList.add(book);
}
@Override
public List<Book> getBookList() throws RemoteException {
Log.d(TAG, "receive a request from client, query book list...");
return mBookList;
}
@Override
public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
if (!mListenerList.contains(listener)) {
Log.d(TAG, "reguister listener to list..." + listener.toString());
mListenerList.add(listener);
} else {
Log.d(TAG, "listener already exists...");
}
Log.d(TAG, "listenerList size: " + mListenerList.size());
}
@Override
public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
if (mListenerList.contains(listener)) {
Log.d(TAG, "remove listener from list..." + listener.toString());
mListenerList.remove(listener);
} else {
Log.d(TAG, "listener not exists...");
}
Log.d(TAG, "listenerList size: " + mListenerList.size());
}
};
private class BookAutoInsertWorker implements Runnable {
@Override
public void run() {
while (!mIsServiceDestoryed.get()) {
try {
Thread.sleep(5000);
int bookId = mBookList.size() + 1;
Book newBook = new Book(bookId, "New book#" + bookId);
insertBookAndNotifyUser(newBook);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
}
}
private void insertBookAndNotifyUser(Book newBook) {
try {
mBookList.add(newBook);
Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
for (int i = 0; i < mListenerList.size(); i++) {
OnNewBookInsertListener onNewBookInsertListener = mListenerList.get(i);
Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
onNewBookInsertListener.onNewBookInsert(newBook);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
super.onDestroy();
mIsServiceDestoryed.set(true);
}
}
新增逻辑如下:
- 定义了一个观察者集合:
mListenerList
,用来记录注册的观察者 - 在mBinder中实现了新增的两个方法。注册观察者与注销观察者
- 开启了一个线程每5s向
mBookList
中新增一条数据并通知所有的观察者
Step4. 客户端注册并监听相关方法。
class MainActivity : AppCompatActivity() {
val TAG = "client"
val MSG_NEW_BOOK_INSERTED = 1
private var mHandler = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
when (msg?.what) {
MSG_NEW_BOOK_INSERTED -> {
Log.d(TAG, "receive a notify from server: " + msg.obj.toString())
}
else -> super.handleMessage(msg)
}
}
}
private var onNewBookInsertListener = object : OnNewBookInsertListener.Stub() {
override fun onNewBookInsert(book: Book?) {
mHandler.obtainMessage(MSG_NEW_BOOK_INSERTED, book).sendToTarget()
}
}
private var mBookManager: IBookManager? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
var bookManager = IBookManager.Stub.asInterface(service)
mBookManager = bookManager
try {
var bookList = bookManager.bookList
Log.d(TAG, "init book list on server: " + bookList.toString())
bookManager.addBook(Book(3, "未来简史"))
Log.d(TAG, "insert a book from client...")
Log.d(TAG, "book list on server after insert : " + bookManager.bookList.toString())
bookManager.registerBookInsertListener(onNewBookInsertListener)
} catch (e: Exception) {
println(e.message)
}
}
override fun onServiceDisconnected(name: ComponentName) {
Log.d(TAG, "onServiceDisconnected...")
mBookManager = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var intent = Intent(this, BookManagerService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
if (mBookManager != null && mBookManager?.asBinder()?.isBinderAlive!!) {
try {
Log.d(TAG, "unregister listener....")
mBookManager?.unregisterBookInsertListener(onNewBookInsertListener)
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null)
}
unbindService(serviceConnection)
}
}
新增逻辑如下:
- 获取服务端初始图书集合
- 调用服务端方法新增一条数据
- 注册成为一个观察者
- 当收到新增图书通知时打印相关信息
- 页面退出时注销观察者
结果:
客户端日志:
image.png
服务端日志:
image.png
存在的问题:
有日志可以观察到,在客户端页面退出时,调用服务端的注销观察者方法,并没有注销成功,而且客户端在退出时,依然能再次受到一条通知。猜测每次传入过去的onNewBookInsertListener
都是一个新的对象。
对象是不能跨进程传输的,对象的跨进程传输本质上是一中反序列化,因此Binder回把客户端传过来的对象重新转化并生成一个新的对象。因此无法像单进程一样操作list集合。
解决的方法:
使用RemoteCallbackList。对BookManagerService
中的mListenerList
做如下修改:
- 将
CopyOnWriteArrayList
改为RemoteCallbackList
private RemoteCallbackList<OnNewBookInsertListener> mListenerList = new RemoteCallbackList<>();
- 适配注册观察者与反注册观察者方法:
@Override
public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
boolean result = mListenerList.register(listener);
if (result) {
Log.d(TAG, "reguister listener to list success..." + listener.toString());
} else {
Log.d(TAG, "register listener failed...");
}
int count = mListenerList.beginBroadcast();
Log.d(TAG,"listener size : "+count);
mListenerList.finishBroadcast();
}
@Override
public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
boolean result = mListenerList.unregister(listener);
if (result) {
Log.d(TAG, "unregister listener success..." + listener.toString());
} else {
Log.d(TAG, "unregister listener failed...");
}
int count = mListenerList.beginBroadcast();
Log.d(TAG,"listener size : "+count);
mListenerList.finishBroadcast();
}
- 修改通知客户端的方法:
private void insertBookAndNotifyUser(Book newBook) {
try {
mBookList.add(newBook);
final int N = mListenerList.beginBroadcast();
Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
for (int i = 0; i < N; i++) {
OnNewBookInsertListener onNewBookInsertListener = mListenerList.getBroadcastItem(i);
if (onNewBookInsertListener != null) {
Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
onNewBookInsertListener.onNewBookInsert(newBook);
}
}
mListenerList.finishBroadcast();
} catch (RemoteException e) {
e.printStackTrace();
}
}
-
结果:
客户端日志:
image.png
服务端日志:
image.png
- RemoteCallbackList说明:
- 虽说多次跨进程传输客户端的对象会在服务端生成不同的对象,但新生的对象都有一个共同点,即他们底层的Binder对象是同一个。当客户端解注册时,我们只需遍历服务端所有的listener,找出和解注册listener具有相同Binder对象的服务端listener并删除即可。同时,客户端进程终止后,它能自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能,所以使用时不需做额外的线程同步工作。
- 我们无法像操作正常List一样操作它,它并不是List,操作它时
beginBroadcast()
和finishBroadcast()
必须配对使用。
二:线程问题
1. IPC时服务端的耗时操作导致的ANR
由于客户端的onServiceConnected
方法实在UI线程中执行的,所有当在该方法内部调用服务端比较耗时的方法时,容易导致客户端ANR,因此在该方法中最好开启线程调用服务端的方法。
打印结果如下:
当在服务端的
addBook
以及 getBookList
方法中睡眠6s后客户端多次调用回发生ANR:image.png image.png
解决方法:在onServiceConnected
方法开启新的线程并在线程内部调用服务端方法,如需操作UI,可通过Handler或runOnUiThread等方法实现。
另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,因此不需再开启新线程
2. IPC时客户端耗时操作导致的问题
同样,如果再服务端调用客户端的方法时,如果客户端的方法中进行了耗时的操作,比如在该demo中,当服务端新增一条数据时,会通过onNewBookInsertListener.onNewBookInsert(newBook);
回调客户端的方法。该种情况请确保客户端的回调不是在UI线程中接受即可。否则也会造成客户端的ANR。
该方法运行在客户端的Binder线程池中,因此不能在该方法中进行UI操作。
三:服务意外断开重连问题
当服务端进程意外停止,Binder会意外死亡,此时需要客户端重新进行连接服务。
1. 方法一:该Binder设置DeathRecipient监听
首先实现IBinder.DeathRecipient
接口,并重写binderDied
方法。
在接受到binder终止后重新绑定远程服务
之后将该回调设置给binder:
var bookManager = IBookManager.Stub.asInterface(service)
mBookManager = bookManager
mBookManager?.asBinder()?.linkToDeath(this@MainActivity, 0)
测试: 使用 adb shell kill pid
杀死远程服务进程【adb shell ps
】,然后客户端打印log如下:查看进程信息
四:IPC权限校验
为了防止任何人都可以连接远程服务,因此有时需要在客户端连接时进行权限校验。
方法1:在onBind中进行验证
首先在客户端的清单文件中声明调用权限:
<permission
android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_MANAGE_SERVICE"
android:protectionLevel="normal" />
<uses-permission android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE" />
然后在服务端的onBind方法中进行校验:
@Nullable
@Override
public IBinder onBind(Intent intent) {
int check = checkCallingOrSelfPermission("com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
Log.d(TAG, "permission denied....");
return null;
}
Log.d(TAG, "permission granted....");
return mBinder;
}
结果:
image.png
反例【更改onBind中的权限内容】:
image.png
方法2:在onTransact
方法中进行权限校验
重写服务端创建Binder中的onTransact方法:
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
Log.d(TAG, "permission denied....");
return false;
}
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
Log.d(TAG, "pacakgeName: " + packageName);
}
if (!packageName.equals("com.zhu.aidldemo")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
双重校验:先校验是否有权限,然后校验用户uid的包名。
测试结果:
正例:
image.png
反例【修改服务端校验的包名】:
image.png
完。