Binder + AMS + AIDL大雜燴

偶爾皮一下的Raina發表於2018-05-11

參考文章:

Binder

強推部落格:

按照參考文章裡說的,我們最好先學習以下AIDL。

AIDL

參考文章:

文章思路:

  • 為什麼要設計這門語言?
  • 它有什麼語法?
  • 如何使用AIDL檔案完成跨程式通訊?

AIDL是什麼?

AIDL 是 Android 介面定義語言。

AIDL 設計的初衷?

用來解決跨程式通訊,其實跨程式通訊還可以使用BroadcastReceiverMessenger,但是BroadcastReceiver佔用的系統資源比較多,如果是頻繁的跨程式通訊是不可取的。Messenger進行跨程式通訊時只能同步,不能非同步。

AIDL 語法

  • 檔案型別:xx.aidl
  • 支援型別:
    • Java中的八種基本資料型別,包括 byte,short,int,long,float,double,boolean,char。
    • String 型別。 CharSequence型別。
    • List型別:List中的所有元素必須是AIDL支援的型別之一,或者是一個其他AIDL生成的介面,或者是定義的parcelable(下文關於這個會有詳解)。List可以使用泛型。
    • Map型別:Map中的所有元素必須是AIDL支援的型別之一,或者是一個其他AIDL生成的介面,或者是定義的parcelable。Map是不支援泛型的
  • 定向tag: 表示跨程式通訊中資料的流向。
    • in: 資料只能從客戶端流向服務端
    • out:資料只能從服務端流向客戶端
    • inout: 表示可以在服務端和客戶端之間雙向流通。in 為定向 tag 的話表現為服務端將會接收到一個那個物件的完整資料,但是客戶端的那個物件不會因為服務端對傳參的修改而發生變動;out 的話表現為服務端將會接收到那個物件的的空物件,但是在服務端對接收到的空物件有任何修改之後客戶端將會同步變動;inout 為定向 tag 的情況下,服務端將會接收到客戶端傳來物件的完整資訊,並且客戶端將會同步服務端對該物件的任何變動。
  • 兩種AIDL檔案:
    • 用來定義parcelable物件,以供其他AIDL檔案使用AIDL中非預設支援的資料類
    • 定義介面方法

下面是例子。

  1. 看一下總體的檔案佈局:
    aidl.png

上面的5個檔案就是我們編寫的檔案了。 2. 先編寫AIDL資料夾裡的檔案 直接右鍵,new 一個 AIDL檔案即可。

AIDL2.png

Book.aidl

aidl3.png

BookManager.aidl

aidl4.png

這裡要確保Book.java檔案和Book.aidl檔案在同一資料夾下,同時又能被編譯器找到,所以我們要在build.gradle中加入:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}
複製程式碼
  1. 編寫服務端程式碼
public class AIDLService extends Service {

    public final String TAG = this.getClass().getSimpleName();

    //包含Book物件的list
    private List<Book> mBooks = new ArrayList<>();

    //由AIDL檔案生成的BookManager
    private final BookManager.Stub mBookManager = new BookManager.Stub() {
        @Override
        public List<Book> getBooks() throws RemoteException {
            synchronized (this) {
                Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());
                if (mBooks != null) {
                    return mBooks;
                }
                return new ArrayList<>();
            }
        }


        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this) {
                if (mBooks == null) {
                    mBooks = new ArrayList<>();
                }
                if (book == null) {
                    Log.e(TAG, "Book is null in In");
                    book = new Book();
                }
                //嘗試修改book的引數,主要是為了觀察其到客戶端的反饋
                book.setPrice(2333);
                if (!mBooks.contains(book)) {
                    mBooks.add(book);
                }
                //列印mBooks列表,觀察客戶端傳過來的值
                Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Book book = new Book();
        book.setName("Android開發藝術探索");
        book.setPrice(28);
        mBooks.add(book);   
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
        return mBookManager;
    }
}
複製程式碼

在清單檔案中進行配置:

<service
    android:name=".service.AIDLService"
    android:exported="true">
        <intent-filter>
            <action android:name="com.lypeer.aidl"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
</service>
複製程式碼
  1. 回到java資料夾下,編寫我們的客戶端程式碼
public class AIDLActivity extends AppCompatActivity {

    //由AIDL檔案生成的Java類
    private BookManager mBookManager = null;

