一、场景
公司项目里用了很多的独立进程的服务与其他进程之间存在了很多跨进程的通信。之前有很长一段时间没有实际去做跨进程通信 AIDL了,查阅了一些资料和文章看了些 Demo 把温习的心路历程介绍一下。
工具
Android studio
小米手机Android11(api 30)
模拟器手机Android8 (api 26)
为什么要准备两个手机,因为这里Android高版本的aidl使用有点坑的地方,需要做兼容处理,后面会讲到原因。
二、创建aidl的服务端 AIDLService(单独进程)
先创建aidl的服务端,因为正常情况下一个APP就是一个进程
新建一个服务,目前这个服务还是空类,因为需要等待aidl的创建好和aidl编译生成的新类,我们才能进一步去完善这个service。
package com.kang.aidlservice
import android.app.Service
import android.content.Intent
import android.os.IBinder
class KtvService:Service() {
override fun onBind(p0: Intent?): IBinder? {//这里参数变量显示p0是因为没下载对应sdk源码
return null
}
}
在清单文件中注册
<service
android:name=".KtvService"
android:enabled="true"
android:exported="true" >
<intent-filter>
<!--添加了一个唯一的action,供客户端隐式启动service-->
<action android:name="com.kang.aidlservice.KtvService"/>
</intent-filter>
</service>
三、在服务端创建aidl文件
1、在main右键--->New--->Directory--->选择aidl
2、新建aidl文件:
右键刚刚新建的aidl目录---》New--->AIDL--->AIDL File--->输入新建的aidl文件名
默认的aidl文件内容
我们修改后的aidl文件内容
之后就make project或者Rebuild project一下,等待编译。
编译成功之后,会在app目录下的build生成一个IKtvController.java的文件。
我们看看这个文件里面生成的内容,我们需要用到那个继承了Binder的类,因为Binder这是IPC的原理,进程之间通信就是用到的Binder机制。
3、使用上面生成的内容,完善刚刚创建的KtvService.kt类
package com.kang.aidlservice
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
class KtvService : Service() {
companion object {
private const val TAG = "binkang"
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate: ")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: ")
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(p0: Intent?): IBinder? {
return KtvBinder()
}
//内部类KtvBinder,实现了aidl文件生成的Stub类
inner class KtvBinder : IKtvController.Stub() {
override fun setPause(pause: String?) {
Log.i(TAG, "setPause: $pause")
}
override fun setPlay(play: String?) {
Log.i(TAG, "setPlay: $play")
}
}
override fun onUnbind(intent: Intent?): Boolean {
Log.i(TAG, "onUnbind: ")
return super.onUnbind(intent)
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: ")
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
Log.i(TAG, "onRebind: ")
}
}
四、创建客户端
1、我这里新建了一个项目,作为另一个APP客户端。当然也可以在服务端的那个项目新建一个APP module也是一样的。
2、将刚刚服务端的main下的aidl文件整个目录都复制过来,放到客户端的main目录下
注意这里的包名也是和服务端的一致。
之后也是一样,make project或者Rebuild project,也会在app--->build目录下生成一个IKtvController.java文件。
3、接下来就是编写客户端的mainActivity的代码,去绑定服务端的KtvService。
package com.kang.aidlclient
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Button
import com.kang.aidlservice.IKtvController
class MainActivity : AppCompatActivity() {
var iKtvController: IKtvController? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bindKtvService()
findViewById<Button>(R.id.pause).setOnClickListener {
iKtvController?.setPause("sorry~, pause")
}
findViewById<Button>(R.id.play).setOnClickListener {
iKtvController?.setPlay("hi~, play")
}
}
private fun bindKtvService() {
//通过action隐式去绑定service
val intent = Intent()
intent.action = "com.kang.aidlservice.KtvService"//服务端的清单文件中的action
intent.setPackage("com.kang.aidlservice")
bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?){
//这个回调就代表客户端和服务端建立了链接,之后便可以通信
Log.i("binkang", "onServiceConnected: ")
//获取Stub(Binder)实例,此处不能使用new的方式创建
iKtvController = IKtvController.Stub.asInterface(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
//这个断开链接的回调,主动调用service的unBindService()不会回调到这个方法
//service所在的宿主由于异常终止或者其他原因终止,导致service与访问者间断才会
//回调这个方法
Log.i("binkang", "onServiceDisconnected: ")
}
}, Context.BIND_AUTO_CREATE)
}
}
4、运行,先运行启动服务端AIDLService,再运行启动客户端AIDLClient:
这里可以看到有两个进程了。
5、看运行的结果:
到这一步,在Android11手机上是没有任何日志输出的。运行在Android8(API26)的手机上是有日志输出的。高版本兼容(看这篇Android12兼容问题第七点,才发现是版本的问题,折腾了半天时间:分享一下适配 Android 12 遇到的坑)
因此我们需要在客户端的清单文件中加上queries这个属性。
package是服务端进程包名。
这下在我的Android11小米手机上就可以看日志了:这里我少打了一个onBind()方法的日志
我后面加了这个日志打印,会在onCreate后打印:com.kang.aidlservice I/binkang: onBind:
点击客户端的两个按钮pause和play。
至此,客户端向服务端发送数据就通了。
五、服务端向客户端发送数据通信
1、我们修改一下服务端的aidl目录下的内容
新增一个aidl文件IControllerStatusListener.aidl(回调监听的作用),内容如下
修改刚刚的IKtvController.aidl,内容如下:多提供了一个方法,用于设置监听回调
修改完,接着就是make project一下,这个时候KtvService会报错:因为我们新增了一个接口方法
inner class KtvBinder : IKtvController.Stub() {
override fun setPause(pause: String?) {
Log.i(TAG, "setPause: $pause")
}
override fun setPlay(play: String?) {
Log.i(TAG, "setPlay: $play")
}
override fun setOnControllerStatusListener(i: IControllerStatusListener?) {
Log.i(TAG, "setOnControllerStatusListener: ")
}
}
修改完服务端之后,将修改之后的aidl目录复制,覆盖客户端的aidl目录(可以先删除,再复制进去,以免覆盖发生错误),同样make project就行。
2、修改一下客户端的工作,把监听设置上即可代码如下,注意回来后操作UI需要自己切线程
修改之后的代码:
package com.kang.aidlclient
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kang.aidlservice.IControllerStatusListener
import com.kang.aidlservice.IKtvController
class MainActivity : AppCompatActivity() {
var iKtvController: IKtvController? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bindKtvService()
findViewById<Button>(R.id.pause).setOnClickListener {
iKtvController?.setPause("sorry~, pause")
}
findViewById<Button>(R.id.play).setOnClickListener {
iKtvController?.setPlay("hi~, play")
}
}
private fun bindKtvService() {
//通过action隐式去绑定service
val intent = Intent()
intent.action = "com.kang.aidlservice.KtvService"//服务端的清单文件中的action
intent.setPackage("com.kang.aidlservice")
bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?){
Log.i("binkang", "onServiceConnected: ")
//获取Stub(Binder)实例,此处不能使用new的方式创建
iKtvController = IKtvController.Stub.asInterface(service)
iKtvController?.setOnControllerStatusListener(object :
IControllerStatusListener.Stub() {
override fun onPauseSucess() {
Log.i("binkang", "onPauseSucess: ")
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this@MainActivity,
"onPauseSuccess",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPauseFailed(errorCode: Int) {
Log.i("binkang", "onPauseFailed: $errorCode")
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this@MainActivity,
"onPauseFailed $errorCode",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPlaySuccess() {
Log.i("binkang", "onPlaySuccess: ")
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this@MainActivity,
"onPlaySuccess",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPlayFailed(errorCode: Int) {
Log.i("binkang", "onPlayFailed: $errorCode")
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this@MainActivity,
"onPlayFailed $errorCode",
Toast.LENGTH_SHORT
).show()
}
}
})
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.i("binkang", "onServiceDisconnected: ")
}
}, Context.BIND_AUTO_CREATE)
}
}
3、服务端代码,我们来回调暂停和播放。各自模拟一个 1 秒的耗时操作,代码如下
package com.kang.aidlservice
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
class KtvService : Service() {
var listener: IControllerStatusListener? = null
companion object {
private const val TAG = "binkang"
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate: ")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: ")
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(p0: Intent?): IBinder? {
Log.i(TAG, "onBind: ")
return KtvBinder()
}
inner class KtvBinder : IKtvController.Stub() {
override fun setPause(pause: String?) {
Log.i(TAG, "setPause: $pause")
//模拟播放耗时 1000 毫秒
if (listener != null) {
Thread {
try {
Thread.sleep(1000)
if (System.currentTimeMillis() % 2 == 0L) {
listener!!.onPauseSucess()
} else {
listener!!.onPauseFailed(1002)
}
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (remoteException: RemoteException) {
remoteException.printStackTrace()
}
}.start()
}
}
override fun setPlay(play: String?) {
Log.i(TAG, "setPlay: $play")
//模拟播放耗时 1000 毫秒
if (listener != null) {
Thread {
try {
Thread.sleep(1000)
if (System.currentTimeMillis() % 2 == 0L) {
listener!!.onPlaySuccess()
} else {
listener!!.onPlayFailed(1002)
}
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (remoteException: RemoteException) {
remoteException.printStackTrace()
}
}.start()
}
}
override fun setOnControllerStatusListener(i: IControllerStatusListener?) {
Log.i(TAG, "setOnControllerStatusListener: ")
listener = i
}
}
override fun onUnbind(intent: Intent?): Boolean {
Log.i(TAG, "onUnbind: ")
return super.onUnbind(intent)
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: ")
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
Log.i(TAG, "onRebind: ")
}
}
4、修改之后运行看效果:
5、点击按钮pause和play看日志:
另外客户端也有toast,这里就不截图了。至此,就完成了进程之间的双向通信。
六、总结
Android使用aidl实现进程之间双向通信,就是借助Binder机制的。上面实现的过程中,唯一卡住的地方就是那个高版本兼容的问题。
另外要注意一点:在 aidl 方法中如果想要操作 UI 需要自己处理线程切换到主线程,否则会报错:
Can't toast on a thread that has not called Looper.prepare(),我也确实遇到了。