AIDL快速使用上手
AIDL即Android接口定義語言,是用來實現跨進程通信的一種模板接口語言,AS可以根據我們編寫的AIDL生成對應的Java代碼,以方便我們的使用。它底層是使用Binder進行通信的,但是自己手寫的話是還是比較麻煩的,因此可以使用AIDL定義接口語言,然後經過構建後就會生成對應的代碼,減少我們的工作量。
之前在學習AIDL的時候也寫過Demo,但時間久了就容易忘,當再寫的時候又得去查資料,因此這裏記錄一下AIDL的使用,方便日後查詢。
新建AIDL文件
首先直接右鍵選擇new一個AIDL文件,命名爲IMyServer
,他默認會生成一個basicTypes
方法,裏面的參數是支持跨進程通信的一些基本類型。可以看到,AIDL和Java接口幾乎是一樣的,可以讓我們輕鬆上手。
// IMyServer.aidl
package com.feng.server;
// Declare any non-default types here with import statements
interface IMyServer {
/**
* 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);
}
跨進程傳遞數據
由生成的默認方法可以看到,基本數據類型int,long,boolean,float,double,String
在AIDL中都是支持的,也就是說基本類型都是可以進行跨進程傳遞的,另外還支持List
和Map
集合,它們最終會轉化成ArrayList
和HashMap
進行傳輸。
但是實際中這些類型肯定是不夠用的,因此需要我們自己實現的對象類型。而自定義的對象類型爲了能夠實現跨進程的能力,必然能夠支持序列化。在Android中有兩種方法,一個是實現Serialzable
接口,一個是實現Parcelable
接口。雖然Serializable和Parcelable都可以實現對象的序列化,但是Serializable是java的序列化接口,實現簡單,但是開銷大,序列化和反序列化有大量io操作;Parcelable是Android的接口,效率很高,Android中推薦使用Parcelable。
使用說明:
在對象序列化於內存中,儘量使用Parcelable
在對象序列化於儲存設備中,使用Serializable
在對象序列化於網絡傳輸中,使用Serializable
而AIDL主要是爲了實現進程中的通信,是序列化於內存中的,因此使用Parcelable比較好。
比如我們定義一個Person
對象
data class Person(var age: Int, var name: String?) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readInt(), parcel.readString())
// 注意這個方法
fun readFromParcel(parcel: Parcel) {
age = parcel.readInt()
name = parcel.readString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(age)
parcel.writeString(name)
}
override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<Person> {
override fun createFromParcel(parcel: Parcel) = Person(parcel)
override fun newArray(size: Int): Array<Person?> = arrayOfNulls(size)
}
override fun toString() = "[${age},${name}]"
}
實現Parcelable
接口,必須實現writeToParcel
和describeContents
兩個方法。writeToParcel是將數據寫入Parcelable中,可以將需要進行傳遞的字段寫入Parcel中進行傳遞。describeContents可以返回0
或者CONTENTS_FILE_DESCRIPTOR
,對於一個普通Bean,可以直接返回0。
還有注意,向Parcel寫入字段的順序是有要求的,先寫入的數據在讀取的時候也要先讀出來,類似於隊列一樣,先寫先讀。可以看上面的Person
類,在writeToParcel
方法中先寫入age
後寫入name
,那麼在對應的構造方法中也是先讀age
屬性後讀name
。
實現Parcelable還需要在對象中定義一個靜態對象CREATOR
,這個對象是Parcelable.Creator
類型的,它負責構造對象。在跨進程傳遞後,會被調用CREATOR
來將Parcel轉化成對象。
經過上述的修改,此時的Person就已經具有序列化的能力了,也就是能夠跨進程傳遞了,於是我們就可以在AIDL中使用它了。AIDL主要是聲明一系列的接口方法,這些方法應該是服務端的方法,也就是這裏的方法會在服務端中實現,由客戶端進行調用。
// IMyServer.aidl
package com.feng.server;
parcelable Person;
interface IMyServer {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void sendPerson(inout Person person);
Person getPerson();
}
我們在aidl中定義了兩個方法,sendPerson
和getPerson
,一個是將Person
對象發送到服務端,一個是從服務端獲取Person
對象,剛好用來測試Person
的跨進程能力。
雖然Person
已經實現了Parcelable
,但若要在aidl中使用,還是需要聲明的,就像import
一樣。可以看到上面的aidl文件中,在import的位置加上了parcelable Person
,這樣纔可以在aidl中使用Person。
當然,也可以新建另一個aidl文件,但是這個文件中不寫接口,只用來聲明這些Parcelable
類,然後在aidl接口文件中import
這個aidl聲明文件,如下:
//Person.aidl
package com.feng.server;
parcelable Person;
然後此時的IMyServer
中應該這樣寫:
package com.feng.server;
import com.feng.server.Person;
interface IMyServer {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void sendPerson(inout Person person);
Person getPerson();
}
這裏的Person.aidl
文件名是可以不設置爲Person
的,當然最好跟聲明的對象保持一致,而且該文件中也是可以聲明多個Parcelable
對象的。若是要使用Person.aidl
的話,在新建AIDL的時候可能會提示Person已存在而無法創建,這時候可以先改爲其他名字,然後再右鍵-refactor-rename
修改回來。或者先定義Person.aidl
,再定義Person.java
。
修飾符
還有就是對象定向修飾符,就像在sendPerson(inout Person person)
中的inout
一樣。這樣的修飾符一共有三個,in/out/inout
。基本類型默認爲in
,自定義的類型必須要指明。
in
:數據由客戶端流向服務端,即對象Person
會由客戶端傳遞給服務端,即正常的傳遞。服務端接收的對象內部數據與客戶端是的一致的,相當於複製了一個對象傳給了服務端。out
:數據由服務端流向客戶端,若要使用這種tag
,Person
對象還要增加一個默認的空構造方法。在服務端接收到Person後,這個Person
的內部數據都爲空,也就是不保留內部數據,實際上會調用Person
的空構造方法構造一個對象。雖然服務端拿到的這個對象並沒有數據,但是它卻是一個類似於客戶端那個對象的分身的存在,也就是說服務端對這個Person對象的字段進行修改,會同步到客戶端。比如客戶端調用sendPerson(Person)
傳遞的Person.age = 10
,而在服務端中將收到的Person的age
改爲20,此時客戶端的那個Person對象的age
就會變成20。inout
:綜合了in和out,即客戶端既能傳遞帶數據的對象,服務端對他的修改也能同步到客戶端。使用這個tag需要給Person對象增加一個readFromParcel(Parcel)方法,這個方法用於從Parcel中讀取數據。注意讀取字段的順序要與寫入的順序一致。
爲了更好的應對這些定向tag,最好在定義Person的時候就加上空構造方法和readFromParcel方法。
注意包名
在新建aidl文件時,默認使用的包名爲項目的包名,因此,定義的Person
類必須放在該包目錄下,也就是新建項目的那個默認包,否則會報錯找不到這個類,也就是說Person的包名必須和在AIDL的包名一致。
因此,當客戶端和服務端不在同一個項目中的時候,爲了實現aidl通信,需要將aidl複製到另一個項目中,此時一定要注意aidl的包名,保證aidl的包名和Person的包名一致。
生成代碼
做完以上這些後可以直接build/rebuild project
,然後就會生成對應的Java代碼。該代碼分爲兩個部分,接口實現部分和對象佔位部分。如下圖中的IMyServer.java
和Person.java
,其中IMyServe
r是主要的實現代碼,而Person.java
則是一個空文件,我們聲明的parcelable都會生成對應的佔位文件,因爲上面只聲明瞭一個Person
對象,因此也只生成了一個。
服務端
這個IMyServer.java
就是對AIDL的實現,生成這個後就可以編寫具體的客戶端和服務端代碼了。首先編寫服務端代碼:
// MyService.java
class MyService : Service() {
override fun onBind(intent: Intent): IBinder = object : IMyServer.Stub() {
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String?
) {
"basic type:[${anInt},${aLong},${aBoolean},${aFloat},${aDouble},${aString}]".logD()
}
override fun sendPerson(person: Person?) {
person?.logD()
person?.age = 19
}
override fun getPerson() = Person(23, "李華")
}
}
AIDL基本上都是基於Service
實現的,因此這裏服務端也是一個Service
。這裏定義了一個MyService
,繼承自Service
,只實現了onBind
方法。在onBind
中,我們的返回對象就是IMyServer.Stub
。這個Stub是AS根據我們的AIDL文件生成的IMyServer
的一個靜態內部抽象類,它繼承自Binder
並且包含有我們的定義的方法。這個Stub就是實現跨進程通信的基礎,因此在onBind中直接返回一個Stub對象,並實現了我們在aidl中定義的那三個方法。實現都比較簡單,都是打印一下傳遞過來的對象的信息,logD()
是定義的一個拓展方法,內部調用的是Log.d()。其中在sendPerson
中改了person的age
屬性,是爲了測試inout
的雙向傳遞能力。
寫完Service後不要忘記在Manifest
中註冊:
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.feng.server.service" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
注意要將exported
設置爲true
,這樣就可以在其他進程中調用到了。還要設置intent-filter
,因爲其他項目中是無法訪問到MyService
對象的,只能通過intent-filter
進行綁定。這裏因爲是在兩個項目中訪問的,所以沒有設置service的進程,它默認運行在當前項目進程中,若是在同一個項目中進行測試,可以設置process
屬性,以讓它運行在其他進程中。
客戶端
服務端完成之後,就要編寫客戶端代碼了:
class MainActivity : AppCompatActivity() {
// IMyServer接口,用於調用服務端代碼
private var iMyServer: IMyServer? = null
// 連接屬性,在連接上服務端Service後,將onBind返回的IBinder轉化成IMyServer
private val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
iMyServer = IMyServer.Stub.asInterface(service)
"bind connected".logD()
}
override fun onServiceDisconnected(name: ComponentName?) {
"bind disconnected".logD()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 按鈕,點擊進行綁定
bind.setOnClickListener {
val intent = Intent().apply {
action = "com.feng.server.service"
addCategory(Intent.CATEGORY_DEFAULT)
setPackage("com.feng.servce")
}
bindService(intent, conn, Context.BIND_AUTO_CREATE).logD()
}
// 按鈕,點擊開始進行交互,分別測試三個方法
send.setOnClickListener {
iMyServer?.basicTypes(12, 13L, true, 13.1F, 13.2, "from client")
val person = Person(18,"client Person")
iMyServer?.sendPerson(person)
person.logD()// 打印person對象,該對象在服務端中age被改爲了19,這裏會顯示age爲19
iMyServer?.person.logD()
}
}
override fun onDestroy() {
unbindService(conn)
super.onDestroy()
}
}
上面就是在Activity
中綁定服務的過程,在綁定上Service
的時候,在onConnected
中會將IBinder
轉化成IMyServer
。這裏的IBinder
就是Service
中的onBind
方法返回的對象,若是同一個進程該對象就是它本身,若是不同的進程中則會轉化成BinderProxy
對象。在生成的IMyServer.Stub
的靜態方法asInterface
中,會根據這種情況進行轉換。因此我們可以直接使用IMyServer
來進行調用Server的方法而不用考慮跨進程的問題。
注意,在不同的項目中是無法訪問到MyService
對象的,因此綁定服務只能通過隱式綁定,即在intent
中設置action
和category
,另外通過隱式綁定的話需要加上查找包名,即設置package
爲service
所在程序的包名。
總結
可見在AIDL的幫助下,我們跨進程的代碼非常簡單,就像是在同一個進程下一樣,可以直接通過接口引用調用服務端的方法。而背後都是AIDL幫我們生成的代碼去實現的,這些代碼全部在生成的文件IMyServer.java
中,可以根據這個文件簡單瞭解一下背後如何通過Binder
進行通信。