Android IPC 之AIDL
Posted Vicent_9920
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 接口和普通接口不一样,其内部仅支持六种数据类型:
- 基本数据类型
String
和CharSequence
- List 接口(会自动将List接口转为
ArrayList
),且集合的每个元素都必须能够被 AIDL 支持 - Map 接口(会自动将 Map 接口转为
HashMap
),且每个元素的 key 和 value 都必须被 AIDL 支持 Parcelable
的实现类- 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
创建服务端注意事项:
- 因为 AIDL 中的方法是在服务端的
Binder
线程池执行,当服务端一对多时需要考虑方法的同步 - 当服务端的参数实现了
List
接口(或者Map
接口),Binder
就会按照List
(或者Map
)的规范去访问数据并形成ArrayList
(或者HashMap
) 返回给客户端。重点是服务端不用考虑自己是什么List
(Map
)。
客户端的实现
我们不生产代码,我们只是代码的搬运工。在这里,我就直接复制书中的代码了:
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()
创建客户端注意事项:
- 调用客户端的方法可以理解为调用服务器方法,即耗时操作的时候需要开启工作线程
- 服务端返回的数据类型如上面所言,只能是
ArrayList
(HashMap
)类型
日志:
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
一样操作数据,在获取元素个数或者注册、注销接口的时候需要按照示例操作,其中beginBroadcast
和finishBroadcast
必须配对使用,否则会有异常beginBroadcast() called while already in a broadcast
或者finishBroadcast() called outside of a broadcast
。
RemoteCallbackList
在register
方法中会触发IBinder.linkToDeath
,在unregister
方法中会触发IBinder.unlinkToDeath
方法。
即时通信的注意事项
- 客户端调用服务端的方法,被调用的方法运行在服务端的
Binder
线程池,同时客户端线程会被挂起。服务端方法运行在Binder
线程池当中,可以执行耗时任务,非必要不建议单独开起工作线程进行异步任务。同理,当服务端调用客户端的方法时服务端挂起,被调用的方法运行在客户端的Binder
线程池,同样需要注意耗时任务的线程切换 - 程序断开连接的回调有两种方式,一个是
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")
- 自定义权限步骤如下:
- 定义权限:
<permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE" android:protectionLevel="normal"/>
(被验证方直接跳过此步骤) - 在项目中使用该权限:
<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"/>
- 如果定义的权限为危险权限,在6.0以上的系统需要动态申请:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) requestPermissions(arrayOf("com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"),1000)
参考:
使用Android studio创建的AIDL编译时找不到自定义类的解决办法
以上是关于Android IPC 之AIDL的主要内容,如果未能解决你的问题,请参考以下文章