1. 程式人生 > >Android 核心--Binder架構分析

Android 核心--Binder架構分析

一、Binder架構

  在Android中,Binder用於完成程序間通訊(IPC),即把多個程序關聯在一起。比如,普通應用程式可以呼叫音樂播放服務提供的播放、暫停、停止等功能。
Binder工作在Linux層面,屬於一個驅動,只是這個驅動不需要硬體,或者說其操作的硬體是基於一小段記憶體。從執行緒的角度來講,Binder驅動程式碼執行在核心態,客戶端程式呼叫Binder是通過系統呼叫完成的。

  Binder是一種架構,這種架構提供了服務端介面、Binder驅動、客戶端介面三個模組。
  服務端:一個Binder服務端實際上就是一個Binder類的物件,該物件一旦建立,內部就啟動一個隱藏執行緒。該執行緒接下來會接收Binder驅動傳送的訊息,收到訊息後,會執行到Binder物件中的onTransact()

函式,並按照該函式的引數執行不同的服務程式碼。因此,要實現一個Binder服務,就必須過載onTransact()方法。過載onTransact()函式的主要內容是把onTransact()函式的引數轉換為服務函式的引數,而onTransact()函式的引數來源是客戶端呼叫transact()函式時輸入的,因此,如果transact()有固定格式的輸入,那麼onTransact()就會有固定格式的輸出。
  Binder驅動:任意一個服務端Binder物件被建立時,同時會在Binder驅動中建立一個mRemote物件,該物件的型別也是Binder類。客戶端要訪問遠端服務時,都是通過mRemote物件。
  客戶端:客戶端要想訪問遠端服務,必須獲取遠端服務在Binder物件中對應的mRemote引用,至於如何獲取,下面將會介紹。獲得該mRemote物件後,就可以呼叫其transact()方法,而在Binder驅動中,mRemote物件也過載了transact()方法,過載的內容主要包括以下幾項。

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


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

二、Service端

設計Service端很簡單,從程式碼的角度來講,只要基於Binder類新建一個Servier類即可。以下以設計一個Service類為例。

複製程式碼

    class MyService 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 name) {
        }
        public void stop() {
        }
    }

複製程式碼

當要啟動該服務時,只需要初始化一個MyService物件即可。之後可以在DDMS中看到多了一個執行緒

  定義了服務類後,接下來需要過載onTrasact()方法,並從data變數中讀出客戶端傳遞的引數,比如start()方法所需要的name變數。然而,服務端如何知道這個引數在data變數中的位置?因此,這就需要呼叫者和服務者雙方有個約定。假定客戶端在傳入的包裹data中放入的第一個資料就是filePath變數,則onTransact()的程式碼可以如下所示:

複製程式碼

            switch (code) {
            case 0x100:
                data.enforceInterface("MyService");
                String name = data.readString();
                start(name);
                break;
            }

複製程式碼

code變數用於標識客戶端期望呼叫服務端的哪個函式,因此,雙方需要約定一組int值,不同的值代表不同的服務端函式,該值和客戶端的transact()函式中第一個引數code的值是一致的。這裡假定0x100是雙方約定要呼叫start()函式的值。
enforceInterface()是為了某種校驗,它與客戶端的writeInterfaceToken()對應,具體見下一小節。
readString()用於從包裹中取出一個字串。取出filePath變數後,就可以呼叫服務端的start()函數了。如果該IPC呼叫的客戶端期望返回一些結果,則可以在返回包裹reply中呼叫Parcel提供的相關函式寫入相應的結果。Parcel.writeXXX();

三、Binder客戶端設計

  要想使用服務端,首先要獲取服務端在Binder驅動中對應的mRemote變數的引用,獲取的方法後面將介紹。獲得該變數的引用後,就可以呼叫該變數的transact()方法。該方法的函式原型:
  public final boolean transact(int code, Parcel data, Parcel reply,int flags)
  其中data表示的是要傳遞給遠端Binder服務的包裹(Parcel),遠端服務函式所需要的引數必須放入這個包裹中。包裹中只能放入特定型別的變數,這些型別包括常用的原子型別,比如String、int、long等,要檢視包裹可以放入的全部資料型別,可以參照Parcel類。除了一般的原子變數外,Parcel還提供了一個writeParcel()方法,可以在包裹中包含一個小包裹。因此,要進行Binder遠端服務呼叫時,服務函式的引數要麼是一個原子類,要麼必須繼承於Parcel類,否則,是不能傳遞的。

複製程式碼

        IBinder mRemote = null;
        String name = "Livingstone";
        int code = 0x100;
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken("MyService");
        data.writeString(name);
        mRemote.transact(code, data, reply, 0);
        IBinder binder = reply.readStrongBinder();
        reply.recycle();
        data.recycle();

