Binder機制-簡單用法(一)


Binder算是android里面比較難懂的部分了,但是非常重要,基本上,當我們深入到進程交互的階段,Binder都是一個繞不開的檻,所以我也希望幫助大家更淺顯地了解到這個知識點。筆者想通過3篇博文簡單介紹Binder,也僅僅是Java層,希望能夠幫助到想了解Binder基本知識的開發者。

為什么需要Binder?

在提及Binder之前,我們先看看我們平時開發的app的狀況。每個app就像孤島一樣,生活在系統分配給自己的虛擬機和內存空間,好處是安全,各個app不會互相影響到對方,IE一個網頁的崩潰卻會導致整個IE應用程序死亡(舉個小栗子,IE不屬於跨進程)。在這種情況下,必須有一種機制,提供安全高效的通信的功能,Binder就為此而生。所以,Binder是Android系統的一種IPC(進程間通信)方式。ActivityManagerService、WinderManagerService等系統服務的背后都是Binder。

為什么選擇Binder?

以前學過Linux的朋友都知道,Linux本身就提供了很多種跨進程通信的方式,如 
- Signals 信號量 
- Pipes 管道 
- Socket 套接字 
- Message Queue 消息隊列 
- Shared Memory 共享內存

Android繼承於Linux,想必更懂上面跨進程的機制(程序員又不傻,有現成好用的干嘛不復用呢),既然Android選擇了Binder,想必是考慮到手機這一種獨特的平台,需要適用一種更高效(手機內存省着用),更安全(防止應用被攻擊、篡改)的煩方法。其實一種技術的運用,往往有更多的綜合考量。有可能是那個年代,沒有比這個技術更牛叉的了;有可能,是各家公司斗爭協調出來的結果。

Binder 結構圖

先通過兩張Binder通用圖介紹Binder是如何起作用的:

image

要運作Binder,需要4個角色通力合作: 
- 客戶端:獲取服務端在Binder驅動中對應的引用,然后調用它的transact方法即可向服務端發送消息。 
- 服務端:指Binder實現類所在的進程,該對象一旦創建,內部則會啟動一個隱藏線程,會接收客戶端發送的數據,然后執行Binder對象中的onTransact()函數。 
- Binder驅動:當服務端Binder對象被創建時,會在Binder驅動中創建一個mRemote對象。 
- Service Manager:作用相當於DNS,就想平時我們通過網址,然后DNS幫助我們找到對應的IP地址一樣,我們在Binder服務端創建的Binder,會注冊到Service Manager,同理,當客戶端需要該Binder的時候,也會去Service Manager查找。

所以以上4者的運作基本上是: 
1. 服務端創建對應Binder實例對象,然后開啟隱藏Binder線程,接收來自客戶端的請求,同時,將自身的Binder注冊到Service Manager,在Binder驅動創建mRemote對象。 
2. 客戶端想和服務端通信,通過Service Manager查找到服務端的Binder,然后Binder驅動將對應的mRemote對象返回 
3. 至此,整個通信連接建立完畢

image

在建立完畢通信之后,客戶端可以通過獲取到的mRemote對象發生消息給遠程服務端了,客戶端通過調用transact()方法,將要請求的內容發送到服務段,然后掛起自己當前的線程,等待回復,服務端收到數據后在自己的onTransact()方法進行處理,然后將對應的結果返回給客戶端,客戶端收到數據,重新拉起線程,至此進程間交互數據完畢。

Binder 實戰演練

下面通過實戰,展示Binder如何成功實現跨進程通信,功能很簡單,我們在服務端創建Binder,然后提供查詢游戲價格的功能,然后客戶端發起遠程調用進行查詢。

服務端代碼

自定義Binder,需要在Service里面提供服務,我們先創建Service,然后在里面創建Binder

public class GameService extends Service {     private Binder mBinder = new Binder() {     };      @Nullable     @Override     public IBinder onBind(Intent intent) {         return mBinder;     } }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意onBind()方法,這里return的是我們在Service創建的Binder對象,客戶端接受到的對象就是他。Binder對象里面,我們需要重載幾個方法,分別是onTransact(),getGamePrice()方法。 
- onTransact() 方法是客戶端發起調用后,服務端Binder所在進程接收到客戶端發送的數據,通過這個方法去處理,根據響應碼分發給具體的不同的方法去處理。 
- getGamePrice()方法 是自定義方法,用於接收客戶端傳送過來的游戲名去查詢對應的價格

    private Binder mBinder = new Binder() {         @Override         protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {             if (code == 1) {                 String _arg0;                 _arg0 = data.readString();                 int _result = getGamePrice(_arg0);                 reply.writeInt(_result);                 return true;             }             return super.onTransact(code, data, reply, flags);         }          public int getGamePrice(String name) {             int price = -1;             if ("逃生2".equals(name)) {                 price = 88;             } else if ("飢荒".equals(name)) {                 price = 24;             }             return price;         }     };
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

