阅读 1128

Android IPC 之AIDL

最近在外面面试,多次被问到跨进程通信,第一次以为人家问的是 AIDL 的使用于是简明扼要的说了句:了解,但是没有在项目中使用过。后来面试的时候这个问题被提及的频率太高了,于是回来把《Android开发艺术探索》又翻了一遍,这次带着问题来看书效率确实很高,因此有了本篇文章的总结

IPC 概念介绍

IPC 是Inter-Process Communication的缩写,意思是进程间通信或者说跨进程通信。通信就如同我们写信、发邮件、打电话、发微信一样,在代码实现方式上也有如下几种:

  • Bundle
  • 文件共享
  • Message
  • AIDL
  • ContentProvider
  • Socket

既然实现方式达六种之多,那么像我这种也选择困难症的患者应该如何来选择呢?可以参考下表来选择适合你自己的业务场景

名称 优点 缺点 适用场景
Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件间的进程间通信
文件共享 简单易用 不支持并发,无法即时通信 无并发访问、数据简单且无实时性要求的场景
AIDL 支持一对多并发通信,支持实时通信 使用复杂,需要自行处理线程同步 一对多通信,且有 RPC 需求
Message 支持一对多串行通信,支持实时通信 不支持高并发、不支持RPC、只能传输Bundle支持的数据类型 低并发的一对多即时通信,无RPC需求或者无需要返回结果的RPC需求
ContentProvider 擅长数据资源访问,支持一对多并发数据共享,可扩展 受约束的AIDL,主要提供数据源的CRDU操作 一对多的进程间数据共享
Socket 通过网络传输字节流,支持一对多并发实时通信 实现细节烦琐,不支持直接的RPC 网络数据交换

*RPC 是Remote Procedure Call,意思是跨进程回调的意思

上面介绍了六种实现方式,接下来进入主题:详细介绍AIDL的使用。

AIDL 的使用

AIDL 是 Android Interface Definition Language 的缩写,意思是Android接口定义语言,用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。其使用可以简单的概括为服务端和客户端,类似于Socket 一样,服务端服务于所有客户端,支持一对多服务。但是服务端如何服务客户端呢?就像酒店里的客人入住以后,叫服务员打扫一下卫生,需要按铃一样,服务端也需要创建一套自己的响应系统,即 AIDL 接口。 但是这个 AIDL 接口和普通接口不一样,其内部仅支持六种数据类型:

  1. 基本数据类型(经网友 劉思奇 提醒,short并不支持)
  2. StringCharSequence
  3. List 接口(会自动将List接口转为 ArrayList),且集合的每个元素都必须能够被 AIDL 支持
  4. Map 接口(会自动将 Map 接口转为 HashMap),且每个元素的 key 和 value 都必须被 AIDL 支持
  5. Parcelable 的实现类
  6. AIDL 接口本身

AIDL接口的创建

创建过程就不贴图了,直接上代码:

// JobsInterface.aidl
package com.vincent.keeplive;

// Declare any non-default types here with import statements
// 使用import语句在此声明任何非默认类型

interface JobsInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}
复制代码

basicTypes 方法是示例,意思是支持的基本数据类型,我们直接删除即可,然后添加两个我们需要测试的方法:

// JobsInterface.aidl
package com.vincent.keeplive;

// Declare any non-default types here with import statements
// 使用import语句在此声明任何非默认类型

import com.vincent.keeplive.Offer;
interface JobsInterface {
    List<Offer> queryOffers();
    void addOffer(in Offer offer);
}

// Offer.aidl
package com.vincent.keeplive;

// Declare any non-default types here with import statements

parcelable Offer;
复制代码

创建 AIDL 注意事项

  • 使用 import 语句在此声明任何非默认类型,即自定义对象需要显示的使用 import 导入进来
  • 如果 AIDL 文件中使用了自定义的 parcelable 对象,那么必须新建一个和它同名的 AIDL 文件,如上面示例。然后在Module的build.gradle中加入下面图片中的代码

  • 除了基本类型数据,其它类型的参数必须标上方向:in、out、inout。in 表示输入;out 表示输出;inout 表示输入输出型的参数,注意按需使用,因为 out 以及 inout 在底层实现是需要一定开销的。
  • AIDL 接口仅支持方法,不支持静态变量,也不支持普通的接口
  • AIDL 所有相关的类在另一个应用使用的时候需要保证所有文件的路径完全一致,因为跨进程涉及到序列化和反序列化。假设 A 进程的 a 经过序列化传输到 B 进程,却在相同的文件路径下找不到响应的对象,这是会出错的。

服务端的实现

先上代码再说注意事项:

/**
 * 服务端service
 */
class RemoteService : Service() {