複製程式碼

  現在來分析以上程式碼。首先,包裹不是客戶端自己建立的,而是呼叫Parcel.obtain()申請的,這正如生活中的郵局一樣,使用者一般只能用郵局提供的信封(尤其是EMS)。其中data和reply變數都由客戶端提供,reply變數使用者服務端把返回的結果放入其中。
  writeInterfaceToken()方法標註遠端服務名稱,理論上講,這個名稱不是必需的,因為客戶端既然已經獲取指定遠端服務的Binder引用,那麼就不會呼叫到其他遠端服務。該名稱將作為Binder驅動確保客戶端的確想呼叫指定的服務端。
  writeString()方法用於向包裹中新增一個String變數。注意,包裹中新增的內容是有序的,這個順序必須是客戶端和服務端事先約定好的,在服務端的onTransact()方法中會按照約定的順序取出變數。

  接著呼叫transact()方法。呼叫該方法後,客戶端執行緒進入Binder驅動,Binder驅動就會掛起當前執行緒,並向遠端服務傳送一個訊息,訊息中包含了客戶端傳進來的包裹。服務端拿到包裹後,會對包裹進行拆解,然後執行指定的服務函式,執行完畢後,再把執行結果放入客戶端提供的reply包裹中。然後服務端向Binder驅動傳送一個notify的訊息,從而使得客戶端執行緒從Binder驅動程式碼區返回到客戶端程式碼區。
  transact()的最後一個引數的含義是執行IPC呼叫的模式,分為兩種:一種是雙向,用常量0表示,其含義是服務端執行完指定服務後會返回一定的資料;另一種是單向,用常量1表示,其含義是不返回任何資料。
  最後,客戶端就可以從reply中解析返回的資料了,同樣,返回包裹中包含的資料也必須是有序的,而且這個順序也必須是服務端和客戶端事先約定好的。

四、使用Service類

以上手工編寫Binder服務端和客戶端的過程存在兩個重要問題。
  第一,客戶端如何獲得服務端的Binder物件引用。
  第二,客戶端和服務端必須事先約定好兩件事情。
   服務端函式的引數在包裹中的順序。
   服務端不同函式的int型標識。
關於第一個問題,為什麼要用Binder。答案很簡單,即為了提供一個全域性服務,所謂的“全域性”,是指系統中的任何應用程式都可以訪問。很明顯,這是一個作業系統應該提供的最基本的功能之一,Android的工程師自然也是這麼認為的,因此,他們提供了一個更傻瓜的解決方法,那就是Service。它是Android應用程式四個基本程式片段(Component)之一,四個基本片段包括Activity、Service、Content Provier、Receiver。
無論是否使用Service類,都必須要解決以上兩個重要問題。

1、獲取Binder物件

事實上,對於有創造力的程式設計師來講,可以完全不使用Service類,而僅僅基於Binder類編寫服務程式,但只是一部分。具體來講,可以僅使用Binder類擴充套件系統服務,而對於客戶端服務則必須基於Service類來編寫。所謂的系統服務是指可以使用getSystemService()方法獲取的服務,所謂的客戶端服務是指應用程式提供的自定義服務。
那麼,Service類是如何解決本節開頭所提出的兩個重要問題的呢?
首先,AmS提供了startService()函式用於啟動客戶服務,而對於客戶端來講,可以使用以下兩個函式來和一個服務建立連線,其原型在android.app. ContextImpl類中。

public ComponentName startService(Intent intent);
該函式用於啟動intent指定的服務,而啟動後,客戶端暫時還沒有服務端的Binder引用,因此,暫時還不能呼叫任何服務功能。

複製程式碼

    @Override
    public boolean bindService(Intent service, ServiceConnection conn, int flags) {
        IServiceConnection sd;
        if (mPackageInfo != null) {
            sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(),
                    mMainThread.getHandler(), flags);
        } 
        // ......int res = ActivityManagerNative.getDefault().bindService(
                mMainThread.getApplicationThread(), getActivityToken(),
                service, service.resolveTypeIfNeeded(getContentResolver()),
                sd, flags);
    }

複製程式碼

該函式用於繫結一個服務,這就是第一個重要問題的關鍵所在。其中第二個引數是一個interface類,該interface的定義如以下程式碼所示:

public interface ServiceConnection {
  public void onServiceConnected(ComponentName name, IBinder service);
  public void onServiceDisconnected(ComponentName name);
}

  此interface中的onServiceConnected()方法的第二個變數Service。當客戶端請求AmS啟動某個Service後, 該Service如果正常啟動,那麼AmS就會遠端呼叫ActivityThread類中的ApplicationThread物件,呼叫的引數中會包含Service的Binder引用,然後在ApplicationThread中會回撥bindService中的conn介面。因此,在客戶端中,可以在onServiceConnected()方法中將其引數Service儲存為一個全域性變數,從而在客戶端的任何地方都可以隨時呼叫該遠端服務。這就解決了第一個重要問題,即客戶端如何獲取遠端服務的Binder引用。


