Binder機制

玩毛線的包子發表於2018-08-27

Binder,英文意思是別針、回形針。在Android中用於完成程式間通訊(IPC),即把多個程式“別”在一起。比如,普通應用程式可以呼叫音樂播放服務提供的播放、暫停、停止等功能。

Binder框架

Binder是一種架構,這種架構提供了服務端介面、Binder驅動、客戶端介面三個模組。

Binder機制

服務端:一個Binder服務端實際上就是一個Binder類的物件,該物件一旦建立,內部就啟動一個隱藏執行緒。該執行緒接下來會接收Binder驅動傳送的訊息,收到訊息後,會執行到Binder物件中的onTransact()函式,並按照該函式的引數執行不同的服務程式碼。因此,要實現一個Binder服務,就必須過載onTransact()方法。

Binder驅動:任意一個服務端Binder物件被建立時,同時會在Binder驅動中建立一個mRemote物件,該物件的型別也是Binder類。客戶端要訪問遠端服務時,都是通過mRemote物件。

應用程式客戶端:客戶端要想訪問遠端服務,必須獲取遠端服務在Binder物件中對應的mRemote引用。獲得該mRemote物件後,就可以呼叫transact()方法,而在Binder驅動中,mRemote物件也過載了transact()方法,過載的內容主要包括以下幾項。

  1. 以執行緒間訊息通訊的模式,向服務端傳送客戶端傳遞過來的引數
  2. 掛起當前執行緒,當前執行緒正是客戶端執行緒,並等待服務端執行緒執行完指定服務函式後通知(notify)
  3. 接收到服務端執行緒的通知,然後繼續執行客戶端執行緒,並返回到客戶端程式碼區

從這裡可以看出,對應用程式開發員來講,客戶端似乎是直接呼叫遠端服務對應的Binder,而事實上則是通過Binder驅動進行了中轉。即存在兩個Binder物件,一個是服務端的Binder物件,另一個則是Binder驅動中的Binder物件,所不同的是Binder驅動中的物件不會再額外產生一個執行緒。

設計Server端

public class MusicPlayerService extends Binder {
    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        return super.onTransact(code, data, reply, flags);
    }

    public void start(String filePath) {
    }

    public void stop() {
    }
}
複製程式碼

初始化MusicPlayerService時,會多一個執行緒。

code:用於標識客戶端期望呼叫服務的哪個函式,因此,雙方需要約定一組int值,不同的值,代表不同的服務端函式,該值和客戶端的transact()函式中第一個引數code的值是一致的。這裡假定1000是雙方約定要呼叫start()函式的值。

data:資料,當約定的code定義好之後,就可以知道data放的是什麼資料。

reply:服務端把返回的結果放在其中。

flag:執行IPC呼叫的模式,分為兩種:一種是雙向,用常量0表示,其含義是服務端執行完指定服務後會返回一定的資料;另一種是單向,用常量1表示,其含義是不返回任何資料。

onTransact(int code, Parcel data, Parcel reply, int flags) {
    switch (code){
        case 1000:
            data.enforceInterface("MusicPlayerService");
            String filePath = data.readString();
            start(filePath);
            break;
    }
}
複製程式碼

enforceInterface()是為了某種校驗,它與客戶端的writeInterfaceToken()對應

Binder客戶端設計

    IBinder mRemote = null;
    // 獲取mRemote物件
    String filePath = "/sdcard/music/xxx.mp3";
    int code = 1000;
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken("MusicPlayerService");
    data.writeString(filePath);
    mRemote.transact(code, data, reply, 0);
    IBinder binder = reply.readStrongBinder();
    reply.recycle();
    data.recycle(); 
複製程式碼

使用步驟:

  1. 申明code,客戶端與服務端的協議。
  2. 獲取data,用於放置資料。可以放入原子型別或者繼承Parcel的類。注意獲取方式是Parcel.obtain()
  3. 獲取reply,同data。
  4. writeInterfaceToken(),標註遠端服務的名稱,不是必需的,申明只是為了確保呼叫的指定的客戶端。
  5. data寫入資料,writeString(),寫入資料的順序也是和服務端約定好的。
  6. 呼叫transact()

使用Service類

Binder服務端和客戶端存在兩個重要的問題

  1. 客戶端如何獲取服務端的Binder物件應用
  2. 客戶端和服務端必須事先約好兩件事情
    • 服務端函式的引數在包裹中的順序
    • 服務端不同函式的int標識

獲取Binder物件

AMS提供了startService()用於客戶端啟動服務,要獲取Binder物件,需要bindService(Intent service, ServiceConnection conn, int flags)

public interface ServiceConnection {
    public void onServiceConnected(ComponentName name, IBinder service);
    public void onServiceDisconnected(ComponentName name);
}
複製程式碼

