Android IPC之AIDL使用(二)线程同步、权限验证

1,878 阅读2分钟

一、简介

这将是一个围绕AIDL的系列文章,内容含AIDL简单使用、进阶使用、AIDL源码探索,希望从简单开始再到复杂,在这个过程中使大家既能掌握AIDL的使用方法和需要注意的细节,同时也能通过对AIDL源码的探索,使各位理解AIDL的处理原理。这篇文章为第二篇,会在第一篇基础上继续介绍AIDL的进阶使用,详细讲解RemoteCallbackList和AIDL权限验证。

二、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: 注销失败");
            }
        }
    };

}

那是不问题都解决了呢?运行一下注册和注销函数,打印日志结果如下:

image.png 非常尴尬!注销既然失败了,为什么呢?回味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: 注销失败");
            }
        }
    };

}

image.png

完美!执行结果完全到位。同时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文件中自定义一个权限,同时申请该权限,权限名称不限。

image.png 第二步,在组件Service上指定permission属性权限。

image.png 此时,服务端的权限设定工作就完成了,如果客户端想要调用此服务,就必须在客户端申请相同的权限,见下图。

image.png 这样,才可以正常启动或绑定这个服务。如果客户端没有申请这个权限,客户端将报错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系列文章,后续将努力学习输出更高质量文章。