Android进程间通信通常使用的Binder的方式,而Binder则是以C/S模式实现的,即Binder的主体作为服务端提供服务,Binder的引用端作为客户端请求服务。但是Binder是在Framework层使用C++实现的,为了方便Android应用端的开发,对Native层的Binder进行包装实现了Java层的Binder,并又进一步提供了AIDL的实现方式来简化进程间调用,这就是本篇文章所记录的内容。
Binder的媒介 作为C/S架构的实现,Binder需要客户端和服务端。服务端也就是Binder的主体,需要注册在ServiceManager中;客户端会通过Binder的名字获取到对应Binder的代理,然后就可以进行通信了。但是在应用层开发中,我们肯定不能直接注册Binder的,因此需要匿名Binder的实现,而匿名Binder应用最广泛的就是四大组件的Service了。
1 2 3 4 5 class ServerService : Service () { override fun onBind (intent: Intent ?) : IBinder? { } }
通过bindService方式启动的服务,可以实现onBind方法来返回Binder实体作为服务端对象。当服务被绑定后,该Service进程会被拉起来,此时该Service进程就是服务端了。绑定者所在的进程会拿到IBinder的代理对象,然后两个进程就通过IBinder来进行进程间的交互。
服务端 前面说了Binder获取的方式,后面则是Binder的实现了。为了简化Binder的实现,Android提供了AIDL的方式来封装各种Binder通用的操作,让开发者仅需关注具体的业务功能即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 plugins { ... } android { ... buildFeatures { aidl = true } } dependencies { ... }
首先需要在build.gradle.kts中开启AIDL的支持,即在android闭包下新增buildFeatures闭包,然后声明aidl=true,groovy语法也是一样的。然后在源码的同级目录下新增aidl目录app\src\main\aidl,该目录就是存放AIDL文件的目录。
如果直接通过右键-new-AIDL-new aidlFile创建AIDL文件的话,会在aidl目录下生成一个和当前应用同样的包名目录,然后新的AIDL文件则是在这个目录下。实际上,这个包名是不必和应用的包名一致的,可以随意定义。AIDL文件不会有任何IDE提示的,需要我们完全手写。
1 2 3 4 5 6 7 8 app\src\main\aidl\com\server\aidl\IServer.aidl package com.server.aidl; interface IServer { // 定义了一个方法,服务端提供文本 String getTextFromServer(); }
接下来需要点击AS的build -> make project触发文件的生成,AS会根据AIDL文件去生成对应的Binder类,然后我们继承该类即可。生成的文件在app\build\generated\aidl_source_output_dir\debug\out\com\server\aidl\IServer.java中,当然我们不需要关注他在哪,我们直接使用即可。新建一个类继承IServer.Stub,然后实现方法即可。如果提示找不到这个类的话,尝试clean一下再重新build。
1 2 3 4 5 6 7 8 9 class Server : IServer.Stub (){ override fun getTextFromServer () : String { return "我是服务端返回的文本" } }
然后在Service中的onBind将该对象返回即可。
1 2 3 4 5 class ServerService : Service () { override fun onBind (intent: Intent ?) : IBinder? { return Server() } }
注意在manifest中声明时,exported属性必须设置成true并且定义intent-filter,因为我们是要在另一个app中绑定该服务的。另外,在exported设为true后,还需要通过权限进行控制,这里偷懒就不加权限了。
1 2 3 4 5 6 7 8 9 10 11 <manifest xmlns:android ="http://schemas.android.com/apk/res/android" <application > <service android:name =".ServerService" android:exported ="true" > <intent-filter > <action android:name ="com.example.server.action" /> </intent-filter > </service > </application > </manifest >
至此,服务端所做的工作已经完成了。总结下来就是创建一个aidl文件并声明所支持的接口方 法,然后build生成对应的类,然后再继承生成的xxx.Stub类实现支持的接口方法即可。
客户端 客户端要做的工作更简单了,首先创建一个新的项目,然后在build.gradle.kts中开启aidl的支持,然后将服务端创建的aidl复制到客户端的目录下。注意:复制过来的AIDL文件的包名必须和服务端保持一致 。然后Build一下生成具体的代码。
接下来就是在客户端中绑定服务端的Service,然后拿到IBinder对象,然后就可以进行交互了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class MainActivity : AppCompatActivity () { private lateinit var mBtnRequest: Button private lateinit var mTextView: TextView private var mServer: IServer? = null private val mConnection = object : ServiceConnection { override fun onServiceConnected (name: ComponentName ?, service: IBinder ?) { mServer = IServer.Stub.asInterface(service) } override fun onServiceDisconnected (name: ComponentName ?) { mServer = null } } override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } init () } private fun init () { mBtnRequest = findViewById(R.id.btn_request) mTextView = findViewById(R.id.text) mBtnRequest.setOnClickListener { mTextView.text = mServer?.textFromServer ?: "default" } val intent = Intent().also { it.action = "com.example.server.action" it.`package ` = "com.example.serverdemo" } bindService(intent, mConnection, BIND_AUTO_CREATE) } override fun onDestroy () { super .onDestroy() unbindService(mConnection) } }
服务端很简单,在Activity启动时绑定服务端的服务,然后拿到IBinder通过生成的IServer.Stub.asInterface转换成IServer,然后就可以直接进行交互了。因为我们是在客户端app中绑定服务端app的,在Android 11以后需要在Manifest中声明要绑的包名,否则无法绑定成功。
1 2 3 4 5 6 7 8 9 10 <manifest <!-- 必须声明服务端的包名 -- > <queries > <package android:name ="com.example.serverdemo" /> </queries > <application </application > </manifest >
AIDL支持的类型 前面的示例说明了AIDL的交互方式,从代码层面上看,在客户端拿到IServer后,可以直接调用方法进行交互,即以同步的方式进行跨进程的调用。既然实际是跨进程调用的,那么实现上肯定是有一些限制的。如参数的类型,就不是全部类型都能使用的。
默认数据类型 基本数据类型:byte, char, int, long, float, double, boolean, 以及基本类型对应的数组
字串类型:CharSequence, String, 以及字串类型对应的数组
集合类型:List, Map, 最新的版本中集合类型已经不局限于ArrayList和HashMap了
除了以上类型外,如果想要传递自定义的类型,则需要实现Parcelable接口。
手动定义Parcelable 首先在代码目录中创建一个User类,然后实现Parcelable接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.example.serverimport android.os.Parcelimport android.os.Parcelabledata class User ( val name:String, val age:Int ):Parcelable { constructor (parcel: Parcel) : this ( parcel.readString().toString(), parcel.readInt() ) { } override fun writeToParcel (parcel: Parcel , flags: Int ) { parcel.writeString(name) parcel.writeInt(age) } override fun describeContents () : Int { return 0 } companion object CREATOR : Parcelable.Creator<User> { override fun createFromParcel (parcel: Parcel ) : User { return User(parcel) } override fun newArray (size: Int ) : Array<User?> { return arrayOfNulls(size) } } }
然后在aidl目录中新建一个User.aidl,注意该文件的包名必须和User类的包名保持一致 :
1 2 3 4 package com.example.server; parcelable User;
然后实际使用的地方也需要进行导包 ,注意as不会提示导包,必须要手动输入:
1 2 3 4 5 6 7 8 9 package com.server.aidl;import com.example.okhttpdemo3.User;interface IServer { String getTextFromServer () ; void otherTypes ( in User user ) ; }
自动生成Parcelable 前面手动生成Parcelable的地方,User类定义在了代码目录中,因此当复制aidl文件到客户端的时候,必须要把User类的文件也同时复制过去,并且包名路径要完全一致,这无疑是很麻烦的。因此可以使用aidl自动生成Parcelable的方式来实现User类。
1,删除前面定义的User类文件
2,修改User.aidl内容
1 2 3 4 5 6 7 package com.example.server; parcelable User { int age; String name; }
这样build之后会自动生成一个User类,不需要再写那些复杂的逻辑了。
属性修饰符 在前面的自定义Parcelable时,可以看到在AIDL使用自定义Parcelable类型的时候,加上了属性修饰符in。实际上,除了byte, char, int, long, float, double, boolean, CharSequence, String外,其他的类型作为参数时都必须添加属性修饰符。
in 数据从客户端传递到服务端,也是最常用的调用方式。也就是数据作为参数从客户端传入,可以理解为值传递,就是在服务端对这个参数进行修改的话,是影响不到客户端的。
1 2 3 4 5 6 7 8 9 10 package com.server.aidl;import com.example.okhttpdemo3.User;interface IServer { String getTextFromServer () ; void otherTypes ( // 定义成in类型 in User user ) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Server : IServer.Stub () { private val TAG = "Server" override fun getTextFromServer () : String { return "服务端返回的字串" } override fun otherType (user: User ?) { Log.d(TAG, "user.name = ${user?.name} , user.age = ${user?.age} " ) user?.age = 80 } } mBtnRequest.setOnClickListener { val user = User().also { it.name = "张三" it.age = 23 } mServer?.otherType(user) Log.d(TAG, "after call, name = ${user.name} , age = ${user.age} " ) }
打印的结果值如下,可以看到即使在服务端修改了User的字段,实际上客户端的User仍没有发生变化:
1 2 com.example.serverdemo D user.name = 张三, user.age = 23 com.example.clientdemo D after call, name = 张三, age = 23
out 数据由服务端流向客户端,这种方式服务端无法获取到User的数据,但是对User的修改却能反馈到客户端中去。将前面aidl文件中的in修改为out后,在分别build并运行后,输入的log如下:
1 2 com.example.serverdemo D user.name = null, user.age = 0 com.example.clientdemo D after call, name = null, age = 80
可以看到,在客户端实际传入的是一个有内容的User,但是在服务端中是无法读取到User的内容的。但是在服务端修改User后,客户端的User也修改了,并且客户端的原始内容被清空了。因此,out修饰符就是提供一个对象,用于接收服务端对该对象的修改。
inout 数据双向流通,即服务端能获取到客户端传入的内容,客户端也能收到服务端对该内容的修改。将前面aidl文件中的in修改为out后,在分别build并运行后,输入的log如下:
1 2 com.example.serverdemo D user.name = 张三, user.age = 23 com.example.clientdemo D after call, name = 张三, age = 80
可以看到,服务端能读取到User的内容,并且对User内容的修改也能反馈到客户端。这种修饰符修饰下,对跨进程调用的方法,表现形式是和本进程内的调用java方法一样的。
总结 AIDL是Android提供的跨进程调用的工具,实际上AIDL文件是不会被打包到app中的,只是根据其内容来生成代码文件,将跨进程那一套通用的逻辑都自动生成了,方便我们只关注业务逻辑部分。从使用上来说,AIDL一般在系统应用中才用的比较多,因为很多系统应用间都需要互相交互,使用AIDL无疑是非常方便的。而对于普通应用,一般是大体量的app会将本身分成多个进程,然后去使用AIDL实现进程间交互。