呼叫流程如下:

Binder機制

保證包裹內引數順序aidl工具的使用

Android的SDK中提供了一個aidl工具,該工具可以把一個aidl檔案轉換為一個java類檔案,在該java類檔案,同時過載了transact和onTransact()方法,統一了存入包裹和讀取包裹引數,從而使設計者可以把注意力放到服務程式碼本身上。

package com.kongge.test
interface IMusicPlayerService {
    boolean start(String filePath);
    void stop();
}
複製程式碼

檔名IMusicPlayerService.aidl,經過編譯後會生成IMusicPlayerService.java檔案,aidl語法基本類似Java,package指定輸出後的Java檔案對應的報名,包裹類只能寫入三種型別資料。

  1. java原子型別,如int、long、String等變數
  2. Binder引用
  3. 實現了parcelable的物件

生成的IMusicPlayerService.java

package com.kongge.test
public interface IMusicPlayerService extends android.os.IInterface 
{
    public static abstract class Stub extends android.os.Binder implements com.kongge.test.IMusicPlayerService
    {
        public static IMusicPlayerService asInterface(IBinder obj){...}
        pubilc IBinder asBinder(){...}
        public boolean OnTransaction(int code, Parcel data, Parcel reply, int flags){...}
        
        private static class Proxy implements com.kongge.test.IMusicPlayerService
        {
            private IBinder mRemote;
            public IBinder asBinder() {retrun mRemote}
            public String getInterfaceDescriptor(){...};
            public boolean start(String filePath){...} // aidl檔案定義的方法
            public void stop(){...} // aidl檔案定義的方法
        }
        static final int TRANSACTION_start = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_stop = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }
    public boolean start(java.lang.String filePath) throws android.os.RemoteException;
    public void stop() throws android.os.RemoteException;
}
複製程式碼

這些程式碼主要完成以下三個任務

  1. 定義一個Java interface,內部包含aidl檔案所宣告的服務函式,類名稱為IMusicPlayerService,並且該類基於IInterface介面,即需要提供一個asBinder()函式。
  2. 定義一個Proxy類,該類將作為客戶端程式訪問服務端的代理。所謂的代理主要就是為了前面所提到的第二個重要問題—統一包裹內寫入引數的順序。
  3. 定義一個Stub類,這是一個abstract類,基於Binder類,並且實現了IMusicPlayerService介面,主要由服務端來使用。該類之所以要定義為一個abstract類,是因為具體的服務函式必須由程式設計師實現,因此,IMusicPlayerService介面中定義的函式在Stub類中可以沒有具體實現。同時,在Stub類中過載了onTranstact()方法,由於transact()方法內部給包裹內寫入引數的順序是由aidl工具定義的,因此,在onTransact()方法中,aidl工具自然知道應該按照何種順序從包裹中取出相應引數。

在Stub類中,除了以上所述的任務外,tub還提供了一個asInterface()函式,提供這個函式的原因是服務端提供的服務除了其他程式可以使用外,在服務程式內部的其他類也可以使用該服務,對於後者,,顯然是不需要經過IPC呼叫,而可以直接在程式內部呼叫的,而Binder內部有一個queryLocalInterface(String description)函式,該函式是根據輸入的字串判斷該Binder物件是一個本地的Binder引用。在圖一所示中曾經指出,當建立一個Binder物件時,服務端程式內部建立一個Binder物件,Binder驅動中也會建立一個Binder物件。如果從遠端獲取服務端的Binder,則只會返回Binder驅動中的Binder物件,,而如果從服務端程式內部獲取Binder物件,則會獲取服務端本身的Binder物件。

Binder機制

因此,asInterface()函式正是利用了queryLocalInterface()方法,提供了一個統一的介面。無論是遠端客戶端還是本地端,當獲取Binder物件後,要以把獲取的Binder物件作為asInterface()的引數,從而返回一個IMusicPlayerService介面,該介面要麼使用Proxy類,要麼直接使用Stub所實現的相應服務函式。

系統服務中的Binder物件

在應用程式程式設計時,經常使用getSystemService(String serviceName)方法獲取一個系統服務,那麼,這些系統服務的Binder引用是如何傳遞給客戶端的呢?須知系統服務並不是通過startService()啟動的。

getSystemService()函式的實現是在ContextImpl類中,該函式所返回的Service比較多,具體可參照原始碼。這些Service一般都由ServiceManager管理。

ServiceManager管理的服務

ServiceManager是一個獨立程式,其作用如名稱所示,管理各種系統服務,管理的邏輯如下圖所示.

Binder機制

