一, 概述
AIDL 是 Android Interface Definition Language 的缩写, 即 Android 接口定义语言. 它是 Android 的进程间通信比较常用的一种方式.
Android 中, 每一个进程都有自己的 Dalvik VM 实例, 拥有自己的独立的内存空间, 进程与进程之间不共享内存, 这就产生了进程间通信的需求.
二, 语法
AIDL 是 Android 接口定义语言, 是一门语言, 所以它拥有自己的语法和特性.
(一) 数据类型
AIDL 支持的数据类型包括以下几种:
Java 的 8 种基本数据类型: int,short,long,char,double,byte,float,boolean;
CharSequence 类型, 如 String,SpannableString 等;
ArrayList
HashMap<K,V>, 并且 K 和 V 必须是 AIDL 所支持的数据类型;
所有 Parceable 接口的实现类, 因为跨进程传输对象时, 本质上是序列化与反序列化的过程;
AIDL 接口, 所有的 AIDL 接口本身也可以作为可支持的数据类型;
有两个需要注意的地方:
1, 在 Java 中, 如果一个对象和引用它的类在同一个 package 下, 是不需要导包的, 即不需要 import, 而在 AIDL 中, 自定义的 Parceable 对象和 AIDL 接口定义的对象必须在所引用的 AIDL 文件中显式 import 进来, 不管这些对象和所引用它们的 AIDL 文件是否在同一个包下.
2, 如果 AIDL 文件中使用到自定义的 Parceable 对象, 则必须再创建一个与 Parceable 对象同名的 AIDL 文件, 声明该对象为 Parceable 类型, 并且根据上一条语法规定, 在 AIDL 文件中进行显式 import.
(二) 文件类型
所有 AIDL 文件都是以. aidl 作为后缀的;
根据用途区分, AIDL 文件的有两种, 一种是用于定义接口, 另一种是用于声明 parceable 对象, 以供其他 AIDL 文件使用;
(三) 定向 tag
AIDL 中, 除了基本数据类型, 其他类型的方法参数都必须标上数据在跨进程通信中的流向: in,out 或 inout:
1,in 表示输入型参数: 只能由客户端流向服务端, 服务端收到该参数对象的完整数据, 但服务端对该对象的后续修改不会影响到客户端传入的参数对象;
2,out 表示输出型参数: 只能由服务端流向客户端, 服务端收到该参数的空对象, 服务端对该对象的后续修改将同步改动到客户端的相应参数对象;
3,inout 表示输入输出型参数: 可在客户端与服务端双向流动, 服务端接收到该参数对象的完整数据, 且服务端对该对象的后续修改将同步改动到客户端的相应参数对象;
定向 tag 需要一定的开销, 根据实际需要去确定选择什么 tag, 不能滥用.
深入理解 tag: 你真的理解 AIDL 中的 in,out,inout 么?
(四) 其他
1, 所有 AIDL 接口都是继承自 IInterface 接口的, IInterface 接口中只声明了一个 asBinder 方法:
- public interface IInterface
- {
- /**
- * Retrieve the Binder object associated with this interface.
- * You must use this instead of a plain cast, so that proxy objects
- * can return the correct result.
- */
- public IBinder asBinder();
- }
2, 系统会帮我们为所有用于定义接口的 AIDL 文件生成相应的 java 代码, 手写这份 java 代码与用 AIDL 系统生成实际上是一样的, AIDL 可以方便系统为我们生成固定格式的 java 代码.
三, 基本用法
在 AndroidStudio 中工程目录的 Android 视图下, 右键 new 一个 AIDL 文件, 默认将创建一个与 java 文件夹同级的 aidl 文件夹用于存放 AIDL 文件, 且 aidl 文件夹下的包名与 build.gradle 中配置的 applicationId 一致, 而 applicationId 默认值是应用的包名.
AIDL 的底层是基于 Binder 实现的, 而 Binder 机制也是一种请求 - 响应式的通信模型, 请求方一般称为 Client, 响应方称为 Server.
Demo 介绍: 在一个应用内部新起一个进程作为服务端, 服务端提供 addStudent 和 getStudentList 两个方法, 分别用于客户端向服务端添加 Student 数据和获取 Student 列表, Student 是自定义对象, 只有 id 和 name 两个属性. 源码下载链接 https://github.com/AmazingChen/AIDLTest .
(一) 服务端
新建 AIDL 文件, 定义一个接口, 在这个接口里声明两个方法, 分别用于添加 Student 数据和获取所有 Student 数据, 因为 AIDL 是接口定义语言, 所以不能在 AIDL 文件里对方法进行实现:
- /aidl/com/sqchen/aidltest/IStudentService.aidl
- package com.sqchen.aidltest;
- // 显式 import
- import com.sqchen.aidltest.Student;
- interface IStudentService {
- List<Student> getStudentList();
- // 定向 tag
- void addStudent(in Student student);
- }
因为 IStudentService.aidl 接口中使用到的 Student 是自定义对象, 不属于 Java 基本数据类型和 CharSequence 类型, 所以按照语法规定, 在 IStudentService.aidl 中需要显式 import, 同时我们要让 Student 实现 Parceable 接口, 并且新建一个 AIDL 文件用于声明 Student 类是 Parceable 类型:
- /aidl/com/sqchen/aidltest/Student.java
- public class Student implements Parcelable {
- private int id;
- private String name;
- public Student(int id, String name) {
- this.id = id;
- this.name = name;
- }
- @Override
- public int describeContents() {
- return 0;
- }
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(id);
- dest.writeString(name);
- }
- public void readFromParcel(Parcel parcel) {
- this.id = parcel.readInt();
- this.name = parcel.readString();
- }
- public static Parcelable.Creator<Student> CREATOR = new Parcelable.Creator<Student>() {
- @Override
- public Student createFromParcel(Parcel source) {
- return new Student(source);
- }
- @Override
- public Student[] newArray(int size) {
- return new Student[0];
- }
- };
- private Student(Parcel in) {
- this.id = in.readInt();
- this.name = in.readString();
- }
- }
- /aidl/com/sqchen/aidltest/Student.aidl
- package com.sqchen.aidltest;
- parcelable Student;
这里, 我们是在 src/main/aidl 文件夹下创建 Student.java 的, 实际上这将因为找不到 Student.java 而报错, 因为在 AndroidStudio 中使用 Gradle 构建项目时, 默认是在 src/main/java 文件夹中查找 java 文件的, 如果把 Student.java 放在 src/main/aidl 对应包名下, 自然就会找不到这个文件了, 所以需要修改 App 的 build.gradle 文件, 在 sourceSets 下添加对应的源文件路径, 即 src/main/aidl:
- Android {
- compileSdkVersion 28
- ...
- sourceSets {
- main {
- java.srcDirs = ["src/main/java", "src/main/aidl"]
- }
- }
- }
在将 src/main/aidl 添加到 sourceSets 中重新构建项目后, 在 AndroidStudio 的 Android 视图下, 项目的目录结构将发生变化, 此时会发现 aidl 文件夹不见了, 而在 java 文件夹下, 将出现两个一样包名的目录结构, 但这只是在当前视图下的一种展示方式, 将 src/main/aidl 下的文件也看作是 java 文件的存放位置, 实际上当切换到 Project 视图时, 会发现 AIDL 文件还是存在于 aidl 文件夹下, 与 java 文件夹同级.
如果 Student.java 是放在 src/main/java 对应的包名路径下, 则不需要这个步骤.
接着, 创建一个 Service 用来响应 Client 端的请求:
- /java/com/sqchen/aidltest/StudentService.java
- public class StudentService extends Service {
- private static final String TAG = "StudentService";
- private CopyOnWriteArrayList<Student> mStuList;
- private Binder mBinder = new IStudentService.Stub() {
- @Override
- public List<Student> getStudentList() throws RemoteException {
- return mStuList;
- }
- @Override
- public void addStudent(Student student) throws RemoteException {
- mStuList.add(student);
- }
- };
- @Override
- public void onCreate() {
- super.onCreate();
- init();
- }
- private void init() {
- mStuList = new CopyOnWriteArrayList<>();
- }
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
- }
在 StudentService 中, 我们创建了一个 Binder 对象并在 onBind 方法中返回它, 这个 Binder 对象继承自 IStudentService.Stub, 并实现了内部的 AIDL 方法.
我们用 CopyOnWriteArrayList 来存放 mStuList 对象, 是因为 AIDL 方法是在服务端的 Binder 线程池中执行的, 当有多个客户端同时连接时, 可能存在多个线程同时访问 mStuList 对象的情况, 而 CopyOnWriteArrayList 支持并发读写, 可以保证线程安全.
按照 AIDL 的语法规定, 只支持传输 ArrayList 对象, 而 CopyOnWriteArrayList 不是继承自 ArrayList, 为什么也可以传输呢? 这是因为 AIDL 中所支持的是抽象的 List, 而 List 只是一个接口, 虽然服务端返回的是 CopyOnWriteArrayList, 但在 Binder 中, 它会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 给客户端. 类似的还有 ConcurrentHashMap.
为 StudentService 服务端另起一个进程, 在 AndroidManifest.xml 配置文件中, 声明 Android:process=":remote", 即可创建一个新的进程实现单应用多进程, 从而模拟进程间通信. 这个进程的名字就是 remote:
- <service
- Android:name="com.sqchen.aidltest.StudentService"
- Android:process=":remote"
- Android:enabled="true"
- Android:exported="true"></service>
(二) 客户端
因为客户端和服务端是在不同的进程中, 所以客户端要想通过 AIDL 与远程服务端通信, 那么必须也要有服务端的这份 AIDL 代码.
这里分为两种情况:
1, 服务端与客户端是两个独立应用
把服务端的 aidl 文件夹整个复制到客户端的与 java 文件夹同级的目录下, 保持客户端和服务端的 aidl 文件夹的目录结构一致. 这种情况下需要注意的是, 如果前面的 Student.java 文件是放置 src/main/java 对应包名路径下, 则在拷贝 aidl 文件夹到客户端的同时, 也要将对应的 Student.java 一并拷贝到客户端相同的包名路径下.
2, 服务端与客户端是同一应用的不同进程
这种情况下因为客户端与服务端同属一个应用, 两个进程都可以使用这份 AIDL 代码, 则不需要拷贝.
客户端进程即主进程, 在 MainActivity.java 中绑定远程 StudentService, 就可以向服务端进程 remote 发起请求了:
- /java/com/sqchen/aidltest/MainActivity.java
- public class MainActivity extends AppCompatActivity implements View.OnClickListener {
- private static final String TAG = "MainActivity";
- private final static String PKG_NAME = "com.sqchen.aidltest";
- private Button btnBind;
- private Button btnAddData;
- private Button btnGetData;
- private Button btnUnbind;
- private IStudentService mStudentService;
- private ServiceConnection mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mStudentService = IStudentService.Stub.asInterface(service);
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService == null");
- return;
- }
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- };
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- initView();
- initData();
- }
- private void initView() {
- btnBind = findViewById(R.id.btn_bind);
- btnAddData = findViewById(R.id.btn_add_data);
- btnGetData = findViewById(R.id.btn_get_data);
- btnUnbind = findViewById(R.id.btn_unbind);
- initListener();
- }
- private void initListener() {
- btnBind.setOnClickListener(this);
- btnAddData.setOnClickListener(this);
- btnGetData.setOnClickListener(this);
- btnUnbind.setOnClickListener(this);
- }
- private void initData() {
- mCallback = new ITaskCallback.Stub() {
- @Override
- public void onSuccess(String result) throws RemoteException {
- Log.i(TAG, "result =" + result);
- }
- @Override
- public void onFailed(String errorMsg) throws RemoteException {
- Log.e(TAG, "errorMsg =" + errorMsg);
- }
- };
- }
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.btn_bind:
- bindStudentService();
- break;
- case R.id.btn_add_data:
- addData();
- break;
- case R.id.btn_get_data:
- getData();
- break;
- case R.id.btn_unbind:
- unbindStudentService();
- break;
- default:
- break;
- }
- }
- private void bindStudentService() {
- Intent intent = new Intent(this, StudentService.class);
- intent.setPackage(PKG_NAME);
- bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
- }
- private void addData() {
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService = null");
- return;
- }
- try {
- mStudentService.addStudent(new Student(1, "陈贤靖"));
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- private void getData() {
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService = null");
- return;
- }
- try {
- List<Student> studentList = mStudentService.getStudentList();
- Log.i(TAG, "studentList =" + studentList);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- private void unbindStudentService() {
- unbindService(mConnection);
- mStudentService = null;
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unbindStudentService();
- }
- }
在 MainActivity.java 中, 创建 4 个按钮, 分别用于绑定服务, 添加数据, 获取数据, 解绑服务:
1, 绑定服务
通过 bindService 方式启动 StudentService,ServiceConnection 是用于监视服务端状态的一个接口, 内部方法都在主线程被调用, 所以不能在该接口的方法中进行耗时操作.
- /**
- * Called when a connection to the Service has been established, with
- * the {@link Android.os.IBinder} of the communication channel to the
- * Service.
- *
- * <p class="note"><b>Note:</b> If the system has started to bind your
- * client App to a service, it's possible that your App will never receive
- * this callback. Your App won't receive a callback if there's an issue with
- * the service, such as the service crashing while being created.
- *
- * @param name The concrete component name of the service that has
- * been connected.
- *
- * @param service The IBinder of the Service's communication channel,
- * which you can now make calls on.
- */
- void onServiceConnected(ComponentName name, IBinder service);
onServiceConnected 方法是在与 Service 建立连接时被调用, 通过注释可以发现, 如果绑定服务的过程中, Service 端如果发生崩溃, 该方法将不会被回调.
- /**
- * Called when a connection to the Service has been lost. This typically
- * happens when the process hosting the service has crashed or been killed.
- * This does <em>not</em> remove the ServiceConnection itself -- this
- * binding to the service will remain active, and you will receive a call
- * to {@link #onServiceConnected} when the Service is next running.
- *
- * @param name The concrete component name of the service whose
- * connection has been lost.
- */
- void onServiceDisconnected(ComponentName name);
onServiceDisconnected 方法是在与 Service 的连接断开时被调用, 通过注释可以发现, 当 Service 发生崩溃或者由于某种原因被杀死时, 将触发该回调, 但客户端与 Service 之间的绑定关系还是存在的, 且 ServiceConnection 对象不会被移除, 当 Service 在下一次被运行起来, 那么还会再次触发 onServiceConnected 方法.
通过查看 ServiceConnection 源码可以知道, 在 onServiceConnected 方法被触发之后, 就可以对服务端 Service 进行操作了, 但是服务端通过 onServiceConnected 返回给客户端的是 IBinder 对象, 我们需要通过 mStudentService = IStudentService.Stub.asInterface(service) 将 IBinder 类型的 service 对象转化为 IStudentService 类型对象, 然后就可以调用 IStudentService 的 addStudent 和 getStudentList 方法了.
2, 添加数据
- private void addData() {
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService = null");
- return;
- }
- try {
- mStudentService.addStudent(new Student(1, "陈贤靖"));
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
先判断 mStudentService 对象是否初始化, 不为空, 则调用 addStudent 向服务端添加一个 Student 对象.
3, 获取数据
- private void getData() {
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService = null");
- return;
- }
- try {
- List<Student> studentList = mStudentService.getStudentList();
- Log.i(TAG, "studentList =" + studentList);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
先判断 mStudentService 对象是否初始化, 不为空, 则调用 getStudentList 方法获取服务端的 Student 列表数据.
4, 解绑服务
- private void unbindStudentService() {
- unbindService(mConnection);
- mStudentService = null;
- }
在 MainActivity 的 onDestory 中或其他需要的地方调用该方法进行解绑服务.
以上就是 AIDL 的基本使用, 流程可以概括为:
(1) 创建服务端的 AIDL 文件, 进行服务端方法的接口定义 (IStudentService);
(2) 创建服务端的 Service, 实现 AIDL 接口定义的方法, 并将 Binder 对象通过 onBind 方法返回给客户端;
(3) 创建客户端的 AIDL 文件, 从服务端拷贝即可, 但要保持 AIDL 文件的包名结构在服务端和客户端是一致的;
(4) 客户端绑定服务端 Service, 在成功建立与 Service 的连接之后, 拿到服务端返回的 Binder 对象, 并将 Binder 对象转为 AIDL 接口类型的对象 (IStudentService);
(5) 通过 IStudentService 类型对象调用 Service 中的实现方法;
(6) 在需要结束与服务端连接的时候, 调用 unbindService 方法进行解绑;
在创建 AIDL 文件时, 如果有报错, 通常说明某个 AIDL 文件书写不规范, 需要检查的点有:
1, 自定义对象是否实现 Parceable 接口;
2, 引用的 AIDL 对象是否显式 import;
3, 定向 tag 的使用是否正确;
4, 定向 tag 为 inout 时, 自定义对象是否同时实现 writeToParcel 和 readFromParcel;
5, 如果有修改过 java 文件的包名, 检查 AIDL 文件的包名是否正确 (是否与 applicationId 一致);
当发现问题并修改后, 可以尝试 Build->Clean Project 或 Build -> Rebuild 以重新刷新或构建项目;
三, 回调机制
在基本用法中, 只实现了客户端向服务端发送调用请求的单向通信, 但在很多场景下, 同时也需要实现服务端主动向客户端发送数据进行双向通信, 比如在观察者模式中, 当有多个客户端绑定服务端, 如果想要实现在服务端数据变化时主动通知所有与它建立绑定的客户端时, 这个时候就需要用到 AIDL 的回调机制了.
在服务端 aidl 文件夹下新建一个 AIDL 文件, 用于定义回调接口, 并声明 onSuccess 和 onFailed 方法, 这两个方法是用于业务层的, 比如服务端添加数据失败时调用 onFailed, 取决于具体场景:
- // ITaskCallback.aidl
- package com.sqchen.aidltest;
- interface ITaskCallback {
- void onSuccess(String result);
- void onFailed(String errorMsg);
- }
修改 IStudentService.aidl, 添加 register 和 unregister 方法用于客户端注册回调和解除回调:
- // IStudentService.aidl
- package com.sqchen.aidltest;
- import com.sqchen.aidltest.Student;
- // 注意: aidl 接口也要显式 import
- import com.sqchen.aidltest.ITaskCallback;
- interface IStudentService {
- List<Student> getStudentList();
- void addStudent(inout Student student);
- void register(ITaskCallback callback);
- void unregister(ITaskCallback callback);
- }
修改 StudentService.java:
- package com.sqchen.aidltest;
- import Android.App.Service;
- import Android.content.Intent;
- import Android.os.Binder;
- import Android.os.IBinder;
- import Android.os.RemoteCallbackList;
- import Android.os.RemoteException;
- import Android.util.Log;
- import java.util.List;
- import java.util.concurrent.CopyOnWriteArrayList;
- public class StudentService extends Service {
- private static final String TAG = "StudentService";
- private CopyOnWriteArrayList<Student> mStuList;
- private static RemoteCallbackList<ITaskCallback> sCallbackList;
- private Binder mBinder = new IStudentService.Stub() {
- @Override
- public void register(ITaskCallback callback) throws RemoteException {
- if (callback == null) {
- Log.i(TAG, "callback == null");
- return;
- }
- sCallbackList.register(callback);
- }
- @Override
- public void unregister(ITaskCallback callback) throws RemoteException {
- if (callback == null) {
- return;
- }
- sCallbackList.unregister(callback);
- }
- @Override
- public List<Student> getStudentList() throws RemoteException {
- return mStuList;
- }
- @Override
- public void addStudent(Student student) throws RemoteException {
- if (mStuList == null) {
- dispatchResult(false, "add student failed, mStuList = null");
- } else {
- mStuList.add(student);
- dispatchResult(true, "add student successfully");
- }
- }
- };
- @Override
- public void onCreate() {
- super.onCreate();
- Log.i(TAG, "onCreate");
- init();
- }
- private void init() {
- mStuList = new CopyOnWriteArrayList<>();
- sCallbackList = new RemoteCallbackList<>();
- }
- /**
- * 分发结果
- * @param result
- * @param msg
- */
- private void dispatchResult(boolean result, String msg) {
- int length = sCallbackList.beginBroadcast();
- for (int i = 0; i <length; i++) {
- ITaskCallback callback = sCallbackList.getBroadcastItem(i);
- try {
- if (result) {
- callback.onSuccess(msg);
- } else {
- callback.onFailed(msg);
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- sCallbackList.finishBroadcast();
- }
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
- }
在 StudentService.java 中, Binder 对象实现了 IStudentService.aidl 中新声明的两个方法, register 和 unregister, 并创建了一个 RemoteCallbackList
RemoteCallbackList<E extends IInterface> 是系统专门提供的用于跨进程传递 callback 的一种接口, 这个接口是泛型, 支持管理所有 AIDL 接口. 这里不能使用普通的 List 来存放 callback, 因为在进程间通信时, 客户端的 List 对象和服务端接收到的 List 对象不在不同的内存空间中. 正是因为不是在同一个内存空间中, 不同进程之间的数据不能进行共享, 所以才有进程间通信这个机制.
那么, 为什么 RemoteCallbackList 能实现传输前后都是相同对象呢? 查看 RemoteCallbackList 源码可以发现, 其内部创建了一个 ArrayMap 用于保存 callback:
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
这个 Map 的 key 是 IBinder 对象, 而 value 是 Callback 对象, 当客户端通过 register 方法注册回调时, 将 callback 传递给服务端, 服务端再通过 RemoteCallbackList.register 方法真正将回调进行保存:
- //RemoteCallbackList
- public boolean register(E callback, Object cookie) {
- synchronized (mCallbacks) {
- if (mKilled) {
- return false;
- }
- // Flag unusual case that could be caused by a leak. b/36778087
- logExcessiveCallbacks();
- 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;
- }
- }
- }
将我们关心的部分抽出来:
- IBinder binder = callback.asBinder();
- Callback cb = new Callback(callback, cookie);
- mCallbacks.put(binder, cb);
将客户端传递过来的 Callback 对象转为 IBinder 对象作为 key, 封装一个 Callback 作为 value. 客户端传递过来的 Callback 对象虽然在服务端被重新序列化生成一个对象, 但它们底层的 Binder 对象是同一个, 所以可以实现 Callback 的跨进程传输.
在服务端注册客户端的回调后, 服务端就可以通过这个回调主动向客户端传递数据了. 比如, 在 addStudent 中, 当添加数据成功时, 将操作的执行结果或者其他数据分发给所有向该服务端注册监听的客户端:
- /**
- * 分发结果
- * @param result
- * @param msg
- */
- private void dispatchResult(boolean result, String msg) {
- int length = sCallbackList.beginBroadcast();
- for (int i = 0; i <length; i++) {
- ITaskCallback callback = sCallbackList.getBroadcastItem(i);
- try {
- if (result) {
- callback.onSuccess(msg);
- } else {
- callback.onFailed(msg);
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- // 在调用 beginBroadcast 之后, 必须调用该方法
- sCallbackList.finishBroadcast();
- }
在客户端中创建 ITaskCallback 对象:
- //MainActivity.java
- ITaskCallback mCallback = new ITaskCallback.Stub() {
- @Override
- public void onSuccess(String result) throws RemoteException {
- Log.i(TAG, "result =" + result);
- }
- @Override
- public void onFailed(String errorMsg) throws RemoteException {
- Log.e(TAG, "errorMsg =" + errorMsg);
- }
- };
修改 ServiceConnection, 在建立连接, 调用 onServiceConnected 方法时, 进行 Callback 的注册:
- private ServiceConnection mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mStudentService = IStudentService.Stub.asInterface(service);
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService == null");
- return;
- }
- try {
- if (mCallback != null) {
- Log.i(TAG, "mCallback != null");
- mStudentService.register(mCallback);
- } else {
- Log.i(TAG, "mCallback == null");
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- };
此时, 客户端与服务端的连接已经建立, 且客户端向服务端注册了回调, 当客户端向服务端添加数据, 服务端执行 addStudent 方法时, 服务端会通过回调将添加数据的执行结果返回给客户端, 从而实现了双向通信.
四, 权限验证
默认情况下, 如果没有加入权限验证功能, 那么我们的远程服务是所有进程都可以进行连接的, 从系统安全性的角度出发, 我们还需要有相应的权限验证机制来保证系统的安全, 有两种方式:
1, 在建立连接之前
在客户端通过 bindService 方法绑定远程服务时, 我们会在服务端的 onBind 方法中将 Binder 对象返回给客户端, 那么我们可以在 onBind 方法中对来自客户端的请求进行权限验证.
2, 在客户端请求执行服务端的 AIDL 方法时
实际上, 每个 AIDL 方法都有一个唯一的方法标识 code, 服务端在 Binder.onTransact 中根据这个 code 判断并确定客户端想要调用的是哪个 AIDL 方法, 所以, 我们可以在 Binder.onTransact 中进行权限验证, 拦截非法的客户端调用.
常用的权限验证机制有包名验证和权限验证, 即根据客户端的包名或所声明的权限是否符合服务端要求来进行验证.
修改 StudentService.java 中的 Binder 对象:
- private Binder mBinder = new IStudentService.Stub() {
- ...
- @Override
- public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
- // 包名验证
- String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
- String pkgName = null;
- if (packages != null && packages.length> 0) {
- pkgName = packages[0];
- }
- if (TextUtils.isEmpty(pkgName) || !pkgName.startsWith("com.sqchen")) {
- Log.i(TAG, "invalid pkgName :" + pkgName);
- return false;
- }
- return super.onTransact(code, data, reply, flags);
- }
- };
这样, 如果客户端的包名不是以 "com.sqchen" 开头的话, 则认为是非法请求, 在 onTranscat 中返回 false 将使得客户端的请求失败, 从而达到权限验证的目的.
五, 死亡回调
当客户端与服务端之间的连接断开, 我们称之为 Binder 死亡, 此时虽然客户端和服务端都在运行, 但因为连接断开, 客户端发出的请求是不会得到响应的, 所以我们需要知道什么时候连接断开, 以便进行重新绑定, 或者执行其他操作.
前面在看 ServiceConnection 的源码时我们发现, 当连接断开时, 会调用 onServiceDisconnected 方法, 所以, 我们可以在这个方法进行重新绑定服务.
此外, Binder 中还有两个很重要的方法, linkToDeath 和 unlinkToDeath, 通过 linkToDeath 我们可以给 Binder 设置一个死亡代理 IBinder.DeathRecipient, 当 Binder 死亡时, 将会调用 DeathRecipient 的 binderDied 方法.
修改 MainActivity.java, 创建一个死亡代理, 当客户端与服务端建立连接时, 为 Binder 设置死亡代理:
- private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- if (mStudentService == null) {
- return;
- }
- // 解除死亡代理
- mStudentService.asBinder().unlinkToDeath(mDeathRecipient, 0);
- mStudentService = null;
- // 重新绑定服务
- bindStudentService();
- Log.i(TAG, "binderDied, bindService again");
- }
- };
- private ServiceConnection mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mStudentService = IStudentService.Stub.asInterface(service);
- if (mStudentService == null) {
- Log.i(TAG, "mStudentService == null");
- return;
- }
- try {
- // 设置死亡代理
- mStudentService.asBinder().linkToDeath(mDeathRecipient, 0);
- if (mCallback != null) {
- Log.i(TAG, "mCallback != null");
- mStudentService.register(mCallback);
- } else {
- Log.i(TAG, "mCallback == null");
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- // 也可以在这里重新绑定服务
- }
- };
接着, 我们模拟服务端意外死亡导致连接断开的情况, 进入 adb shell, 查找服务端进程 remote 的 pid, 并 kill 掉:
- E:\Blog\src\AIDLTest>adb shell
- mido:/ # ps
- USER PID PPID VSZ RSS WCHAN ADDR S NAME
- root 26112 28641 8968 1900 sigsuspend 74a7005e08 S sh
- root 26116 26112 10540 1992 0 7115eac768 R ps
- mido:/ # ps -A | grep com.sqchen
- u0_a140 26015 745 5238588 68324 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
- u0_a140 26046 745 5217176 39364 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote
- mido:/ # kill 26046
然后, 查看日志:
- 2019-04-05 21:43:00.530 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null
- 2019-04-05 21:45:21.955 26015-26028/com.sqchen.aidltest I/MainActivity: binderDied, bindService again
- 2019-04-05 21:45:22.048 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null
发现 remote 被 kill 之后, 确实调用了 DeathRecipient 的 binderDied 方法, 再次查看 remote 进程, 观察发现 remote 进程的 pid 在被 kill 掉前后是不一样的, 说明成功地重新绑定服务.
- E:\Blog\src\AIDLTest>adb shell
- mido:/ # ps -A | grep com.sqchen
- u0_a140 26015 745 5239648 68328 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
- u0_a140 26125 745 5217176 39604 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote
binderDied 和 onServiceDisconnected 的区别:
1,binderDied 早于 onServiceDisconnected 被调用 (参考: linkToDeath 机制了解和使用 https://www.jianshu.com/p/e38f08e34686 )
2,binderDied 在客户端的 Binder 线程池被调用, 不能在这个方法中访问 UI, 而 onServiceDisconnected 在客户端的 UI 线程被调用;
六, 需要注意的地方
1, 客户端调用远程服务的方法, 被调用的方法运行在服务端的 Binder 线程池中, 同时, 客户端线程会被挂起, 进入阻塞状态, 如果被调用的服务端方法比较耗时, 那么我们不能在客户端的主线程去调用服务端的方法, 否则将导致客户端 ANR.
2, 查看 ServiceConnection 源码时, 发现客户端的 onServiceConnnected 和 onServiceDisconnected 方法运行在主线程, 即 UI 线程, 所以也不能在这两个方法中调用服务端的耗时方法.
3, 服务端的方法运行在服务端的 Binder 线程池中, 所以在编写服务端代码时, 不需要新建线程去执行服务端方法.
源码地址: https://github.com/AmazingChen/AIDLTest
来源: https://www.cnblogs.com/sqchen/p/10660939.html