一、简介
这将是一个围绕AIDL的系列文章,内容含AIDL简单使用、进阶使用、AIDL源码探索,希望从简单开始再到复杂,在这个过程中使大家既能掌握AIDL的使用方法和需要注意的细节,同时也能通过对AIDL源码的探索,使各位理解AIDL的处理原理。这篇文章为第二篇,会在第一篇基础上继续介绍AIDL的进阶使用,详细讲解RemoteCallbackList和AIDL权限验证。
- Android IPC之AIDL使用(一)
- Android IPC之AIDL使用(二)
- Android IPC之AIDL源码探索(三) 文章词汇解释:
- AS:AndroidStudio开发工具
- AIDL文件:指在aidl目录下创建的aidl接口
- AIDL类:指由aidl文件Build生成的类
二、RemoteCallbackList的使用
我们接着 Android IPC之AIDL使用(一)中,Service实现类继续看,可能有些小伙伴发现了问题。没有发现的小伙伴,可能需要先补充一个知识,在Android Binder实现机制中,为了避免在多个客户端同时调用服务端时,出现请求阻塞,所以服务端的AIDL函数执行都是在Binder线程池中执行的。现在再想,我们是否在第一篇文章中没有对多线程做同步处理呢?当然,我们这里指register()和unregister()函数。
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
private List<CallBackAIDLInterface> interfaces = new ArrayList<>();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
MyAIDLInterface.Stub stub = new MyAIDLInterface.Stub() {
... //无关内容隐藏
@Override
public void register(CallBackAIDLInterface aidl) throws RemoteException {
if (!interfaces.contains(aidl)) {
interfaces.add(aidl);
Log.d(TAG, "register: 注销成功");
}
}
@Override
public void unregister(CallBackAIDLInterface aidl) throws RemoteException {
if (interfaces.contains(aidl)) {
interfaces.remove(aidl);
Log.d(TAG, "unregister: 注销成功");
} else {
Log.d(TAG, "unregister: 注销失败");
}
}
};
}
通过前面补充的Binder知识点,在注册和注销方法中,由于是执行在Binder线程池中的,肯定存在线程安全问题,所以我们采用ArrayList去存储这个AIDL类对象是不妥的,因为ArrayList是非线程安全的。那先解决这个问题,把ArrayList换成CopyOnWriteArrayList,这里不做过多介绍,它是一个线程安全的List。
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
private CopyOnWriteArrayList<CallBackAIDLInterface> interfaces = new CopyOnWriteArrayList<>();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
MyAIDLInterface.Stub stub = new MyAIDLInterface.Stub() {
... //无关内容隐藏
@Override
public void register(CallBackAIDLInterface aidl) throws RemoteException {
if (!interfaces.contains(aidl)) {
interfaces.add(aidl);
Log.d(TAG, "register: 注销成功");
}
}
@Override
public void unregister(CallBackAIDLInterface aidl) throws RemoteException {
if (interfaces.contains(aidl)) {
interfaces.remove(aidl);
Log.d(TAG, "unregister: 注销成功");
} else {
Log.d(TAG, "unregister: 注销失败");
}
}
};
}
那是不问题都解决了呢?运行一下注册和注销函数,打印日志结果如下:
非常尴尬!注销既然失败了,为什么呢?回味Android IPC之AIDL使用(一)中内容,AIDL的核心是Binder机制,有一个很重要的点,当服务端Service和客户端不在同一个进程时,服务端拿到的只是CallBackAIDLInterface的代理对象,所以注册函数和注销函数中得到的AIDL类对象并不是客户端传入的AIDL类对象,而是它的代理对象。意味着每次函数中得到的对象都不相同,所以导致通过interfaces.contains(aidl)方法永远无法在List缓存的对象中找到相同的AIDL接口对象。那有什么解决办法呢?RemoteCallbackList是系统专门提供,用于管理跨进程IInterface类的,RemoteCallbackList可以传入泛型,查看它的类声明可以看出,支持管理任意的AIDL类,因为所有的AIDL类都继承IInteface接口。
我们做完优化,再运行看一下!
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
private RemoteCallbackList<CallBackAIDLInterface> interfaces = new RemoteCallbackList<>();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
MyAIDLInterface.Stub stub = new MyAIDLInterface.Stub() {
... //无关内容隐藏
@Override
public void register(CallBackAIDLInterface aidl) throws RemoteException {
if (interfaces.register(aidl)) {//RemoteCallbackList自带注册方法
Log.d(TAG, "register: 注册成功");
} else {
Log.d(TAG, "register: 注册失败");
}
}
@Override
public void unregister(CallBackAIDLInterface aidl) throws RemoteException {
if (interfaces.unregister(aidl)) {//RemoteCallbackList自带注销方法
Log.d(TAG, "unregister: 注销成功");
} else {
Log.d(TAG, "unregister: 注销失败");
}
}
};
}
完美!执行结果完全到位。同时RemoteCallbackList的注册和注销方法都是自带同步处理的,既解决了同步问题,也处理了跨进程注销问题。不过,需要注意RemoteCallbackList不是一个真正的List,在操作上有所差异。遍历RemoteCallbackList 必须要以下面方式进行,其中 beginBroadcast 和 finishBroadcast 必须配套使用,哪怕我们仅仅是想要获取 RemoteCallbackList 中的元素个数。
int n = mListenerList.beginBroadcast();
try {
for (int i = 0; i < n; i++) {
IOnNewPersonArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
listener.callback();
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
mListenerList.finishBroadcast();
到这里,服务端的处理基本上没有问题了。
三、AIDL权限验证
当应用出于安全考虑或不想自己的服务随意被使用,权限验证就显得很有必要,下面就来看一下如何实现,总共有三种方式,我们一一来看。
方式一:
第一步,首先在服务端AndroidManifest文件中自定义一个权限,同时申请该权限,权限名称不限。
第二步,在组件Service上指定permission属性权限。
此时,服务端的权限设定工作就完成了,如果客户端想要调用此服务,就必须在客户端申请相同的权限,见下图。
这样,才可以正常启动或绑定这个服务。如果客户端没有申请这个权限,客户端将报错SecurityException: Not allowed to bind to service Intent。
方式二:
第一步,同方式一。
第二步,在服务端Service类的onBind()方法中进行权限验证。
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
private RemoteCallbackList<CallBackAIDLInterface> interfaces = new RemoteCallbackList<>();
@Nullable
@Override
public IBinder onBind(Intent intent) {
boolean result = checkPermission(getBaseContext(), "com.zhukai.aidlclient.ACCESS_MY_SERVICE");
if (!result) {
return null;
}
return stub;
}
public static boolean checkPermission(Context context, String permission) {
//是否在当前应用
if (Binder.getCallingPid() == Process.myPid()) {
return true;
}
if (context.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
return true;
}
return false;
}
}
第二种方式是在服务端Service类中,返回Binder对象时进行了验证,如果没有权限,返回给你null对象。这里有一点需要提醒一下,网上很多文章中的验证代码没有下面这一段处理逻辑。
if (Binder.getCallingPid() == Process.myPid()) {
return true;
}
如果没有添加这段逻辑,直接调用checkCallingPermission()函数,将会返回PackageManager.PERMISSION_DENIED(-1),表示验证失败。所以一定要加上上面的处理逻辑。至于原因,我想应该是第一次调用,Binder还没有来得急把Pid修改成调用进程的Pid,会导致Pid还是当前进程的,此时就和当前线程的Pid相等。如果把两个相同的Pid传入checkCallingPermission()函数执行,会导致内部执行条件不满足,默认返回PackageManager.PERMISSION_DENIED(-1)。
方式三
介绍了前两种方式,现在可能会想,要通过验证只要知道你的权限就可以了!这也很简单,那第三种方式将会灵活复杂一点。
第一步,同方式一。
第二步,在服务端重写Stub类的onTransact()方法,具体实现看下面代码。
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
private RemoteCallbackList<CallBackAIDLInterface> interfaces = new RemoteCallbackList<>();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
MyAIDLInterface.Stub stub = new MyAIDLInterface.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (null != packages && packages.length > 0) {
packageName = packages[0];
}
if (!TextUtils.equals(packageName, "com.zhukai.aidlclient")) {//包名验证
return false;
}
boolean result = checkPermission(getBaseContext(), "com.zhukai.aidlclient.ACCESS_MY_SERVICE");//权限验证
return result && super.onTransact(code, data, reply, flags);
}
... //无关内容隐藏
}
在上面的处理中,既进行了权限验证也进行了包名验证,这样验证的安全性就更高了。这种方式有点类似于拦截器,每次IPC通信都会调用onTransact(),所以验证的频率也会更高。
四、总结
此篇希望能让大家在AIDL的使用流程以及一些注意事项上有所帮助,这篇文章是AIDL的进阶使用。学习完这部分,下篇文章我们的重点将转移到对AIDL源码分析,弄清楚AIDL的实现逻辑,以加深对AIDL的理解。欢迎关注点赞,继续阅读AIDL系列文章,后续将努力学习输出更高质量文章。