下面檢視原始碼

複製程式碼

        public final void scheduleBindService(IBinder token, Intent intent, boolean rebind) {
            BindServiceData s = new BindServiceData();
            s.token = token;
            s.intent = intent;
            s.rebind = rebind;
            queueOrSendMessage(H.BIND_SERVICE, s);
        }
    // if the thread hasn't started yet, we don't have the handler, so just
    // save the messages until we're ready.
    private final void queueOrSendMessage(int what, Object obj) {
        queueOrSendMessage(what, obj, 0, 0);
    }
    private final void queueOrSendMessage(int what, Object obj, int arg1, int arg2) {
        synchronized (this) {
            // ...
            Message msg = Message.obtain();
            msg.what = what;
            msg.obj = obj;
            msg.arg1 = arg1;
            msg.arg2 = arg2;
            mH.sendMessage(msg); 
        }
    }

複製程式碼

2、AIDL工具物件使用

關於第二個問題,Android的SDK中提供了一個aidl工具,該工具可以把一個aidl檔案轉換為一個Java類檔案,在該Java類檔案中,同時過載了transact和onTransact()方法,統一了存入包裹和讀取包裹引數,從而使設計者可以把注意力放到服務程式碼本身上。aidl工具不是必需的,對於有經驗的程式設計師來講,手工編寫一個引數統一的包裹存入和包裹讀出程式碼並不是一件複雜的事情。
下面是一個服務對應的AIDL檔案,服務中包含兩個服務函式,分別是start()和stop()。那麼,可以首先編寫一個IMyService.aidl檔案。如以下程式碼所示:

package com.androidstudy;
interface IMyService{
boolean start(String path);
void stop();
}

該檔案的名稱必須遵循一定的規範,第一個字母“I”不是必需的,但是,為了程式風格的統一,“I”的含義是IInterface類,即這是一個可以提供訪問遠端服務的類。服務的類名可以是任意的,但是,aidl工具會以該名稱命名輸出Java類。這些規則都只是Eclipse下ADT外掛的預設規則,aidl本身只是一個命令列程式,藉助命令列的話,則可以靈活指定輸出檔案的名稱及位置。

aidl檔案的語法基本類似於Java,package指定輸出後的Java檔案對應的包名。如果該檔案需要引用其他Java類,則可以使用import關鍵字,但需要注意的是,包裹內只能寫入以下三個型別的內容。

  •  Java原子型別,如int、long、String等變數。
  •  Binder引用。
  •  實現了Parcelable的物件。

因此,基本上來講,import所引用的Java類也只能是以上三個型別。
interface為關鍵字,有時會在interface前面加一個oneway,代表該service提供的方法都是沒有返回值的,即都是void型別。

複製程式碼

    public interface IMyService extends android.os.IInterface {
        /** Local-side IPC implementation stub class. */
        public static abstract class Stub extends android.os.Binder implements
                com.androidstudy.IMyService {
            private static final java.lang.String DESCRIPTOR = "com.androidstudy.IMyService";

            /** Construct the stub at attach it to the interface. */
            public Stub() {
                this.attachInterface(this, DESCRIPTOR);
            }

            /**
             * Cast an IBinder object into an com.androidstudy.IMyService
             * interface, generating a proxy if needed.
             */
            public static com.androidstudy.IMyService asInterface(
                    android.os.IBinder obj) {
                if ((obj == null)) {
                    return null;
                }
                android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
                if (((iin != null) && (iin instanceof com.androidstudy.IMyService))) {
                    return ((com.androidstudy.IMyService) iin);
                }
                return new com.androidstudy.IMyService.Stub.Proxy(obj);
            }

            @Override
            public android.os.IBinder asBinder() {
                return this;
            }

            @Override
            public boolean onTransact(int code, android.os.Parcel data,
                    android.os.Parcel reply, int flags)
                    throws android.os.RemoteException {
                switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_start: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    boolean _result = this.start(_arg0);
                    reply.writeNoException();
                    reply.writeInt(((_result) ? (1) : (0)));
                    return true;
                }
                case TRANSACTION_stop: {
                    data.enforceInterface(DESCRIPTOR);
                    this.stop();
                    reply.writeNoException();
                    return true;
                }
                }
                return super.onTransact(code, data, reply, flags);
            }

            private static class Proxy implements com.androidstudy.IMyService {
                private android.os.IBinder mRemote;

                Proxy(android.os.IBinder remote) {
                    mRemote = remote;
                }

                @Override
                public android.os.IBinder asBinder() {
                    return mRemote;
                }

                public java.lang.String getInterfaceDescriptor() {
                    return DESCRIPTOR;
                }

                @Override
                public boolean start(java.lang.String path)
                        throws android.os.RemoteException {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    boolean _result;
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        _data.writeString(path);
                        mRemote.transact(Stub.TRANSACTION_start, _data, _reply,
                                0);
                        _reply.readException();
                        _result = (0 != _reply.readInt());
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                    return _result;
                }

                @Override
                public void stop() throws android.os.RemoteException {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        mRemote.transact(Stub.TRANSACTION_stop, _data, _reply,
                                0);
                        _reply.readException();
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
            }

            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 path)
                throws android.os.RemoteException;

        public void stop() throws android.os.RemoteException;
    }