    //標誌當前與服務端連線狀況的布林值,false為未連線,true為連線中
    private boolean mBound = false;

    //包含Book物件的list
    private List<Book> mBooks;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidl);
    }

    /**
     * 按鈕的點選事件,點選之後呼叫服務端的addBookIn方法
     *
     * @param view
     */
    public void addBook(View view) {
        //如果與服務端的連線處於未連線狀態,則嘗試連線
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "當前與服務端處於未連線狀態,正在嘗試重連,請稍後再試", Toast.LENGTH_SHORT).show();
            return;
        }
        if (mBookManager == null) return;

        Book book = new Book();
        book.setName("APP研發錄In");
        book.setPrice(30);
        try {
            mBookManager.addBook(book);
            Log.e(getLocalClassName(), book.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 嘗試與服務端建立連線
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("com.lypeer.aidl");
        intent.setPackage("com.lypeer.ipcserver");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (!mBound) {
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(getLocalClassName(), "service connected");
            mBookManager = BookManager.Stub.asInterface(service);
            mBound = true;

            if (mBookManager != null) {
                try {
                    mBooks = mBookManager.getBooks();
                    Log.e(getLocalClassName(), mBooks.toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "service disconnected");
            mBound = false;
        }
    };
}
複製程式碼

之後就可以開始通訊啦。 如果發現有問題,檢查下面配置:

aidl6.png

AIDL 原理

其實在寫完AIDL檔案後,編譯器會幫我們自動生成一個同名的 .java 檔案。(圖是截圖參考文章的,寫的時候忘記截了~~~)

aidl5.png

  1. 從客戶端開始
public void onServiceConnected(ComponentName name, IBinder service) 
    mBookManager = BookManager.Stub.asInterface(service);
}
複製程式碼

這裡的傳遞給了我們一個service物件,經過Debug,發現它是AIDLService

aidl7.png

不深究它是怎麼來的。我們接著看。 我們看到這裡呼叫了asInterface方法,我們點選檢視一下原始碼:

public static com.lypeer.ipcclient.BookManager asInterface(android.os.IBinder obj) {
    //驗空
    if ((obj == null)) {
        return null;
    }
    //DESCRIPTOR = "com.lypeer.ipcclient.BookManager",搜尋本地是否已經
    //有可用的物件了,如果有就將其返回
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.lypeer.ipcclient.BookManager))) {
        return ((com.lypeer.ipcclient.BookManager) iin);
    }
    //如果本地沒有的話就新建一個返回
    return new com.lypeer.ipcclient.BookManager.Stub.Proxy(obj);
}
複製程式碼

返回一個proxy物件。這裡proxy物件就是客戶端跟服務端進行溝通的橋樑了。 2. proxy中方法做了什麼 我們接著看proxy方法中做了什麼:

@Override
public java.util.List<com.lypeer.ipcclient.Book> getBooks() throws android.os.RemoteException {
    //很容易可以分析出來,_data用來儲存流向服務端的資料流,
    //_reply用來儲存服務端流回客戶端的資料流
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    java.util.List<com.lypeer.ipcclient.Book> _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        //呼叫 transact() 方法將方法id和兩個 Parcel 容器傳過去
        mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
        _reply.readException();
        //從_reply中取出服務端執行方法的結果
        _result = _reply.createTypedArrayList(com.lypeer.ipcclient.Book.CREATOR);
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    //將結果返回
    return _result;
}
複製程式碼
  • transact方法:這是客戶端和服務端通訊的核心方法。呼叫這個方法之後,客戶端將會掛起當前執行緒,等候服務端執行完相關任務後通知並接收返回的 _reply 資料流。關於這個方法的傳參,這裡有兩點需要說明的地方:
    • 方法 ID :transact() 方法的第一個引數是一個方法 ID ,這個是客戶端與服務端約定好的給方法的編碼,彼此一一對應。在AIDL檔案轉化為 .java 檔案的時候,系統將會自動給AIDL檔案裡面的每一個方法自動分配一個方法 ID。
    • 第四個引數:transact() 方法的第四個引數是一個 int 值,它的作用是設定進行 IPC 的模式,為 0 表示資料可以雙向流通,即 _reply 流可以正常的攜帶資料回來,如果為 1 的話那麼資料將只能單向流通,從服務端回來的 _reply 流將不攜帶任何資料。 注:AIDL生成的 .java 檔案的這個引數均為 0。