    private val TAG = this.javaClass.simpleName
    private val mList = mutableListOf<Offer>()
    private val mBinder = object :JobsInterface.Stub(){
        override fun queryOffers(): MutableList<Offer> {
            return mList;
        }

        override fun addOffer(offer: Offer) {
            mList.add(offer)
        }
    }

    override fun onCreate() {
        super.onCreate()
        mList.add(Offer(5000,"智联招聘"))
    }

    override fun onBind(intent: Intent): IBinder {
        return mBinder
    }
}
复制代码

创建服务端注意事项

  1. 因为 AIDL 中的方法是在服务端的Binder线程池执行,当服务端一对多时需要考虑方法的同步
  2. 当服务端的参数实现了List接口(或者 Map 接口),Binder就会按照List(或者Map )的规范去访问数据并形成 ArrayList(或者HashMap) 返回给客户端。重点是服务端不用考虑自己是什么ListMap)。

客户端的实现

我们不生产代码,我们只是代码的搬运工。在这里,我就直接复制书中的代码了:

class MainActivity : AppCompatActivity() {

    private val TAG = this.javaClass.simpleName
    private val mConnection = object :ServiceConnection{
        override fun onServiceDisconnected(name: ComponentName?) {

        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val manager = JobsInterface.Stub.asInterface(service)
            val list = manager.queryOffers()
            Log.e(TAG,"list type:${list.javaClass.canonicalName}")
            Log.e(TAG,"queryOffers:${list.toString()}")
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bindService(Intent(this, RemoteService::class.java),mConnection,Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        unbindService(mConnection)
        super.onDestroy()
    }
}
复制代码

创建客户端注意事项

  1. 调用客户端的方法可以理解为调用服务器方法,即耗时操作的时候需要开启工作线程
  2. 服务端返回的数据类型如上面所言,只能是ArrayListHashMap)类型

日志:

list type:java.util.ArrayList
queryOffers:[[salary:5000, company:智联招聘]]
复制代码

以上就是一次完整的 IPC 通信了,但是这样的通信只能是单向的。就好像 APP 只能访问服务器,而服务器不能访问 APP 一样,但是现在人家服务器已经有推送了,我们的服务端怎么即时通信呢?接下来就看看通过观察者实现即时通信。

即时通信的实现

即时通信原理

原理就是声明一个 AIDL 接口,然后在服务端所实现的 AIDL 接口中通过注册和注销来添加和删除声明的 AIDL 接口。然后在服务端需要发消息给客户端的时候遍历所有已注册的接口来发起通信。

代码说起比较枯燥,接下来就通过代码实战来看看具体过程吧!

即时通信的实战

1.声明 AIDL 接口

package com.vincent.keeplive.aidl;

import com.vincent.keeplive.aidl.Offer;
//  offer 观察接口
interface IOnNewOfferArrivedInterface {
    void onNewOfferArrived(in Offer offer);
}
复制代码

2.修改服务端 AIDL 接口

// JobsInterface.aidl
package com.vincent.keeplive.aidl;

// Declare any non-default types here with import statements
// 使用import语句在此声明任何非默认类型

import com.vincent.keeplive.aidl.Offer;
import com.vincent.keeplive.aidl.IOnNewOfferArrivedInterface;
interface JobsInterface {
    List<Offer> queryOffers();
    void addOffer(in Offer offer);
    void registerListener(IOnNewOfferArrivedInterface listener);
    void unregisterListener(IOnNewOfferArrivedInterface listener);
}
复制代码

3.在服务端使用接口来实现即时通信

/**
 * <p>文件描述:服务端service<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/4/14 0014 <p>
 * <p>@update 2019/4/14 0014<p>
 * <p>版本号:1<p>
 *
 */
class RemoteService : Service() {

    private val TAG = this.javaClass.simpleName
    // offer 容器
    private val mList = mutableListOf<Offer>()
    // aidl 接口专用容器
    private val mListenerList = RemoteCallbackList<IOnNewOfferArrivedInterface>()
    private val mBinder = object : JobsInterface.Stub(){
        override fun registerListener(listener: IOnNewOfferArrivedInterface?) {
            mListenerList.register(listener)
        }

        override fun unregisterListener(listener: IOnNewOfferArrivedInterface?) {
            mListenerList.unregister(listener)
        }

        override fun queryOffers(): MutableList<Offer> {
            return mList;
        }

        override fun addOffer(offer: Offer) {
            mList.add(offer)
            // 向客户端通信
            val size = mListenerList.beginBroadcast()
            for (i in 0 until size ){
                val listener = mListenerList.getBroadcastItem(i)
                listener.onNewOfferArrived(offer)
            }
            mListenerList.finishBroadcast()
        }
    }


    override fun onCreate() {
        super.onCreate()
        mList.add(Offer(5000, "智联招聘"))

    }

