I. IPC Communication Overview
IPC stands for inter-process Communication.
Android is based on Linux, which, for security reasons, prevents different processes from directly manipulating each other’s data, a practice called process isolation.
Process isolation is a set of different hardware and software techniques designed to protect processes in an operating system from interfering with each other. Process isolation implementation, using virtual address space. The virtual address of process A is different from that of process B. This prevents process A from writing data to process B
Cross-process communication requires that the method call and its data be decomposed to a level recognized by the operating system, transferred from the local process and address space to the remote process and address space, and then reassembled and executed in the remote process. The return value is then transmitted back in the opposite direction.
Common process communication mechanisms in Android include files, AIDL (Binder), Binder, Messenger (Binder), ContentProvider (Binder), and Socket.
Binder and AIDL are the focus here
Two, Binder analysis
2.1 Binder understand
Binder is the IPC that Android gives us.
At the JAVA level, a Binder is simply a class that implements the IBinder interface
Viewed from the Linux kernel, a Binder is a virtual physical device that relies on something called a Binder Driver to start up and transfer data between processes.
Binder is the medium through which clients and servers communicate, as understood from the Android application layer.
Concept of knowledge
Let’s start with user space and other related concepts.
-
User space/kernel space
The Linux Kernel is the core of the operating system. It is independent of common applications and can access the protected memory space as well as all permissions to access the underlying hardware devices.
For things with high security level such as Kernel, it is not allowed to be called or accessed by other applications. Therefore, certain protection mechanism should be provided to the Kernel to inform application programs which resources can be accessed and which resources are denied access. The Kernel is separated from the upper application program, which is divided into Kernel space and user space
-
System call/kernel/user mode
User space sometimes requires access to kernel resources, such as files for application access.
The only way user space can access kernel space is through system calls. Through this interface, all resource access is under the control of the kernel, so as to avoid unauthorized access to system resources by user programs and ensure the security and stability of the system.
When a task (process) executing a system call is trapped in kernel code, the process is said to be in kernel state. When a process executes the user’s own code, it is called user mode.
-
Kernel module/driver
Through system calls, user space can access kernel space. If one user space wants to communicate with another, you can have the operating system kernel add support, such as sockets, pipes, and so on.
Binder is not part of the Linux kernel and addresses this problem through Linux’s dynamically loadable kernel module (LKM) mechanism.
A module is a self-contained program that can be compiled separately but cannot be run independently. It is linked to the kernel at runtime and runs in kernel space as part of the kernel. The Android system can run in the kernel space by adding a kernel module, which serves as a bridge for communication between user processes.
In Android, the kernel module that runs in the kernel space and is responsible for communication between user processes through Binder is called the Binder driver.
A driver is the interface that operates the hardware. To support the Binder communication process, Binder uses a “hardware”, so this module is called a driver.
2.2 Binder communication model
For cross-process communication, the two parties are generally called Server process (Server) and Client process (Client).
Daily telephone communication process, complete the communication process needs two roles: address book and base station. With a Binder, the Binder driver functions like a base station, while the address book function is implemented by a ServiceManager.
The communication steps are as follows:
-
SM
Set up (set up directory)First, a process requests the driver as SM; After the driver agrees, the SM process is responsible for managing the Service
-
The various
Server
toSM
Register (Complete Directory)After each Server process starts, it reports the name and address to SM, so that SM builds up a list of contacts
-
Address the query
If the Client wants to communicate with the Server, it first asks SM. SM returns information such as the communication address of the Server. After receiving the IP address, the Client communicates with the IP address
2.3 principle of Binder
With Binder communication model, the communication process has four roles: Client, Server, ServiceManager, and driver. Take a look at how the Client and Server actually communicate.
The kernel has access to all of A and B’s data, so the easiest way to do this is through the kernel.
If process A wants to send data to process B, it copies the data from process A to the kernel space and then the corresponding data from the kernel space to process B. User space manipulates kernel space through system calls, two of which are provided here: copy_from_user and copy_to_user.
There are two problems with this type of IPC communication:
-
Poor performance
One data transfer goes through: memory cache –> kernel cache –> memory cache, and two data copies are required
-
Waste of time and space resources
Receive data buffer provided by the data receiving process, but the receiving process may not know how much space to store data to be passed, therefore can only be set up as large memory space or to call API to receive the size of the header to get the message body, the two is not a waste of space is a waste of time.
The memory mapping
All data is stored in physical memory, and processes can access memory only through virtual addresses. To access memory successfully, there must be a mapping between virtual addresses and physical memory.
With this mapping, the process can read and write the memory as a pointer to a file (or other mapped object) without having to call system call functions such as read/write. This feature of memory mapping can reduce the number of data copies and achieve efficient interaction between user space and kernel space.
The most efficient way for process A to access data in process B through virtual addresses in (user space) is to change the PTE of some virtual addresses in process A/B so that they map to the same physical area. There is no copy and therefore only one copy of the data in physical space.
Shared memory is efficient, but because there is only one copy of physical memory, synchronization issues have to be considered. The memory copy method can ensure that different processes have their own data area, which does not take into account the data synchronization between processes.
Binder in Android is the choice based on speed and security.
Binder IPC is implemented based on memory mapping (Mmap), but Mmap () is typically used on file systems with physical media.
For example, the user area in the process cannot directly interact with the physical device. If you want to read the data from the disk into the user area of the process, you need to copy it twice (disk -> kernel space -> user space). This is usually where mmap() comes in handy. The mapping between the physical media and user space reduces the number of data copies and improves file read efficiency by replacing I/O reads with memory reads.
Binder does not have physical media, so Binder drivers use mmap() not to create a mapping between physical media and user space, but to create a cache space for data reception in kernel space.
Binder
Communication principle
One-shotBinder
The communication process is roughly as follows:
- First of all,
Binder
The driver creates a data receive cache in kernel space - Then, a kernel cache is created in the kernel space, and the mapping relationship between the kernel cache and the kernel data receiving cache, as well as the mapping relationship between the kernel data receiving cache and the user space address of the receiving process is established.
- The sender process makes a system call
copyfromuser()
The datacopy
To the kernel cache in the kernel. Since there is a memory mapping between the kernel cache and the user space of the receiving process, data is sent to the user space of the receiving process, thus completing an inter-process communication.
Combined with the communication model of Binder introduced above, the communication process of Binder is as follows:
- First, a process uses
BINDER SET CONTEXT_MGR
Command throughBinder
The driver registers itself asServiceManager
; Server
By drive toServiceManager
Registered inBinder
(Server
In theBinder
Entity), indicating that services can be provided externally. It’s driven by thisBinder
Create entity nodes located in the kernel as wellServiceManager
A reference to an entity, packaged with the name and the newly created referenceServiceManager
.ServiceManger
Fill it in the lookup tableClient
By name, inBinder
Drive with help fromServiceManager
Get the rightBinder
The reference to the entity through which the sum is implementedServer
Process communication.
2.4 Binder agent Mode
Client and Server realize cross-process communication with Binder driver.
How to implement ** if **A wants an object in B? They belong to different processes and cannot be used directly.
When process A wants to retrieve object from process B, the driver does not actually return object to process A. Instead, it returns objectProxy, which looks exactly like Object. ObjectProxy has the same methods as Object, but these methods do not have the same capabilities as object methods in the B process, which only need to pass the request parameters to the driver. This is the same for process A as calling the method in object directly.
Suppose the Client process wants to call add, a method on the Server process’s object. For this cross-process communication, what does the Binder mechanism do
- First of all,
Server
The process toSM
To register,SM
Establish corresponding table - then
Client
toSM
The queryServer
. The data that processes communicate with each other passes through a driver running in kernel space, and the driver does something wrong with the data flow. It doesn’t giveClient
The process returns a trueobject
Object, but returns an object that looks likeobject
Identical proxy objectsobjectProxy
theobjectProxy
There is also aadd
Method, but thisadd
Method does notServer
Process insideobject
The object’sadd
Method that ability;objectProxy
theadd
It’s just a puppet. The only thing it does is wrap parameters and hand them to the driver Client
Access to theobjectProxy
Object, and then calladd
Methods.- The driver received the message and found this one
objectProxy
, check the table and discover what it really wants to accessobject
The object’sadd()
Methods. So driver notificationServer
Process and call itobject
The object’sadd()
Method, and return the result to the driver. Server
The process receives the message and executesadd()
After the operation returns to the driver, which returns the result toClient
Process, complete the entire communication process.
Client
The process is simply heldServer
End proxy; Proxy objects assist drivers in cross-process communication.
Combined with the above content, a secondary understanding of Binder:
-
A Binder is a communication mechanism. When WE say THAT AIDL uses Binder to communicate, we are referring to the IPC mechanism called Binder.
-
For Server processes, Binder refers to **Binder native object **
-
To Client, Binder refers to the Binder proxy object, which is simply a remote proxy of the Binder native object.
Operations on the Binder agent object are eventually forwarded to the Binder native object by the driver. A consumer with a Binder object does not need to care whether it is a Binder proxy object or a Binder native object. Operations on proxy objects are no different from operations on local objects.
-
For transport, binders are objects that can be passed across processes. Binder drivers automatically convert proxy objects to local objects for special handling of objects that can be passed across processes.
Binder objects in the Server process are Binder native objects and Binder proxy objects in the Client process.
The Binder driver automatically performs these two types of transformations as the Binder objects are passed across processes, so the Binder driver must keep information about each of the Binder objects that cross processes. In drivers, Binder local objects are represented by a data structure called binder_node. Binder proxy objects are denoted by binder_ref. Some Binder local objects are referred to as Binder entities. Binder proxy objects are called Binder references, which refer to the representation of Binder objects in drivers.
AIDL parsing
In practice, the way to implement interprocess communication is usually through AIDL. When we define the AIDL file, the compiler generates code for IPC communication at compile time.
The code compiled with AIDL is provided here to further understand how Binder IPC communicates.
AIDL (Android Interface Definition Language) is an IDL Language. Code that generates interprocess communication (IPC) between two processes on an Android device.
Only for cross-process communication, you can also choose BraodcastReceiver, Messenger, etc., but the former occupies more system resources, while the latter cannot be executed concurrently, which is not applicable in multithreading. At this time, AIDL can be implemented.
3.1 Basic Syntax
AIDL files have an.aidl suffix and are stored in a folder named AIDL (the same as the Java folder).
By default, AIDL supports the following data types:
-
Basic data types: byte, char, int, long, float, double, Boolean. Short is not supported
-
Supports String and CharSequence
-
Implements the data type of the Parcelable interface
-
The List and Map types must carry data of the types supported by AIDL
directionaltag
Except for the above data types, you must import packages before using them, even if the target is in the same package as the.aidl file you are currently writing.
When passing parameters of non-basic data types, you need to specify a direction tag to indicate the direction of the data. The value can be in, out, or Inout. The default value of basic data types, String, and CharSequence can only be in. These three modifiers, called directional tags, are used to specify the flow of data when communicating across processes.
in
: Indicates that data can only be transferred from the client to the serverout
: Indicates that data can only be transferred from the server to the clientinout
: Indicates that data can be bidirectional between the server and client
AIDL
The core class in:
-
IBinder
Is an interface that represents the ability to communicate across processes. As long as this interface is implemented, the object can be transferred to the process. This is what drives the underlying support; As cross-process data flows through the driver, the driver recognizes IBinder type data and automatically converts Binder native objects and Binder proxy objects between different processes
-
IInterface
Represents what capabilities the Server process has. (What methods can be provided, in fact, corresponds to the interface defined in AIDL file)
-
Binder
Binder classes in the Java layer represent Binder native objects.
The BinderProxy class is an inner class of Binder that represents the local proxy for the Binder objects of remote processes. Both classes inherit from IBinder and thus have the ability to transfer across processes; In fact, the Binder driver automatically converts the two objects across processes.
-
Stub
With AIDL, the compiler will give us a static inner class called Stub. This class inherits Binder, which means it is a Binder native object. It implements the IInterface interface, indicating that it has the capabilities promised by the Server to the Client. A Stub is an abstract class, and the implementation of a concrete IInterface needs to be implemented by the developer.
3.2 Basic Usage
AIDL communication is divided into two aspects: client and server:
-
The service side
Create a Service to listen for client connection requests. Create an AIDL file, declare the interface exposed to the client in the file, and then implement the AIDL interface in the Service
-
The client
Bind the server Service, convert the IBinder object returned by the server to the type of the AIDL interface, and call the methods in AIDL.
Server execution steps:
1. Define transmission data
First determine the data to be transmitted. Suppose the data to be transferred here is an object of class User that implements Parcelabel
@Parcelize
data class User(
var age: Int.var name: String
):Parcelable
Copy the code
If a custom Parcelable object is used in AIDL, you must create an AIDL file with the same name and declare it as a Parcelable type. The example uses the User class, so declare a user.aidl
// User.aidl
package com.zhewen.aidlserverstudy;
parcelable User;
Copy the code
2, create,AIDL
File that declares exposed interfaces
After defining the transferred data class, you need to create an AIDL file in which to declare the interface exposed to the client
package com.zhewen.aidlserverstudy;
import com.zhewen.aidlServerstudy.User;
interface ICommandServer {
int add(int value1, int value2);
List<User> addUser(in User user);
}
Copy the code
Custom Parcelabel objects and AIDL objects must display import regardless of whether they are in the same package as the current AIDL file
After declaring the interface exposed to the client and compiling it with a compilation tool, the corresponding Java implementation class can be obtained
//ICommandServer.java
package com.zhewen.aidlserverstudy;
public interface ICommandServer extends android.os.IInterface
{
/** Default implementation for ICommandServer. */
public static class Default implements com.zhewen.aidlserverstudy.ICommandServer
{
@Override public int add(int value1, int value2) throws android.os.RemoteException
{
return 0;
}
@Override public java.util.List<com.zhewen.aidlserverstudy.User> addUser(com.zhewen.aidlserverstudy.User user) throws android.os.RemoteException
{
return null;
}
@Override
public android.os.IBinder asBinder(a) {
return null; }}/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.zhewen.aidlserverstudy.ICommandServer
{
/** Construct the stub at attach it to the interface. */
public Stub(a)
{
this.attachInterface(this, DESCRIPTOR);
}
/** * Cast an IBinder object into an com.zhewen.aidlserverstudy.ICommandServer interface, * generating a proxy if needed. */
public static com.zhewen.aidlserverstudy.ICommandServer asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if(((iin! =null)&&(iin instanceof com.zhewen.aidlserverstudy.ICommandServer))) {
return ((com.zhewen.aidlserverstudy.ICommandServer)iin);
}
return new com.zhewen.aidlserverstudy.ICommandServer.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder(a)
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true; }}switch (code)
{
case TRANSACTION_add:
{
data.enforceInterface(descriptor);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_addUser:
{
data.enforceInterface(descriptor);
com.zhewen.aidlserverstudy.User _arg0;
if ((0! =data.readInt())) { _arg0 = com.zhewen.aidlserverstudy.User.CREATOR.createFromParcel(data); }else {
_arg0 = null;
}
java.util.List<com.zhewen.aidlserverstudy.User> _result = this.addUser(_arg0);
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
default:
{
return super.onTransact(code, data, reply, flags); }}}private static class Proxy implements com.zhewen.aidlserverstudy.ICommandServer
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder(a)
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor(a)
{
return DESCRIPTOR;
}
@Override public int add(int value1, int value2) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(value1);
_data.writeInt(value2);
boolean _status = mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
if(! _status) {if(getDefaultImpl() ! =null) {
return getDefaultImpl().add(value1, value2);
}
}
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public java.util.List<com.zhewen.aidlserverstudy.User> addUser(com.zhewen.aidlserverstudy.User user) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.zhewen.aidlserverstudy.User> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
if((user! =null)) {
_data.writeInt(1);
user.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
boolean _status = mRemote.transact(Stub.TRANSACTION_addUser, _data, _reply, 0);
if(! _status) {if(getDefaultImpl() ! =null) {
return getDefaultImpl().addUser(user);
}
}
_reply.readException();
_result = _reply.createTypedArrayList(com.zhewen.aidlserverstudy.User.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
public static com.zhewen.aidlserverstudy.ICommandServer sDefaultImpl;
}
static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addUser = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
public static boolean setDefaultImpl(com.zhewen.aidlserverstudy.ICommandServer impl) {
// Only one user of this interface can use this function
// at a time. This is a heuristic to detect if two different
// users in the same process use this function.
if(Stub.Proxy.sDefaultImpl ! =null) {
throw new IllegalStateException("setDefaultImpl() called twice");
}
if(impl ! =null) {
Stub.Proxy.sDefaultImpl = impl;
return true;
}
return false;
}
public static com.zhewen.aidlserverstudy.ICommandServer getDefaultImpl(a) {
returnStub.Proxy.sDefaultImpl; }}public static final java.lang.String DESCRIPTOR = "com.zhewen.aidlserverstudy.ICommandServer";
public int add(int value1, int value2) throws android.os.RemoteException;
public java.util.List<com.zhewen.aidlserverstudy.User> addUser(com.zhewen.aidlserverstudy.User user) throws android.os.RemoteException;
}
Copy the code
After generating this file, the system simply inherits the Stub abstract class, implements the related methods, and returns AIDL in the Service’s onBind.
Take a look at the Java file generated by the system.
public static abstract class Stub extends android.os.Binder implements com.zhewen.aidlserverstudy.ICommandServer {
/ /...
private static class Proxy implements com.zhewen.aidlserverstudy.ICommandServer{
/ /...}}Copy the code
The Stub class inherits from Binder, which is a Binder native object that implements the ICommandServer interface. The ICommandServer interface is the interface that we define to be exposed to the client, so it carries the capabilities that the client needs.
A Stub has an internal class Proxy, a Binder Proxy object
-
asInterface()
When the Client creates a server connection, it needs to create a ServiceConnection object as an input parameter when calling bindService. In ServiceConnection’s onServiceConnected callback method, the ICommandServer object is retrieved via asInterface(Android.os.ibinder obj)
/** * Cast an IBinder object into an com.zhewen.aidlserverstudy.ICommandServer interface, * generating a proxy if needed. */ public static com.zhewen.aidlserverstudy.ICommandServer asInterface(android.os.IBinder obj) { if ((obj==null)) { return null; } // Find local Binder objects android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if(((iin! =null)&&(iin instanceof com.zhewen.aidlserverstudy.ICommandServer))) { return ((com.zhewen.aidlserverstudy.ICommandServer)iin); } // Create a Binder proxy object return new com.zhewen.aidlserverstudy.ICommandServer.Stub.Proxy(obj); } Copy the code
The function takes an object of type IBinder, which is provided by the driver. Binder is a local object of Binder type. Binder proxy objects are of BinderProxy type.
The asInterface() method looks for Binder local objects. If yes, the Client and Server are in the same process. The function parameter is a local object and can be directly converted to a local object. Create a Binder proxy object to access remote objects.
-
Proxy
The add() and addUser() methods provided in the example can be invoked directly if the Client and Server are in the same process. If the call is remote, it needs to be done through a Binder Proxy, the Proxy class here
private static class Proxy implements com.zhewen.aidlserverstudy.ICommandServer { private android.os.IBinder mRemote; Proxy(android.os.IBinder remote) { mRemote = remote; } @Override public android.os.IBinder asBinder(a) { return mRemote; } public java.lang.String getInterfaceDescriptor(a) { return DESCRIPTOR; } @Override public int add(int value1, int value2) throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeInt(value1); _data.writeInt(value2); boolean _status = mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0); if(! _status) {if(getDefaultImpl() ! =null) { return getDefaultImpl().add(value1, value2); } } _reply.readException(); _result = _reply.readInt(); } finally { _reply.recycle(); _data.recycle(); } return _result; } @Override public java.util.List<com.zhewen.aidlserverstudy.User> addUser(com.zhewen.aidlserverstudy.User user) throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); java.util.List<com.zhewen.aidlserverstudy.User> _result; try { _data.writeInterfaceToken(DESCRIPTOR); if((user! =null)) { _data.writeInt(1); user.writeToParcel(_data, 0); } else { _data.writeInt(0); } boolean _status = mRemote.transact(Stub.TRANSACTION_addUser, _data, _reply, 0); if(! _status) {if(getDefaultImpl() ! =null) { return getDefaultImpl().addUser(user); } } _reply.readException(); _result = _reply.createTypedArrayList(com.zhewen.aidlserverstudy.User.CREATOR); } finally { _reply.recycle(); _data.recycle(); } return _result; } public static com.zhewen.aidlserverstudy.ICommandServer sDefaultImpl; } Copy the code
Take a look at the add() method. It first serializes the data with a Parcel and then calls the Transact () method.
The Proxy class is created in the asInterface method. The IBinder returned by the driver mentioned above is actually a BinderProxy. Therefore, the actual type of mRemote in Proxy is BinderProxy.
Take a look at BinderProxy’s transact() method
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { / /... // call a native method return transactNative(code, data, reply, flags); } Copy the code
Transact () method calls a native method, concrete implementation frameworks/base/core/jni/android_util_Binder CPP file, a series of function calls, eventually called by talkWithDriver function, The communication process is handed over to the driver.
This function is finally called by the ioctl system, and the Client process falls into kernel mode. The thread that Client calls add hangs and waits to return. After a series of operations, the driver wakes up the Server process and calls the onTransact function of the Server process’s local object (which is actually done by the server-side thread pool).
-
onTransact()
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { java.lang.String descriptor = DESCRIPTOR; switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(descriptor); return true; }}switch (code) { case TRANSACTION_add: { data.enforceInterface(descriptor); int _arg0; _arg0 = data.readInt(); int _arg1; _arg1 = data.readInt(); int _result = this.add(_arg0, _arg1); reply.writeNoException(); reply.writeInt(_result); return true; } case TRANSACTION_addUser: { data.enforceInterface(descriptor); com.zhewen.aidlserverstudy.User _arg0; if ((0! =data.readInt())) { _arg0 = com.zhewen.aidlserverstudy.User.CREATOR.createFromParcel(data); }else { _arg0 = null; } java.util.List<com.zhewen.aidlserverstudy.User> _result = this.addUser(_arg0); reply.writeNoException(); reply.writeTypedList(_result); return true; } default: { return super.onTransact(code, data, reply, flags); }}}Copy the code
OnTransact () According to the code number to confirm the Client calls which function (every AIDL function has a serial number, at the time of across processes, not transfer function, but pass number indicates which function calls), and then from the parameters of the data to retrieve the target method need, after the execution after to reply to the return value (if the target method returns a value), The driver then wakes up the thread in the suspended Client process, returns the result, and completes a cross-process call.
3, create,Service
, implement the interface
After completing the previous operations, the server also needs to create a Service and implement the interface exposed to the client
class CommandService: Service() {
override fun onBind(p0: Intent?).: IBinder? {
return mRemoteBinder
}
private val mRemoteBinder:ICommandServer.Stub = object: ICommandServer.Stub() {
override fun add(value1: Int, value2: Int): Int {
return CommandServer.sInstance.add(value1,value2)
}
override fun addUser(user: User?).: MutableList<User> {
return CommandServer.sInstance.addUser(user)
}
}
Copy the code
Client execution steps:
1. Copy files
Copy the server AIDL file to the same directory on the client and Java to keep the same directory structure. If any custom Parcelabel object is transmitted, copy it to the corresponding package name path
2. Bind the serverService
The operation of the
class MainActivity : AppCompatActivity(), View.OnClickListener {
private var mRemoteService: ICommandServer? = null
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.bind_service).setOnClickListener(this)
bindService()
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Log.d("MainActivity"."onServiceConnected")
mRemoteService = ICommandServer.Stub.asInterface(service)
// The binding service callback succeeded
}
override fun onServiceDisconnected(name: ComponentName) {
// Callback when the service is disconnected}}private fun bindService(a) {
val intent = Intent()
// Starting with Android 5.0, services must be started explicitly, not implicitly
intent.action = "com.zhewen.aidlserverstudy.aidl.commandservice"
intent.`package` = "com.zhewen.aidlserverstudy"
intent.component = ComponentName("com.zhewen.aidlserverstudy"."com.zhewen.aidlserverstudy.aidl.CommandService")
val result = bindService(intent, connection, BIND_AUTO_CREATE)
Log.d("MainActivity"."bindService$result")}override fun onClick(p0: View?). {
when(p0? .id) { R.id.bind_service -> { bindService() } R.id.add_view -> {valresult = mRemoteService? .add(11.14)
findViewById<TextView>(R.id.add_view_result_show).text = "The results -- >$result"}}}}Copy the code
directionalTag
The use of directional tags is explained briefly in the basic syntax section, which is shown in detail here.
The implementation steps are the same as the basic usage steps described above
1. Define the data class connection
@Parcelize
data class User(
var age: Int = 0.var name: String = ""
):Parcelable {
fun readFromParcel(parcel: Parcel) {
this.name = parcel.readString().toString()
this.age = parcel.readInt()
}
}
Copy the code
2, definitions,aidl
Interface and compile implementation
interface ICommandServer {
void addUserIn(in User user);
void addUserOut(out User user);
void addUserInOut(inout User user);
}
Copy the code
private val mRemoteBinder:ICommandServer.Stub = object: ICommandServer.Stub() {
override fun addUserIn(user: User?). {
Log.d(TAG,"addUserIn,userName = ${user? .name}") user? .name ="AddUserIn modify"
}
override fun addUserOut(user: User?). {
Log.d(TAG,"addUserOut,userName = ${user? .name}") user? .name ="AddUserOut modify"
}
override fun addUserInOut(user: User?). {
Log.d(TAG,"addUserInOut,userName = ${user? .name}") user? .name ="AddUserInOut modify"}}Copy the code
3. Obtain the server from the clientBinder
Object and call the related methods
fun addUserIn(a){
val user = User(11."ClientAddUserIn")
Log.d(TAG,"User. Name = before Client calls addUserIn${user.name}") mRemoteService? .addUserIn(user) Log.d(TAG,"User. Name = after Client calls addUserIn${user.name}")}fun addUserOut(a){
val user = User(22."ClientAddUserOut")
Log.d(TAG,"User. Name = before Client calls addUserOut${user.name}") mRemoteService? .addUserOut(user) Log.d(TAG,"User. Name = after Client calls addUserOut${user.name}")}fun addUserInOut(a){
val user = User(33."ClientAddUserInOut")
Log.d(TAG,"User.name = before Client calls addUserInOut${user.name}") mRemoteService? .addUserInOut(user) Log.d(TAG,"User. Name = after Client calls addUserInOut${user.name}")}Copy the code
Take a look at the log output after the call:
// In mode, the server modifies data, but the client is unaware of itCommandClient: Client calls addUserIn before user. name = ClientAddUserIn CommandService: AddUserIn,userName = ClientAddUserIn CommandClient: After the Client calls addUserIn, user. name = ClientAddUserInCopy the code
In //out mode, the client can sense the modification of the server, but cannot send data to the server, so the server does not get the data from the clientCommandClient: Client calls addUserOut before user. name = ClientAddUserOut CommandService: AddUserOut,userName = CommandClient: After the Client calls addUserOut, user.name =Copy the code
/ / inoutCommandClient: Client calls addUserInOut before user. name = ClientAddUserInOut CommandService: AddUserInOut,userName = ClientAddUserInOut CommandClient: After the Client calls addUserInOut, user.name =null
Copy the code
Note: Regarding the inout mode, the server can get the client data during personal verification, and the client can also sense the server’s modification of the data, but can’t get the modification data. Read the relevant articles can get the data, the specific reasons for further analysis
3.3 Advanced Usage
3.3.1 Permission Verification
If you need to authenticate clients using your own service for security purposes, you can perform permission authentication.
Methods a
Define and apply for a custom permission in the Manifest file of the server and specify the Permission attribute permission on the component Service
<permission android:name="com.zhewen.aidlService.permission.ACCESS_SERVICE"
android:protectionLevel="normal"/>
<uses-permission android:name="com.zhewen.aidlService.permission.ACCESS_SERVICE"/>
<! - / /... -->
<service android:name=".aidl.CommandService"
android:permission="com.zhewen.aidlService.permission.ACCESS_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="com.zhewen.aidlserverstudy.aidl.commandservice"/>
</intent-filter>
</service>
Copy the code
Then apply for this permission on the client. If the client does not apply for the service, the client will report an error and cannot start or bind the service.
Way 2
On the basis of method 1, further verification is carried out.
Verify permissions in the onBind() method of the Service class on the server side
override fun onBind(p0: Intent?).: IBinder? {
// Permission validation
Log.d(TAG,"onBind")
if(Binder.getCallingPid() == Process.myPid()) {
return mRemoteBinder
}
val check = checkCallingOrSelfPermission("com.zhewen.aidlService.permission.ACCESS_SERVICE")
if (check == PackageManager.PERMISSION_DENIED) {
Log.d(TAG,"onBind,null")
return null
}
Log.d(TAG,"OnBind permission verified")
return mRemoteBinder
}
Copy the code
Methods three
Method 3 verifies the packet name based on method 2. OnTransact () is called for every IPC communication, and client package name verification can be done in this method
private val mRemoteBinder:ICommandServer.Stub = object: ICommandServer.Stub() {
override fun onTransact(code: Int.data: Parcel, reply: Parcel? , flags:Int): Boolean {
val clientPkgName = getCallingClientPkgName(getCallingUid())
Log.d(TAG,"calling pkgName is $clientPkgName")
return super.onTransact(code, data, reply, flags)
}
/ /...
}
/ /...
private fun getCallingClientPkgName(callingUid:Int) : String {
var packageName = ""
val packages = packageManager.getPackagesForUid(callingUid)
if(packages ! =null && packages.isNotEmpty()) {
packageName = packages[0]}return packageName
}
Copy the code
3.3.2 Agent of Death
Binder runs on the server process, and if the server process dies for some reason, the Binder dies with it, and the remote call fails, affecting the client function.
Binder provides the linkToDeath method to set up a death proxy on the client. When the Binder object on the server “dies”, the client is notified of its death and the link can be restored.
class CommandClient private constructor() {
private var mRemoteService: ICommandServer? = null
private var mContext:WeakReference<Context>? = null
fun init(context:Context) {
mContext = WeakReference(context)
}
private val mDeathRecipient:IBinder.DeathRecipient = object : IBinder.DeathRecipient {
override fun binderDied(a) {
Log.d(TAG,"binder died") mRemoteService? .asBinder()? .unlinkToDeath(this.0)
mRemoteService = null
connectService()
}
}
private val mServiceConnection:ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName? , IBinder:IBinder?). {
Log.d(TAG,"onServiceConnected")
try {
// Set the death proxyIBinder? .linkToDeath(mDeathRecipient,0)
mRemoteService = ICommandServer.Stub.asInterface(IBinder)
} catch (e : RemoteException) {
e.printStackTrace()
}
}
override fun onServiceDisconnected(componentName: ComponentName?). {
Log.d(TAG,"onServiceDisconnected")
mRemoteService = null}}fun add(value1:Int, value2:Int):Int {
returnmRemoteService? .add(value1,value2)? : -1
}
fun connectService(a) : Boolean? {
val intent = Intent()
intent.action = "com.zhewen.aidlserverstudy.aidl.commandservice"
intent.`package` = "com.zhewen.aidlserverstudy"
intent.component = ComponentName("com.zhewen.aidlserverstudy"."com.zhewen.aidlserverstudy.aidl.CommandService")
returnmContext? .get()? .bindService(intent,mServiceConnection, BIND_AUTO_CREATE) }companion object {
val sInstance = SingletonHolder.holder
const val TAG = "CommandClient"
}
private object SingletonHolder{
val holder = CommandClient()
}
}
Copy the code
3.3.3 oneway
To prevent the calling thread from blocking, you can add the oneway keyword to the AIDL interface method, which is called asynchronously. Asynchronous invocation can be used to improve execution efficiency when a client calls a server-side method without knowing the return result.
Note:
The use of Oneway can be problematic in some scenarios.
The calls to Oneway by Binder drivers are similar to handler SendMessage. If the oneway interface on the server side is too slow and the client side makes too many calls, the missed calls will fill the Binder driver cache, causing other calls to throw transaction failed exceptions.
So OneWay is not suitable for scenarios where the client calls frequently and the server takes time to process. Otherwise, the preceding exception may occur and the service logic may be lost.
3.3.4 Two-way communication
Two-way communication means that the server can actively return data to the client. The core of two-way communication is that the client also creates a Binder object and delivers it to the server.
1. Interface definition
Interface IClientCallback{void doClientCallback(String value); }Copy the code
interface ICommandServer {
void registerClientCallback(IClientCallback client,String pkgName);
void unregisterClientCallback(IClientCallback client,String pkgName);
}
Copy the code
The above interfaces are mainly used for client registration and unregistration callback interfaces, enabling the server to obtain Binder objects of the client.
2. Register/unregister on the server
Implement the registration/de-registration methods defined above on the server side. Android provides RemoteCallbackList to store a collection of listening interfaces. Internally, the data is stored in an ArrayMap, with key as the Binder we transfer and value as the encapsulation of the listening interface.
When RemoteCallbackList internally operates on data, thread synchronization has been performed.
private val mRemoteBinder:ICommandServer.Stub = object: ICommandServer.Stub() {
override fun registerClientCallback(client: IClientCallback?).{ client? .let { CommandServer.sInstance.registerClientCallback(it) } }override fun unregisterClientCallback(client: IClientCallback?).{ client? .let { CommandServer.sInstance.unRegisterClientCallback(it) } } }Copy the code
class CommandServer private constructor() {companion object {
val sInstance = SingletonHolder.holder
}
private object SingletonHolder{
val holder = CommandServer()
}
// If the client dies unexpectedly, the corresponding callback will disappear by itself. Death listening can be done here
private val mClientCallbackList = object :RemoteCallbackList<IClientCallback>(){
override fun onCallbackDied(callback: IClientCallback? , cookie:Any?). {
super.onCallbackDied(callback, cookie)
}
}
fun registerClientCallback(client: IClientCallback) {
mClientCallbackList.register(client)
}
fun unRegisterClientCallback(client: IClientCallback){ mClientCallbackList.unregister(client); }}Copy the code
3, the client implementation of the exposed interface and registration and anti-registration
private val mClientCallback: IClientCallback.Stub = object : IClientCallback.Stub() {
override fun doClientCallback(value: String?). {
Log.d(TAG,"doClientCallback,value = $value")}}private val mServiceConnection:ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName? , IBinder:IBinder?). {
Log.d(TAG,"onServiceConnected")
try {
// Set the death proxyIBinder? .linkToDeath(mDeathRecipient,0) mRemoteService = ICommandServer.Stub.asInterface(IBinder) mRemoteService? .registerClientCallback(mClientCallback); }catch (e : RemoteException) {
e.printStackTrace()
}
}
override fun onServiceDisconnected(componentName: ComponentName?). {
Log.d(TAG,"onServiceDisconnected") mRemoteService? .unregisterClientCallback(mClientCallback) mRemoteService =null}}Copy the code
4. Simulate the callback on the server
//CommandService.kt
// The person is added every 5 seconds in an infinite loop to notify the observer
private val serviceWorker = Runnable {
while(! Thread.currentThread().isInterrupted) { Thread.sleep(20000)
val user = User(Random().nextInt(100),"server")
Log.d(TAG, "User = produced by server onDataChange()$user}")
CommandServer.sInstance.onDataChange(user)
}
}
override fun onDestroy(a) {
CommandServer.sInstance.clean()
super.onDestroy()
}
Copy the code
fun onDataChange(user: User) {
val callbackCount = mClientCallbackList.beginBroadcast()
for (i in 0 until callbackCount) {
try{ mClientCallbackList.getBroadcastItem(i)? .doClientCallback(user.age.toString()) }catch (e:RemoteException) {
e.printStackTrace()
}
}
mClientCallbackList.finishBroadcast()
}
fun clean(a) {
mClientCallbackList.kill()
}
Copy the code