Android IPC 之AIDL

蘇燦烤魚發表於2019-04-14

最近在外面面試,多次被問到跨程式通訊,第一次以為人家問的是 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 介面和普通介面不一樣,其內部僅支援六種資料型別:

  1. 基本資料型別
  2. StringCharSequence
  3. List 介面(會自動將List介面轉為 ArrayList),且集合的每個元素都必須能夠被 AIDL 支援
  4. Map 介面(會自動將 Map 介面轉為 HashMap),且每個元素的 key 和 value 都必須被 AIDL 支援
  5. Parcelable 的實現類
  6. 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中加入下面圖片中的程式碼

Android IPC 之AIDL

  • 除了基本型別資料,其它型別的引數必須標上方向: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
    }
}
複製程式碼

建立服務端注意事項

  1. 因為 AIDL 中的方法是在服務端的Binder執行緒池執行,當服務端一對多時需要考慮方法的同步
  2. 當服務端的引數實現了List介面(或者 Map 介面),Binder就會按照List(或者Map )的規範去訪問資料並形成 ArrayList(或者HashMap) 返回給客戶端。重點是服務端不用考慮自己是什麼ListMap)。

客戶端的實現

我們不生產程式碼,我們只是程式碼的搬運工。在這裡,我就直接複製書中的程式碼了:

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()
    }
}
複製程式碼

建立客戶端注意事項

  1. 呼叫客戶端的方法可以理解為呼叫伺服器方法,即耗時操作的時候需要開啟工作執行緒
  2. 服務端返回的資料型別如上面所言,只能是ArrayListHashMap)型別

日誌:

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一樣運算元據,在獲取元素個數或者註冊、登出介面的時候需要按照示例操作,其中beginBroadcastfinishBroadcast必須配對使用,否則會有異常beginBroadcast() called while already in a broadcast或者finishBroadcast() called outside of a broadcast

RemoteCallbackListregister方法中會觸發IBinder.linkToDeath,在unregister方法中會觸發IBinder.unlinkToDeath方法。

即時通訊的注意事項

  1. 客戶端呼叫服務端的方法,被呼叫的方法執行在服務端的Binder執行緒池,同時客戶端執行緒會被掛起。服務端方法執行在Binder執行緒池當中,可以執行耗時任務,非必要不建議單獨開起工作執行緒進行非同步任務。同理,當服務端呼叫客戶端的方法時服務端掛起,被呼叫的方法執行在客戶端的Binder執行緒池,同樣需要注意耗時任務的執行緒切換
  2. 程式斷開連線的回撥有兩種方式,一個是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")
  • 自定義許可權步驟如下:
  1. 定義許可權:<permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE" android:protectionLevel="normal"/>(被驗證方直接跳過此步驟)
  2. 在專案中使用該許可權: <uses-permission android:name="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE" />
  3. 需要驗證的元件新增許可權:<service android:name=".RemoteService" android:enabled="true" android:exported="true" android:process=":jing" android:permission="com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"/>
  4. 如果定義的許可權為危險許可權,在6.0以上的系統需要動態申請:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(arrayOf("com.vincent.keeplive.permission.ACCESS_OFFER_SERVICE"),1000) }

參考:

使用Android studio建立的AIDL編譯時找不到自定義類的解決辦法

自定義許可權

原始碼

相關文章