ServiceManager本身也是一個Service,Framework提供了一個系統函式,可以獲取該Service對應的Binder引用,那就是BinderInternal.getContextObject()。該靜態函式返回ServiceManager後,就可以通過ServiceManager提供的方法獲取其他系統Service的Binder引用。這種設計模式在日常生活中到處可見,ServiceManager就像是一個公司的總機,這個總機號碼是公開的,系統中任何程式都可以使用BinderInternal.getContextObject()獲取該總機的Binder物件,而當使用者想聯絡公司中的其他人(服務)時,則要經過總機再獲得分機號碼。這種設計的好處是系統中僅暴露一個全域性Binder引用,那就是ServiceManager,而其他系統服務則可以隱藏起來,從而有助於系統服務的擴充套件,以及呼叫系統服務的安全檢查。其他系統服務在啟動時,首先把自己的Binder物件傳遞給ServiceManager,即所謂的註冊(addService)。

下面從程式碼實現來看以上邏輯。可以檢視ContextImpl.getSystemService()中各種Service的具體獲取方法,比如INPUT_METHOD_SERVICE,程式碼如下:

} else if (INPUT_METHOD_SERVICE.equals(name)) {
    return InputMethodManager.getInstance(this);
複製程式碼

而InputMethodManager.getInstance(this)的關鍵程式碼如下:

synchronized (InputMethodManager.class) {
    if (sInstance == null) {
        IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
        IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
        sInstance = new InputMethodManager(service, Looper.getMainLooper());
    }
    return sInstance;
}
複製程式碼

即通過ServiceManager獲取InputMethod Service對應的Binder物件b,然後再將該Binder物件作為IInputMethodManager.Stub.asInterface()的引數,返回一個IInputMethodManager的統一介面。

ServiceManager.getService()的程式碼如下:

public static IBinder getService(String name) {
    try {
        IBinder service = sCache.get(name);
        if (service != null) {
            return service;
        } else {
            return getIServiceManager().getService(name);
        }
    } catch (RemoteException e) {
        Log.e(TAG, "error in getService", e);
    }
    return null;
}
複製程式碼

即首先從sCache快取中檢視是否有對應的Binder物件,有則返回,沒有則呼叫getIServiceManager().getService(name),第一個函式getIServiceManager()即用於返回系統中唯一的ServiceManager對應的Binder,其程式碼如下:

private static IServiceManager getIServiceManager() {
    if (sServiceManager != null) {
        return sServiceManager;
    }

    // Find the service manager
    sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
    return sServiceManager;
}
複製程式碼

以上程式碼中,BinderInternal.getContextObject()靜態函式即用於返回ServiceManager對應的全域性Binder物件,該函式不需要任何引數,因為它的作用是固定的。從這個角度來看,這個函式的命名似乎更應用明確一些,比如,可以命名為getServiceManager()。

其他所有通過ServiceManager獲取的系統服務的過程與以上基本類似,所不同的就是傳遞給ServiceManager的服務名稱不同,因為ServiceManager正是按照服務的名稱(String型別)來儲存不同的Binder物件的。

關於使用addService()向ServiceManager中新增一個服務一般是在SystemService程式啟動時完成的。

理解Manager

ServiceManager所管理的所有Service都是以相應的Manager返回給客戶端。因此,這裡簡述一下Framework中關於Manager的語義。在我們中國的企業裡,Manager一般指經理,比如專案經理,人事經理,部門經理。經理本身的含義比較模糊,其角色有些是給我們分配任務,比如專案經理;有些是給我們提供某種服務,比如人事經理;有些則是監督我們的工作等。而在Android中,Manager的含義更應該翻譯為經紀人,Manager所manager的物件是服務本身,因為每個具體的服務一般都會提供多個API介面,而Manager所manager的正是這些API。客戶端一般不能直接通過Binder引用去訪問具體的服務,而是要經過一個Manager,相應的Manager類對客戶端是可見。而遠端的服務類客戶端則是隱藏的。

而這些Manager的類內部都會有一個遠端服務Binder的變數,而且在一般情況下,這些Manager的建構函式引數中會包含這個Binder物件。簡單地講,即先通過ServiceManager獲取遠端服務的Binder引用,然後使用這個Binder引用構造一個客戶端本地可以訪問的經紀人,然後客戶端就可以通過該經紀人訪問遠端的服務。

這種設計的作用是遮蔽直接訪問遠端服務,從而可以給應用程式提供靈活的,可控的API介面,比如AMS.。系統不希望使用者直接去訪問AMS,而是經過ActivityManager類去訪問,而ActivityManager內部提供了一些更具可操作性的資料結構,比如RecentTaskInfo資料類封裝了最近訪問過的Task列表,MemoryInfo資料類封裝了和記憶體相關的資訊。

通過本地Manager訪問遠端服務的模型如下圖所示:

Binder機制


copy from -----> 《Android核心剖析》

相關文章