Preliminary Exploration of IPC (3) - Using AIDL
This article will introduce AIDL communication.
Other articles in this series:
Android Cross-Process Communication-IPC Preliminary Exploration (1)
Android Cross-Process Communication-IPC Preliminary Exploration (2) - Using Messenger
1. New requirements for examples
Looking back at the example in IPC Preliminary Exploration (1) , the server provides two functions for the client:
addStudent()
: Add new data to the server's List on the client sidegetStudentList()
: Obtain the List data of the server from the client.
Now we have new requirements:
- Problem: When the client calls to
addStudent()
add a new piece of data, it needs to query again to get the latest data. This means that the client must actively initiate a query to the server to determine when the List has updated the data. - New requirements: Now we want to change this process, when the server adds new data, it will actively notify the client. This is the observer pattern.
- Solution: The common callback interface method does not work here - because this is a cross-process, an AIDL interface needs to be provided to achieve the purpose of cross-process.
- Problem: When the client calls to
Design Solutions
- Design a callback interface
IOnNewStudentAddedListener
for the server to notify the client. Since it is cross-process, this interface uses AIDL, which is automatically generated by the system. The specific implementation is provided by the client, and the server will call this interface after completing the addition of Student. - for
IStudentManager
adding registration and deregistration methods. These two methods are implemented by the server and provided to the client to call. - The server needs to provide specific implementations of the registration and deregistration methods. After the client obtains the remote binder, it calls these two methods through the binder (where appropriate).
- The client needs to provide
IOnNewStudentAddedListener
the specific implementation, and notify the server to add/delete this interface object through the registration/deregistration method. - In order to do the simulation test, we added a thread on the server side, which will add a new student every 5 seconds and notify the client.
- Design a callback interface
accomplish:
Callback interface
IOnNewStudentAddedListener
://IOnNewStudentAddedListener.aidl package com.dou.ipcsimple; import com.dou.ipcsimple.Student; interface IOnNewStudentAddedListener { void onNewStudentAdded(in Student student); }
Added two new methods for
IStudentManager
registering and unregistering observers.package com.dou.ipcsimple; import com.dou.ipcsimple.Student; import com.dou.ipcsimple.OnNewStudentAddedListener; interface IStudentManager { void addStudent(in Student student); List<Student> getStudentList(); //新增的接口,用于注册和注销观察者 void registerListener(OnNewStudentAddedListener listener); void unregisterListener(OnNewStudentAddedListener listener); }
Server
StudentManagerService
modification:In Binder, the specific implementation of the two methods of registration/deregistration is provided:
private CopyOnWriteArrayList<IOnStudentAddedListener> mListeners = new CopyOnWriteArrayList<>(); private Binder binder = new IStudentManager.Stub() { @Override public void addStudent(Student student) throws RemoteException { students.add(student); } @Override public List<Student> getStudentList() throws RemoteException { return students; } //注册 @Override public void registerListener(IOnStudentAddedListener listener) throws RemoteException { if (mListeners.contains(listener)) { mListeners.add(listener); } else { Log.d(TAG,"already exists."); } Log.d(TAG,"registerListener size:"+ mListeners.size()); } //注销 @Override public void unregisterListener(IOnStudentAddedListener listener) throws RemoteException { if (mListeners.contains(listener)) { mListeners.remove(listener); Log.d(TAG,"unregister listener succeed."); } else { Log.w(TAG,"Warning:listener not found, cannot unregister."); } Log.d(TAG,"unregisterListener size:"+ mListeners.size()); } };
In addition, we do a mock test to add a new Student every 5 seconds on the server:
private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false); private class AddStudentWork implements Runnable { @Override public void run() { while (!mIsServiceDestroyed.get()){ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } int studentID = students.size() + 1; Student student = new Student(studentID, "newStu_" + studentID); try { noticeStudentAdded(student); } catch (RemoteException e) { e.printStackTrace(); } } } } //挨个儿通知各位观察者 private void noticeStudentAdded(Student student) throws RemoteException { students.add(student); Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size()); for (int i = 0; i < mListeners.size(); i++) { IOnStudentAddedListener listener = mListeners.get(i); listener.onStudentAdded(student); } } @Override public void onDestroy() { mIsServiceDestroyed.set(true); super.onDestroy(); }
Client modification:
IOnNewStudentAddedListener
The specific implementation, and the creation of this interface object://这里提供了IOnNewStudentAddedListener的匿名实现,这个mOnStudentAdded将作为接口对象注册/注销到远程服务端。 private IOnStudentAddedListener mOnStudentAdded = new IOnStudentAddedListener.Stub() { @Override public void onStudentAdded(Student student) throws RemoteException { //这里我们给handler发送消息,后续操作将由handler处理。 handler.obtainMessage(MESSAGE_NEW_STUDENT_ADDED, student).sendToTarget(); } }; //在handler中处理消息,可以更新UI线程。 private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_NEW_STUDENT_ADDED: Log.d(TAG, "receive new student:" + msg.obj); break; default: super.handleMessage(msg); } } };
Register/deregister where appropriate
mOnStudentAdded
:private IStudentManager remoteStudentManager; private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bindService = true; IStudentManager studentManager = IStudentManager.Stub.asInterface(service); try { ... ... //注册 studentManager.registerListener(mOnStudentAdded); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { remoteStudentManager = null; Log.d(TAG, "binder died."); } }; @Override protected void onDestroy() { if (remoteStudentManager != null && remoteStudentManager.asBinder().isBinderAlive()) { Log.i(TAG, "unregister listener:" + mOnStudentAdded); try { //注销 remoteStudentManager.unregisterListener(mOnStudentAdded); } catch (RemoteException e) { e.printStackTrace(); } } if (bindService){ unbindService(mServiceConnection); } super.onDestroy(); }
- The complete code can be obtained from Github IPCSimple
2. Analysis of running results after adding callback interface
Run the test:
Client: can observe normal operation results:
04-23 09:03:14.146 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry]]
04-23 09:03:14.149 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry], [stuId:1003, name:Jack]]
04-23 09:03:19.141 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:4, name:newStu_4]
04-23 09:03:24.143 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:5, name:newStu_5]
04-23 09:03:29.146 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:6, name:newStu_6]
04-23 09:03:34.149 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:7, name:newStu_7]
Server: can accept registration normally. But when we click the back button to exit the Activity,
onDestroy
the logout method called has a bug:04-23 09:05:44.276 22645-22662/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1
04-23 09:05:45.646 22645-22658/com.dou.ipcsimple:remote W/StudentService: Warning:listener not found, cannot unregister.
04-23 09:05:45.647 22645-22658/com.dou.ipcsimple:remote D/StudentService: unregisterListener size:1
Unregister failed: deserialization - every generated object is brand new.
- Reason exploration: Obviously
mListeners.contains(listener)
returnedfalse
, the deregisteredlistener
and registeredlistener
objects are not the same.
- Here objects are passed through binder, and the essence of this pass is serialization and deserialization. Therefore, the listener to the client is passed to the server through registration and deregistration respectively, and the two listeners obtained by deserialization are two different objects - because each serialization creates a separate object.
Solution: use
RemoteCallbackList
.RemoteCallbackList
Used internallyArrayMap<IBinder,Callback>
to hold all AIDL callbacks. For the three listener objects in this example (one for the client and two for the server), their low-level Binders are all the same. UsingRemoteCallbackList
this feature, you can fix the bug that was logged out above.Use
RemoteCallbackList
overridesCopyOnWriteArrayList<IOnStudentAddedListener>
, then modify the implementation of the register/unregister methods:private RemoteCallbackList<IOnStudentAddedListener> mListeners = new RemoteCallbackList<>(); // 注册 mListeners.register(listener); // 注销 mListeners.unregister(listener);
And, modify the notification method (please pay attention to the traversal method):
``` private void noticeStudentAdded(Student student) throws RemoteException { students.add(student); /** 旧版本 Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size()); for (int i = 0; i < mListeners.size(); i++) { IOnStudentAddedListener listener = mListeners.get(i); listener.onStudentAdded(student); } //*/ //RemoteCallbackList not-is a List... final int COUNT = mListeners.beginBroadcast(); Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + COUNT); for (int i = 0; i < COUNT; i++) { IOnStudentAddedListener listener = mListeners.getBroadcastItem(i); if (null != listener) { listener.onStudentAdded(student); } } mListeners.finishBroadcast(); } ```
Curiously took a look
getBroadcastItem()
, internally arrays are used to support this index traversal method:``` public E getBroadcastItem(int index) { return ((Callback)mActiveBroadcast[index]).mCallback; } ```
The running result after the repair: When the Activity is exited, the logout is successful:
04-23 10:17:46.187 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1
04-23 10:17:47.369 14105-14105/com.dou.ipcsimple:remote W/StudentService: Service onDestroy
04-23 10:17:51.189 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:0
All modifications can be found in the complete example code on Github. Annotated is the old version of the code, that is, the code that caused the logout to fail.
- Reason exploration: Obviously
3. Review the Binder workflow
Let's take a look at the working diagram of Binder:
- After the client initiates a remote request, it will be suspended. At this time, if the execution of the server method takes too much time, the client thread will be blocked. If the Client thread is the UI thread, it may cause ANR.
We can
getStudentList
add a delay to the Service method to simulate a method that takes too long:public List<Student> getStudentList() throws RemoteException { SystemClock.sleep(5000); return students; }
Then run and find ANR:
onServiceConnected
AndonServiceDisconnected
also runs on the UI thread, so it might also cause an ANR. The above results are proof.
To resolve blocking/ANR:
- When the client calls the method of the remote service, the called method runs in the Binder thread pool of the server. Similarly, when the remote server calls the client's method, the called method runs in the client's Binder thread pool.
When a method on the server side is time-consuming, the client should not call it in the UI thread. Of course, do not call this long time-consuming method in the and
onServiceConnected
method.onServiceDisconnected
- Calls to long-running methods should be placed on a non-UI thread. For example, another Thread to run.
Similarly, we can't call the client's time-consuming method in the main thread of the server. For example, the server's
noticeStudentAdded
will call the client's callback interface methodonStudentAdded
. If itonStudentAdded
takes a lot of time, please ensure that the servernoticeStudentAdded
runs in a non-UI thread, otherwise the server will not be able to respond.- The client's
onStudentAdded
method runs in the client's Binder thread pool, so you cannot access UI-related content in the method. If you want to access the UI, use Handler to switch to the UI thread.
Reconnection after Binder's unexpected death:
Set death proxy for Binder:
linkToDeath
andunlinkToDeath
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { if (remoteStudentManager == null) { return; } remoteStudentManager.asBinder().unlinkToDeath(mDeathRecipient, 0); remoteStudentManager = null; //重新绑定远程服务 Intent intent = new Intent(MainActivity.this, StudentManagerService.class); bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); } };
Then after the client binds the remote service successfully, set the death proxy to the binder:
service.linkToDeath(mDeathRecipient,0);
onServiceDisconnected
Reconnect the remote service in .Either of the two methods can be used at will. The difference between them is:
- Binder's death proxy is called back in the client Binder thread pool, so it cannot access the UI.
onServiceDisconnected
Called back on the client's UI thread.
4. Add permission verification function
There are two methods for authorization verification in AIDL:
1. Verify in the onBind
method on the server side . If the verification fails, return null, and the client that fails the verification cannot bind the service. Here we use the permission authentication method.
First declare the required permissions in the manifest:
<permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE" android:protectionLevel="normal"/>
onBind
Add permission validation for the method:public IBinder onBind(Intent intent) { Log.d("StudentManagerService", "Service onBind"); int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE"); if (check == PackageManager.PERMISSION_DENIED) { Log.e("StudentManagerService", "Service onBind :" + null); return null; } return binder; }
At this point, the client cannot connect to the remote service. Looking at the Logcat, you can find the following information:
com.dou.ipcsimple:remote E/StudentManagerService: Service onBind :null
Register the permission in manifest:
``` <uses-permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE" /> ``` 再次运行,客户端就可以连接上远程服务了。
2. onTransact
Perform permission verification in the method of the server , and return false if the verification fails.
In addition to permission verification, you can also use Uid and Pid to verify, getCallingUid
and getCallingPid
you can get the Uid and Pid of the application to which the client belongs.
```
//StudentManagerService
private Binder binder = new IStudentManager.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String pkgName = null;
String[] pkgs = getPackageManager().getPackagesForUid(getCallingUid());
if (null != pkgs && pkgs.length > 0) {
pkgName = pkgs[0];
}
if (!pkgName.startsWith("com.dou")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
...
...
...
};
```
Obviously, this service can only successfully connect to the remote server if it declares usage rights and the client whose "com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE"
package name starts with .com.dou
4. Epilogue
The Binder communication mechanism will come to an end temporarily. The next article will introduce another IPC mechanism, Content Privider.
In this article, the complete code can be obtained from Github: IPCSimple .