AIDL快速使用上手

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中都是支持的,也就是說基本類型都是可以進行跨進程傳遞的,另外還支持ListMap集合,它們最終會轉化成ArrayListHashMap進行傳輸。

  但是實際中這些類型肯定是不夠用的,因此需要我們自己實現的對象類型。而自定義的對象類型爲了能夠實現跨進程的能力,必然能夠支持序列化。在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接口,必須實現writeToParceldescribeContents兩個方法。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中定義了兩個方法,sendPersongetPerson,一個是將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:數據由服務端流向客戶端,若要使用這種tagPerson對象還要增加一個默認的空構造方法。在服務端接收到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.javaPerson.java,其中IMyServer是主要的實現代碼,而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中設置actioncategory,另外通過隱式綁定的話需要加上查找包名,即設置packageservice所在程序的包名。

總結

  可見在AIDL的幫助下,我們跨進程的代碼非常簡單,就像是在同一個進程下一樣,可以直接通過接口引用調用服務端的方法。而背後都是AIDL幫我們生成的代碼去實現的,這些代碼全部在生成的文件IMyServer.java中,可以根據這個文件簡單瞭解一下背後如何通過Binder進行通信。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章