這里為了方便起見,code(響應碼)直接判斷是否為1,是的話就取第一個數據,然后調用getGamePrice()方法查詢價格,得到結果后通過reply封裝結果,然后return true,表面成功接收並處理了結果。后面客戶端會通過這個reply 去獲取結果.

當然,別忘記了,設置Service,筆者在開發的時候將服務端和客戶端寫在同一個應用里面,所以需要主動將其中一個所在的進程改掉,這里直接將Service放到另一個進程,然后添加對應的隱式action

        <service  android:name="com.smartwork.bindertest.GameService" android:process=":remote">             <intent-filter>                 <action android:name="android.intent.action.bind.gameservice" />             </intent-filter>         </service>
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

客戶端代碼

客戶端要做的只有兩步: 
1.綁定服務 
2.發起請求

我們先看第一步,綁定,這里我們在button的onClick()事件里面進行處理

        mButton.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View view) {                 String action = "android.intent.action.bind.gameservice";                 Intent intent = new Intent(action);                 intent.setPackage("com.smartwork.bindertest");                 bindService(intent, mServiceConnection, BIND_AUTO_CREATE);             }         });
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意 Android 5.0一出來后,其中有個特性就是校驗Servuce Intent,如果發現這個intent compponent==null 而且 package ==null 而且版本大於等於5.0 ,會拋出Service Intent must be explitict的異常,也就是說從Lollipop開始,service服務必須采用顯示方式啟動。

private void validateServiceIntent(Intent service) {         if (service.getComponent() == null && service.getPackage() == null) {             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {                 IllegalArgumentException ex = new IllegalArgumentException(                         "Service Intent must be explicit: " + service);                 throw ex;             } else {                 Log.w(TAG, "Implicit intents with startService are not safe: " + service                         + " " + Debug.getCallers(2, 3));             }         }     }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

繼續回到上面的方法,其中最重要的方法是bindService(),第二個參數是ServiceConnection對象,這個方法接收綁定成功和斷開的回調。

    private IBinder mRemote = null;     private ServiceConnection mServiceConnection = new ServiceConnection() {         @Override         public void onServiceConnected(ComponentName name, IBinder service) {             mRemote = service;             Toast.makeText(MainActivity.this, "綁定成功", Toast.LENGTH_SHORT).show();         }          @Override         public void onServiceDisconnected(ComponentName name) {             mRemote = null;             Toast.makeText(MainActivity.this, "遠程服務鏈接已斷", Toast.LENGTH_SHORT).show();         }     }; 
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

拿到這個service是屬於IBinder接口,也就是之前我們在服務端onBind()方法返回的Binder對象,之后我們就可以利用這個mRemote進行查詢操作。

簡單一點,直接在一個button里面寫onClick()查詢游戲價格:

        mPriceButton.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View view) {                 String gameName = "逃生2";                 int price = -1;                 try {                     price = getPrice(gameName);                 } catch (RemoteException e) {                     e.printStackTrace();                 }                 Toast.makeText(MainActivity.this, gameName + " price is : " + price, Toast.LENGTH_SHORT).show();             }         });
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

最近逃生2很火啊,查查多少錢來着。發送請求需要封裝一下數據,抽了個方法另外去實現。

    private int getPrice(String name) throws RemoteException {         android.os.Parcel _data = android.os.Parcel.obtain();         android.os.Parcel _reply = android.os.Parcel.obtain();         int _result;         try {             _data.writeString(name);             mRemote.transact(1, _data, _reply, 0);             _result = _reply.readInt();         } finally {             _reply.recycle();             _data.recycle();         }         return _result;     }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

mRemote.transact(1, _data, _reply, 0)這個語句最重要,傳遞的分別是響應碼,封裝好發過去的數據包(裝上我們要查詢的游戲名),需要接收的數據包,flags(flags:0:normal調用,同步方式,1:異步調用,發送數據結束后不等待調用結果返回)。當執行到這個方法之后,當前的線程會被掛起,然后跳到服務端所在的線程進行處理,得到結果后,當前線程才會重新被喚醒,然后_reply得到數據。

至此整個跨進程交互通過Binder得到了實現。

!!! 
需要強調的是,在客戶端遠程調用服務的時候,這個時候客戶端當前線程會被掛起,被調用的方法運行在服務端的Binder線程里面,如果該方法是耗時方法,而且當前線程屬於UI線程,很可能會出現ANR的問題,所以需要注意這個遠程調用,如果不明確,盡量避免在UI線程發起調用。另外onServiceConnected和onService也是運行在UI線程里面,需要注意避免耗時操作。