通過一個方法可以總結一下proxy方法的工作流程:

  1. 生成_data和_reply資料流,將它傳給服務端

  2. 通過transact方法傳遞給服務端,並請求服務端呼叫指定方法

  3. 接收_reply資料流,並從中取出服務端返回的資料(服務端是如何返回_reply資料流回來的,這點是底層封裝好的,我們不必要知道)

  4. 我們把資料分成Pacel傳給服務端了,服務端幹了些啥? 前面說了客戶端通過呼叫 transact() 方法將資料和請求傳送過去,那麼理所當然的,服務端應當有一個方法來接收這些傳過來的東西:在 BookManager.java 裡面我們可以很輕易的找到一個叫做 onTransact() 的方法。我們看看這個方法是怎麼寫的:

@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_getBooks: {
            //省略
            return true;
        }
        case TRANSACTION_addBook: {
            //省略
            return true;
        }
    }
    return super.onTransact(code, data, reply, flags);
複製程式碼

可以看到,它在接收了客戶端的 transact() 方法傳過來的引數後,什麼廢話都沒說就直接進入了一個 switch 選擇:根據傳進來的方法 ID 不同執行不同的操作。接下來看一下每個方法裡面它具體做了些什麼,以 getBooks() 方法為例:

case TRANSACTION_getBooks: {
    data.enforceInterface(DESCRIPTOR);
    //呼叫 this.getBooks() 方法,在這裡開始執行具體的事務邏輯
    //result 列表為呼叫 getBooks() 方法的返回值
    java.util.List<com.lypeer.ipcclient.Book> _result = this.getBooks();
    reply.writeNoException();
    //將方法執行的結果寫入 reply ,
    reply.writeTypedList(_result);
    return true;
}
複製程式碼

很簡單,呼叫服務端的具體實現,然後獲取返回值寫入到reply流。這裡的this.getBooks()方法就是由子類去實現的,也就是我們的AIDLService中的bookManager

aidl8.png

好了,整個流程結束了,現在我們來總結一下:

  • 獲取客戶端傳遞過來的資料,根據方法id執行相應的操作
  • 將傳遞過來的資料取出來,如果需要返回資料給服務端,那就封裝成_data傳回。否則,執行相應操作。
  • 服務端接收到資料後,將需要回傳的資料寫入_reply流,傳回客戶端。

整個流程的序列圖是:

aidl9.png

這張圖,跟我們之前ActivityThread那張圖很像,因為本質上兩者都是採用了Binder進行通訊。

總結:

如果要使用Binder進行通訊,那麼你首先需要定義一個協議BookManager,然後你需要有兩個代理,一個是服務端代理,也是最終真正做事的類--AIDLService,還需要一個本地代理類--BookMangerProxy,它只負責封裝資料傳遞給服務端,然後服務端在onTransact中拿到資料進行處理即可。

Binder學習

1 使用者空間和核心空間

Android系統基於Linux核心,即Linux Kernel。Linux Kernel是作業系統的核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了給Kernel提供一定的保護機制,於是就把Kernel和上層的應用程式抽象的隔離開,分別稱之為核心空間(Kernel Space)和使用者空間(User Space)

2 Binder驅動定義

Android使用的Linux核心擁有著非常多的跨程式通訊機制,比如管道,System V,Socket等,但是出於安全和效能的考慮,採用了一種全新的通訊方式——Binder。Binder本身並不是Linux核心的一部分,但是通過Linux的動態可載入核心模組機制(LKM),Binder模組在執行時被連結到核心作為核心的一部分在核心空間執行。

模組是具有獨立功能的程式,它可以被單獨編譯,但是不能獨立執行。它在執行時被連結到核心作為核心的一部分在核心空間執行。

在Android系統中,這個執行在核心空間的,負責各個使用者程式通過Binder通訊的核心模組叫做Binder驅動;

總結:

  • Binder是執行在核心態的,單不屬於核心態。
  • 使用者程式之間進行通訊只能通過核心空間。

3 為什麼使用Binder?

  • 安全

傳統的程式通訊方式對於通訊雙方的身份並沒有做出嚴格的驗證,只有在上層協議上進行架設;比如Socket通訊ip地址是客戶端手動填入的,都可以進行偽造;而Binder機制從協議本身就支援對通訊雙方做身份校檢,因而大大提升了安全性。這個也是Android許可權模型的基礎。

  • 效能