複製程式碼

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

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

  在Stub類中還定義了一些int常量,比如TRANSACTION_start,這些常量與服務函式對應, transact()和onTransact()方法的第一個引數code的值即來源於此。
  在Stub類中,除了以上所述的任務外,Stub還提供了一個asInterface()函式。提供這個函式的作用是這樣的:首先需要明確的是,aidl所產生的程式碼完全可以由應用程式設計師手工編寫,IMyService中的函式只是一種編碼習慣而已,asInterface即如此,提供這個函式的原因是服務端提供的服務除了其他程序可以使用外,在服務程序內部的其他類也可以使用該服務,對於後者,顯然是不需要經過IPC呼叫,而可以直接在程序內部呼叫的,而Binder內部有一個queryLocalInterface(String description)函式,該函式是根據輸入的字串判斷該Binder物件是一個本地的Binder引用。在第一張圖中曾經指出,當建立一個Binder物件時,服務端程序內部建立一個Binder物件,Binder驅動中也會建立一個Binder物件。如果從遠端獲取服務端的Binder,則只會返回Binder驅動中的Binder物件,而如果從服務端程序內部獲取Binder物件,則會獲取服務端本身的Binder物件。

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

五、系統服務中的Binder物件

  在應用程式中,經常使用getSystemService(String serviceName)方法獲取一個系統服務,那麼,這些系統服務的Binder引用是如何傳遞給客戶端的呢?須知系統服務並不是通過startService()啟動的。getSystemService()函式的實現是在ContextImpl類中,該函式所返回的Service比較多,具體可參照原始碼。這些Service一般都由ServiceManager管理。
1、ServiceManger管理的服務
ServiceManager是一個獨立程序,其作用如名稱所示,管理各種系統服務,管理的邏輯如下

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

        if (INPUT_METHOD_SERVICE.equals(name)) {
            return InputMethodManager.getInstance(this);

複製程式碼

    static public InputMethodManager getInstance(Looper mainLooper) {
        synchronized (mInstanceSync) {
            if (mInstance != null) {
                return mInstance;
            }
            IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
            IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
            mInstance = new InputMethodManager(service, mainLooper);
        }
        return mInstance;
    }

複製程式碼

  即通過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物件,該函式不需要任何引數,因為它的作用是固定的。其他所有通過ServiceManager獲取的系統服務的過程與以上基本類似,所不同的就是傳遞給ServiceManager的服務名稱不同,因為ServiceManager正是按照服務的名稱(String型別)來儲存不同的Binder物件的。
  使用addService()向ServiceManager中新增一個服務一般是在SystemService程序啟動時完成的。

2、理解Manger

  ServiceManager所管理的所有Service都是以相應的Manager返回給客戶端,因此,這裡簡述一下Framework中關於Manager的語義。在我們中國的企業裡,Manager一般指經理,比如專案經理、人事經理、部門經理。經理本身的含義比較模糊,其角色有些是給我們分配任務,比如專案經理;有些是給我們提供某種服務,比如人事經理;有些則是監督我們的工作等。而在Android中,Manager的含義更應該翻譯為經紀人,Manager所manage的物件是服務本身,因為每個具體的服務一般都會提供多個API介面 ,而Manager所manage的正是這些API。客戶端一般不能直接通過Binder引用去訪問具體的服務,而是要經過一個Manager,相應 的Manager類對客戶端是可見的,而遠端的服務類對客戶端則是隱藏的。而這些Manager的類內部都會有一個遠端服務Binder的變數,而且在一般情況下,這些Manager的建構函式引數中會包含這個Binder物件。簡單地講,即先通過ServiceManager獲取遠端服務的Binder引用,然後使用這個Binder引用構造一個客戶端本地可以訪問的經紀人,然後客戶端就可以通過該經紀人訪問遠端的服務。這種設計的作用是遮蔽直接訪問遠端服務,從而可以給應用程式提供靈活的、可控的API介面,比如AmS。系統不希望使用者直接去訪問AmS,而是經過ActivityManager類去訪問,而ActivityManager內部提供了一些更具可操作性的資料結構,比如RecentTaskInfo資料類封裝了最近訪問過的Task列表,MemoryInfo資料類封裝了和記憶體相關的資訊。

通過本地Manger訪問遠端服務的模型圖如下