    override fun onBind(intent: Intent): IBinder {
        Handler().postDelayed({
            mBinder.addOffer(Offer(4500,"51job"))
        },1000)
        return mBinder
    }
}
复制代码

4.客户端接收服务端实时信息

/**
 * 客户端
 */
class MainActivity : AppCompatActivity() {

    private val TAG = this.javaClass.simpleName
    var manager:JobsInterface? = null
    private val mConnection = object :ServiceConnection{
        override fun onServiceDisconnected(name: ComponentName?) {

        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
             manager = JobsInterface.Stub.asInterface(service)
            val list = manager?.queryOffers()
            Log.e(TAG,"list type:${list?.javaClass?.canonicalName}")
            Log.e(TAG,"queryOffers:${list.toString()}")
            manager?.registerListener(mArrivedListener)
//            service?.linkToDeath({
//                // Binder 连接死亡回调 此处需要重置 manager 并发起重连
//            },0)
        }
    }

    private val mArrivedListener = object : IOnNewOfferArrivedInterface.Stub(){
        override fun onNewOfferArrived(offer: Offer?) {
            Log.e(TAG,"ThreadId:${Thread.currentThread().id}    offer:${offer}")
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bindService(Intent(this, RemoteService::class.java),mConnection,Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        manager?.let {
            if(it.asBinder().isBinderAlive){
                it.unregisterListener(mArrivedListener)
            }
        }
        unbindService(mConnection)
        super.onDestroy()
    }
}

复制代码

RemoteCallbackList

RemoteCallbackList 是专门用来处理 AIDL 接口的容器:public class RemoteCallbackList<E extends IInterface>

内部通过ArrayMap来保存客户端实现的 AIDL 接口:ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>(); 其中的Binder是客户端底层传输信息的Binder作为key,AIDL 接口作为value

RemoteCallbackList 无法像List一样操作数据,在获取元素个数或者注册、注销接口的时候需要按照示例操作,其中beginBroadcastfinishBroadcast必须配对使用,否则会有异常beginBroadcast() called while already in a broadcast或者finishBroadcast() called outside of a broadcast

RemoteCallbackListregister方法中会触发IBinder.linkToDeath,在unregister方法中会触发IBinder.unlinkToDeath方法。

即时通信的注意事项

  1. 客户端调用服务端的方法,被调用的方法运行在服务端的Binder线程池,同时客户端线程会被挂起。服务端方法运行在Binder线程池当中,可以执行耗时任务,非必要不建议单独开起工作线程进行异步任务。同理,当服务端调用客户端的方法时服务端挂起,被调用的方法运行在客户端的Binder线程池,同样需要注意耗时任务的线程切换
  2. 程序断开连接的回调有两种方式,一个是ServiceConnection.onServiceDisconnected(),该方法运行在客户端的 UI 线程;另一个是Binder.DeathRecipient.binderDied(),该方法运行在客户端的Binder线程池,不能访问 UI

日志:

queryOffers:[[salary:5000, company:智联招聘]]
ThreadId:1262    offer:[salary:4500, company:51job]
复制代码

AIDL 权限验证

默认情况下,我们的远程访问任何人都可以使用,这不是我们希望看到的,因此需要添加权限验证。权限验证可以在服务端的onBind()方法中执行,也可以在onTransact()方法中执行,既可以自定义权限验证,也可以通过包名的方式验证。

示例:

private val mBinder = object : JobsInterface.Stub(){
        ......

        override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
            // 验证权限 返回false代表权限未验证通过
            val check = checkCallingOrSelfPermission("com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE")
            if(check == PackageManager.PERMISSION_DENIED){
                return false
            }
            val packages = packageManager.getPackagesForUid(Binder.getCallingUid())
            if(packages != null && packages.size>0){
                if(!packages[0].endsWith("keeplive")){
                    return false
                }
            }
            return super.onTransact(code, data, reply, flags)
        }
    }
    
// AndroidManifest
<uses-permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"/>
<service android:name=".RemoteService"
                 android:enabled="true"
                 android:exported="true"
                 android:process=":jing"
                 android:permission="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"/>
复制代码

自定义权限注意事项

  • 设置了自定义权限的组件开起时需要通过隐式的开启Intent().setClassName("com.vincent.keeplive","com.vincent.keeplive.RemoteService")
  • 自定义权限步骤如下:
  1. 定义权限:<permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE" android:protectionLevel="normal"/>(被验证方直接跳过此步骤)
  2. 在项目中使用该权限: <uses-permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE" />
  3. 需要验证的组件添加权限:<service android:name=".RemoteService" android:enabled="true" android:exported="true" android:process=":jing" android:permission="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"/>
  4. 如果定义的权限为危险权限,在6.0以上的系统需要动态申请:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(arrayOf("com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"),1000) }

参考:

使用Android studio创建的AIDL编译时找不到自定义类的解决办法

自定义权限

源码

关注下面的标签,发现更多相似文章
评论