在移動裝置上,廣泛地使用跨程式通訊肯定對通訊機制本身提出了嚴格的要求;Binder相對出傳統的Socket方式,更加高效

4 Binder通訊模型

比喻:打電話

SM : 通訊錄,記錄著你要找的人的電話號碼 Binder:基站

整個通訊流程如圖:

binder.png

5 Binder機制跨程式原理

上文Binder通訊圖給出了四個角色:Client,Server,SM,driver。但是我們仍然不清楚他們是怎麼幹活的? 看下圖:

binder2.png

畫畫太爛,看一下原來的圖吧。

binder3.png

整個流程簡述為:

  1. Server向SM註冊,告訴自己是誰,有什麼能力(IInterface介面),對應到場景中也就是告訴SM,自己叫"zhangsan",有一個物件object,可以add
  2. Client向SM查詢zhangsanobject
  3. 驅動在資料流過時,返回一個一模一樣的代理類
  4. Client呼叫代理類的add方法
  5. 代理類封裝方法引數,傳送給Binder驅動
  6. Binder驅動拿到這個引數,通知Server程式呼叫object物件的add方法,並返回結果
  7. Server返回結果,Binder返回結果給Client

一句話總結就是:

Client程式只不過是持有了Server端的代理;代理物件協助驅動完成了跨程式通訊。

總結四個角色的作用:

  • Client程式(簡稱Client):通訊的發起程式。
  • Server程式(簡稱Server):通訊的響應程式。
  • ServiceManager程式(簡稱SM): 所有的Server將自己的資訊註冊到SM,併為Client提供查詢的功能。
  • Binder驅動:將SM為Client查詢到的Server的目標資料轉換為proxy,再傳遞給Client

深入瞭解Java的Binder

IBinder/IInterface/Binder/BinderProxy/Stub是什麼?

  • IBinder:是一個介面,它代表了一種跨程式傳輸的能力
  • IInterface:代表的就是遠端server物件具有什麼能力。具體來說,就是aidl裡面的介面。
  • Java層的Binder類:代表的其實就是Binder本地物件。BinderProxy類是Binder類的一個內部類,它代表遠端程式的Binder物件的本地代理;這兩個類都繼承自IBinder, 因而都具有跨程式傳輸的能力;實際上,在跨越程式的時候,Binder驅動會自動完成這兩個物件的轉換。
  • Stub:繼承自Binder,說明他是Binder的本地物件;實現IInterface介面,說明具有遠端Server承諾給Client的能力;Stub是抽象類,說明具體的實現需要我們自己完成,這裡使用了策略模式。

知道了這一點後,我們可以分析AIDL中各個類的作用了。

先從AIDL資料夾,也就是服務端中檔案看起。

  1. ICompute.aidl
package com.example.test.app;
interface ICompute {
     int add(int a, int b);
}
複製程式碼

然後用編譯工具編譯之後,可以得到對應的ICompute.java類:

public interface ICompute extends android.os.IInterface 
複製程式碼

繼承自IInterface,說明ICompute是用來定義了Server端具有的能力。

  1. 接下來看的內部類Stub
 /**
         * Cast an IBinder object into an com.example.test.app.ICompute interface,
         * generating a proxy if needed.
         */
        public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {
                return ((com.example.test.app.ICompute) iin);
            }
            return new com.example.test.app.ICompute.Stub.Proxy(obj);
        }

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

複製程式碼

Stub類繼承自Binder,意味著這個Stub其實自己是一個Binder本地物件,然後實現了ICompute介面,ICompute本身是一個IInterface,因此他攜帶某種客戶端需要的能力(這裡是方法add)。

  1. 然後看看asInterface方法
/**
 * Cast an IBinder object into an com.example.test.app.ICompute interface,
 * generating a proxy if needed.
 */
public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {
        return ((com.example.test.app.ICompute) iin);
    }
    return new com.example.test.app.ICompute.Stub.Proxy(obj);
}
複製程式碼

我們在bind一個Service之後,在onServiceConnecttion的回撥裡面,就是通過這個方法拿到一個遠端的service的,這個方法做了什麼呢?

