Android IPC 之AIDL

最近在外面面試,屢次被問到跨進程通訊,第一次覺得人家問的是 AIDL 的使用因而簡明扼要的說了句:瞭解,可是沒有在項目中使用過。後來面試的時候這個問題被說起的頻率過高了,因而回來把《Android開發藝術探索》又翻了一遍,此次帶着問題來看書效率確實很高,所以有了本篇文章的總結html

IPC 概念介紹

IPC 是Inter-Process Communication的縮寫,意思是進程間通訊或者說跨進程通訊。通訊就如同咱們寫信、發郵件、打電話、發微信同樣,在代碼實現方式上也有以下幾種:java

  • Bundle
  • 文件共享
  • Message
  • AIDL
  • ContentProvider
  • Socket

既然實現方式達六種之多,那麼像我這種也選擇困難症的患者應該如何來選擇呢?能夠參考下表來選擇適合你本身的業務場景android

名稱 優勢 缺點 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據類型 四大組件間的進程間通訊
文件共享 簡單易用 不支持併發,沒法即時通訊 無併發訪問、數據簡單且無實時性要求的場景
AIDL 支持一對多併發通訊,支持實時通訊 使用複雜,須要自行處理線程同步 一對多通訊,且有 RPC 需求
Message 支持一對多串行通訊,支持實時通訊 不支持高併發、不支持RPC、只能傳輸Bundle支持的數據類型 低併發的一對多即時通訊,無RPC需求或者無須要返回結果的RPC需求
ContentProvider 擅長數據資源訪問,支持一對多併發數據共享,可擴展 受約束的AIDL,主要提供數據源的CRDU操做 一對多的進程間數據共享
Socket 經過網絡傳輸字節流,支持一對多併發實時通訊 實現細節煩瑣,不支持直接的RPC 網絡數據交換

*RPC 是Remote Procedure Call,意思是跨進程回調的意思git

上面介紹了六種實現方式,接下來進入主題:詳細介紹AIDL的使用。github

AIDL 的使用

AIDL 是 Android Interface Definition Language 的縮寫,意思是Android接口定義語言,用於讓某個Service與多個應用程序組件之間進行跨進程通訊,從而能夠實現多個應用程序共享同一個Service的功能。其使用能夠簡單的歸納爲服務端和客戶端,相似於Socket 同樣,服務端服務於全部客戶端,支持一對多服務。可是服務端如何服務客戶端呢?就像酒店裏的客人入住之後,叫服務員打掃一下衛生,須要按鈴同樣,服務端也須要建立一套本身的響應系統,即 AIDL 接口。 可是這個 AIDL 接口和普通接口不同,其內部僅支持六種數據類型:面試

  1. 基本數據類型(經網友 劉思奇 提醒,short並不支持)
  2. StringCharSequence
  3. List 接口(會自動將List接口轉爲 ArrayList),且集合的每一個元素都必須可以被 AIDL 支持
  4. Map 接口(會自動將 Map 接口轉爲 HashMap),且每一個元素的 key 和 value 都必須被 AIDL 支持
  5. Parcelable 的實現類
  6. AIDL 接口自己

AIDL接口的建立

建立過程就不貼圖了,直接上代碼:bash

// 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中加入下面圖片中的代碼

  • 除了基本類型數據,其它類型的參數必須標上方向: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編譯時找不到自定義類的解決辦法

自定義權限

源碼

相關文章
相關標籤/搜索