還需要注意一點,在Binder傳輸數據的時候,經歷過序列化和反序列化,簡單一點就是,就算你兩個傳遞過去給服務端都是同一個對象,服務端都會認為是兩個不同的對象,只是里面的數據一樣而已。這也很好理解,客戶端和服務端處於不同的進程,你發過去的對象必須經過序列化和反序列化,所以對於服務端,每次都感覺接收到新的對象。如果想解決這個問題: 
請看XXX

好的,上面就是Binder最簡單的用法了,基本上也能完成老大交代下來的工作了,但是辛(苦)勤(逼)的程序員是不會就這樣就滿足的。如果你不滿足上面簡單的實現,想深入了解怎樣可以優化整個流程或者深入到源碼理解具體的內部實現,請繼續。。。

首先你要知道,我騙了你。(不要打我,不要打我臉/(ㄒoㄒ)/~~)。其實上面的例子中,客戶端獲取到的並不是Binder實現類對象,而是Binder的一個代理。意思就是,在跨進程訪問中,獲取到的IBinder接口的對象,一般是實現類的代理類,並不是本體,相對的,如果是在同一個進程請求,那么獲取到的IBinder接口的對象,就是對應Binder的實現類。

image

基本上就是這樣,而這個獲取IBinder具體對象的邏輯發生在Native層,所以暫時不建議深追,先把java層的東西記住就好:其實也很好理解,當發現是同一個進程訪問的時候,系統會直接返回這個Binder實現類,這樣之后調用的方法就不用再經過跨進程IPC一系列的步驟,簡單的應用類方法調用就是了,當發現是跨進程訪問的時候,發給客戶端就是一個Binder的代理類;發給客戶端就是一個Binder的代理類;發給客戶端就是一個Binder的代理類。OK重要的事情說了3遍。

好的,下面我們來看看具體可以怎么優化, 
1. 在客戶端我們知道,每次發送過去最重要的數據就是狀態碼和Parcel的數據包,我們可以直接抽象出一個代理類,包裹這個IBinder接口,然后以調用類的方法的形式進行訪問, 
2. 然后將我們想要的查詢游戲價格的方法以接口的形式讓這個代理類去實現 
3. 每個方法對應一個響應碼,以常量形式出現

public interface GameInterface {     public static final int GET_PRICE_CODE = 1;   //響應碼      int getPrice(String name) throws RemoteException; }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
public class GameBinderProxy implements GameInterface {     private IBinder mRemote;      GameBinderProxy(IBinder binder) {         mRemote = binder;     }      @Override     public int getPrice(String name) throws RemoteException {         android.os.Parcel _data = android.os.Parcel.obtain();         android.os.Parcel _reply = android.os.Parcel.obtain();         int _result;         try {             _data.writeString(name);             mRemote.transact(GET_PRICE_CODE, _data, _reply, 0);             _result = _reply.readInt();         } finally {             _reply.recycle();             _data.recycle();         }         return _result;     } }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

這個構造方法還是在我們onServiceConnected()處調用:

        @Override         public void onServiceConnected(ComponentName name, IBinder service) {             mRemote = new GameBinderProxy(service);             Toast.makeText(MainActivity.this, "綁定成功", Toast.LENGTH_SHORT).show();         }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5

相對的服務端也進行優化:

public class GameBinderNative extends Binder implements GameInterface {     @Override     public int getPrice(String name) throws RemoteException {         int price = -1;         if ("逃生2".equals(name)) {             price = 88;         } else if ("飢荒".equals(name)) {             price = 24;         }         return price;     }      @Override     protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {         if (code == GET_PRICE_CODE) {             String _arg0;             _arg0 = data.readString();             int _result = getPrice(_arg0);             reply.writeInt(_result);             return true;         }         return super.onTransact(code, data, reply, flags);     } }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
public class GameService extends Service {     private Binder mBinder = new GameBinderNative();      @Nullable     @Override     public IBinder onBind(Intent intent) {         return mBinder;     } }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

OK ,這就很舒服,整個代碼塊干凈了不少,其實優秀到七七八八了,要是硬要挑一下骨頭的話,我們可以對客戶端這個代理類再動動手腳,如果是跨進程訪問的,就用代理包裝一下BinderProxy,如果是同一個進程訪問直接返回就可以了:

    //這里返回的是GameInterface接口     public static GameInterface asInterface(android.os.IBinder obj) {         if (obj == null) {             return null;         }         if (obj instanceof Binder) {             Log.d("chenjiahui", "asInterface: GameBinderNative : obj instanceof Binder");             return (GameInterface) obj;         } else {             Log.d("chenjiahui", "asInterface: GameBinderNative : obj instanceof GameBinderProxy");             return new GameBinderProxy(obj);         }     }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

客戶端