首先看函式的引數IBinder型別的obj,這個物件是驅動給我們的,如果是Binder本地物件,那麼它就是Binder型別,如果是Binder代理物件,那就是BinderProxy型別;然後,正如上面自動生成的文件所說,它會試著查詢Binder本地物件,如果找到,說明Client和Server都在同一個程式,這個引數直接就是本地物件,直接強制型別轉換然後返回,如果找不到,說明是遠端物件(處於另外一個程式)那麼就需要建立一個Binde代理物件,讓這個Binder代理實現對於遠端物件的訪問。一般來說,如果是與一個遠端Service物件進行通訊,那麼這裡返回的一定是一個Binder代理物件,這個IBinder引數的實際上是BinderProxy;

我們知道,對於遠端方法的呼叫,是通過Binder代理完成的,在這個例子裡面就是Proxy類;Proxy對於add方法的實現如下:

Override
public int add(int a, int b) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    int _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeInt(a);
        _data.writeInt(b);
        mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
        _reply.readException();
        _result = _reply.readInt();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}
複製程式碼

它首先用Parcel把資料序列化了,然後呼叫了transact方法.

transact方法的作用:將Client執行緒掛起等待返回;驅動完成操作後呼叫Serer的onTransact方法,這個方法被結果返回給驅動,驅動之後喚醒掛起的Client並將結果返回。

整個過程結束。

6 Binder本地物件和Binder代理物件區別

  • Binder本地物件(Stub):實現IInterface,繼承Binder(is a Binder)
  • Binder代理物件(Proxy):實現IInterace,並持有IBinder引用(has a Binder)

比喻來說就是一個是真正的皇帝,一個是挾天子以令諸侯。

7.結合Activity的啟動流程看Binder

Activity的啟動流程中涉及以下類:

  • Launcher
  • Activity
  • Instrumentation
  • ActivityManagerNative
  • ActivityManagerProxy
  • ActivityManagerService
  • ActivityStack
  • ApplicationThreadProxy
  • ApplicationThread
  • ActivityThread

再進行講解Activity的啟動過程之前,我們首先要知道各個類之間的關係,以及各個類的作用,另外還需要知道這些類是屬於Server端還是Client端的。我們知道,Android IPC 通訊採用的Binder,用到Binder就需要區分Server和Client.

類關係圖

Activity1.png

getService()方法舉例,說明呼叫順序,Activity採用的是遠端代理ActivityManagerProxyAMS在Client端的代理,同時,它也是個Binder物件,傳遞時將它傳遞給AMS,以後,AMS想對Activity做啥,就可以通過它。

代理模式2.png

總的來說,就是:

  1. ActivityManager首先通過遠端代理ActivityManagerNative得到本地代理ActivityManagerProxy
  2. ActivityManager要呼叫什麼方法,就通過本地代理ActivityManagerProxy的成員變數ActivityManagerNative去呼叫,其實就是將Binder物件ActivityManagerNative傳遞過去。

如果沒了解過Activity的啟動流程,請參考文章Activity啟動流程

分析Activity啟動流程中各個類對應的角色

  • IAcivityManger:是一個IInterface,代表遠端Service有什麼能力;
  • ActivityManagerNative:Binder本地物件(類似Stub
  • AtivityMangerService:真正的服務端, ActivityManagerNative的實現類
  • ActivityManagerProxy:Bidner代理物件
  • ActivityManager:不過是一個管理類而已,可以看到真正的操作都是轉發給ActivityManagerNative進而交給他的實現ActivityManagerService 完成的。

總結:

  • ----------------------------- 程式間通訊步驟 -----------------------------

  • 1.Client 發起遠端呼叫請求 也就是RPC 到Binder。同時將自己掛起,掛起的原因是要等待RPC呼叫結束以後返回的結果

  • 2.Binder 收到RPC請求以後 把引數收集一下,呼叫transact方法,把RPC請求轉發給service端。

  • 3.service端 收到rpc請求以後 就去執行緒池裡 找一個空閒的執行緒去走service端的 onTransact方法 ,實際上也就是真正在執行service端的 方法了,等方法執行結束 就把結果 寫回到binder中。

  • 4.Binder 收到返回資料以後 就喚醒原來的Client 執行緒,返回結果。至此,一次程式間通訊 的過程就結束了

  • ----------------------------- 程式間通訊步驟 -----------------------------

相關文章