        public void onServiceConnected(ComponentName name, IBinder service) {             mRemote = GameBinderNative.asInterface(service);             Toast.makeText(MainActivity.this, "綁定成功", Toast.LENGTH_SHORT).show();         }
    
    
    
            
  • 1
  • 2
  • 3
  • 4

重要的事情說三篇,onServiceConnected回調返回的IBinder service對象,==如果是跨進程訪問的返回的是BinderProxy(Binder的代理類),同一進程訪問返回的是Binder;如果是跨進程訪問的返回的是BinderProxy(Binder的代理類),同一進程訪問返回的是Binder;如果是跨進程訪問的返回的是BinderProxy(Binder的代理類),同一進程訪問返回的是Binder。==

OK,到這里,整篇文章差不多要結束了,因為我剛才教你的優化方法,就是AIDL。

AIDL是一個縮寫,全稱是Android Interface Definition Language,也就是Android接口定義語言。

這個是官方的說辭,以我的理解就是,幫助開發者更有效率地去實現這個Binder,因為用了AIDL之后生成了專門跨進程通信的模板,免除了一大堆統一的代碼,用了AIDL之后,在服務端,關注具體跨進程方法的實現就可以,客戶端更是不用寫任何額外的代碼,因為全部自動生成了。

AIDL 實操

我們試試打開Android Studio,然后新建一個aidl文件 
image

interface IGameService {     /** * 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);      int getPrice(String name); }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

好的,還是我們查詢游戲價格的方法getPrice(),上面還有一個方法basicTypes(),沒啥用,看google注釋就是告訴我們aidl可以傳的基礎類型有哪些,當然還可以傳parcelable的對象,這個放到第3篇文章講吧(因為別的文章都說了很多了,看兩三篇博文一下子就懂了)

寫完這個IGameService接口后,我們clean 一下項目,打開目錄看看有什么東西: 
app/build/generated/source/aidl/debug or release / 包名 / IGameService

image

我把IGameService的代碼抽出來先看個大概:

public interface IGameService extends android.os.IInterface {      public static abstract class Stub extends android.os.Binder implements com.smartwork.bindertest.IGameService {         public Stub()         {             this.attachInterface(this, DESCRIPTOR);         }          public static com.smartwork.bindertest.IGameService asInterface(android.os.IBinder obj)         {             if ((obj==null)) {                 return null;             }             android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);             if (((iin!=null)&&(iin instanceof com.smartwork.bindertest.IGameService))) {                 return ((com.smartwork.bindertest.IGameService)iin);             }             return new com.smartwork.bindertest.IGameService.Stub.Proxy(obj);         }          @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException         {             switch (code)             {                 case TRANSACTION_getPrice:                 {                     //省略了代碼,就是我們之前寫的那個!!!                 }             }             return super.onTransact(code, data, reply, flags);         }         private static class Proxy implements com.smartwork.bindertest.IGameService {             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 int getPrice(java.lang.String name) throws android.os.RemoteException             {                 //省略了代碼,就是我們之前寫的那個!!!             }         }         static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);         static final int TRANSACTION_getPrice = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);     }      public int getPrice(java.lang.String name) throws android.os.RemoteException; }
    
    
    
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 先簡單說下三者的結構,是一個interface接口,里面有一個抽象類Stub(這個Stub是交給服務端去具體實現的),然后抽象類Stub里面有一個內部類Proxy(聽名字就知道是給Stub做代理的)
  • 這3者對應之前我們優化的過程,對比着看,其實是一樣的,只是google將他們三個放在一起了,剛看這個生產的java感覺特別亂,但是你跟之前的優化過程對比一看就會立馬清晰很多了。
  • 當然了,生成的這個java和我們之前優化的有點不同,就是多了android.os.IInterface這個接口,所以Stub里面的asInterface方法(也就是我之前說的那個,同一個進程Binder直接返回,不然就用代理類封裝的方法)也有點不一樣。
  • asInterface是一個static方法,提供客戶端調用,然后通過queryLocalInterface()這個方法判斷是否同一個進程,因為假如是服務端所在的進程請求,獲得的是Binder實現類,初始化的時候Stub()會調用attachInterface(),其實就是自己把自己存起來了,后面如果queryLocalInterface()的話就能返回到對象。但是跨進程訪問,返回的是BinderProxy,這個時候queryLocalInterface()只能是null了。
    public IInterface queryLocalInterface(String descriptor) {         return null;     }
    
    
    
            
  • 1
  • 2
  • 3
版權聲明:本文為博主原創文章,未經博主允許不得轉載。技術交流可郵:cjh94520@outlook.com https://blog.csdn.net/cjh94520/article/details/71374872


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM