Android進階知識樹——Android 多程式、Binder 你必須知道的一切

Alex@W發表於2019-01-28

概述

想當初在第一次拜讀《Android藝術開發探索》時,深感真的是一本很“藝術”的書(因為當初菜的看不懂..),隨著自己的成長和多次閱讀,從開始的完全不懂到現在的有所理解、使用和總結,才體會到其中探索的奧妙,現在跟著安卓高階開發的學習路線,進一步學習、總結和梳理知識。

多程式作為Android開發者邁向高階開發者的第一關,也使許多初級開發者望而卻步,這也是每個開發者必經階段,正好筆者在公司的開發專案中也一直使用了多程式,之前只是在使用階段、和平時零散的知識點,而對Binder的原理和理解並不深入,本文結合最近所看的文章和實際使用,從開發者的角度總結多程式和Binder的使用,即為自己梳理知識也希望幫助有需要的人。

在這裡插入圖片描述

  • 定義 提到多程式就會想到多執行緒,這也是很多初級的面試問題,二者對比著可能更好理解:
  1. 執行緒:執行緒是CPU最小的排程單元,是有限的系統資源,也是處理任務的地方
  2. 程式:是一個執行單元,一般指裝置上的一個程式或一個應用
  3. 理解:程式和執行緒是包含和被包含的關係;一個程式可以包含多個執行緒
  • 開啟方式 Android開啟多程式只有一個方式:註冊清單檔案中,在Android四大元件中指定process屬性,命名方式如下:
  1. 以“:”命名方式:最終的程式名為在當前的命名前面新增預設的包名
  2. 完整命名方式:最終的程式名就為設定的名稱
android:process=":consume"
android:process="com.alex.kotlin.myapplication.consume"
複製程式碼
  • 多程式問題 因為程式開啟時Application都會重新建立,所以很多資料和物件都會產生副本,因此在多程式模式下資料共享就會變得不穩定,多程式模式下會造車如下的問題:
  1. 靜態成員和單例模式完全失效
  2. 執行緒同步機制完全失效
  3. SharePreference可靠性下降
  4. Application會多次建立

程式間通訊

關於程式間的通訊首先想到的是Binder機制,當然開發中如果使用多程式,那Binder自當是首當其衝要了解和學習的,下文也會重點介紹Binder,在此之前來看看我們實際開發中使用的、或者可以跨程式通訊的機制

  • 序列化
  1. Serializable Serializable序列的使用很簡單,只需要實現在Java類中實現Serializable介面,設定serialVersionUID即可
public class Book implements Serializable {
    private static final long serialVersionUID = 871136882801008L;
    String name;
    int age;

    public Book(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
複製程式碼

在儲存資料時只需將物件序列化在磁碟中,在需要使用的地方反序列化即可獲取Java例項,使用過程如下:

//序列化
val book = Book("Android",20)
        val file = File(cacheDir,"f.txt")
        val out = ObjectOutputStream(FileOutputStream(file))
        out.writeObject(book)
        out.close()
//反序列化
val file = File(cacheDir,"f.txt")
        val input = ObjectInputStream(FileInputStream(file))
        val book: Book = input.readObject() as Book
        input.close()
複製程式碼

針對上面的serialVersionUID可能有的認為不設定也可以使用,但如果不設定serialVersionUID值,Java物件同樣可以序列化,但是當Java類改變時,這時如果去反序列化的化就會報錯,因為serialVersionUID是輔助序列化和反序列化的,只有兩者的serialVersionUID一致才可實現反序列化,所以你不指定serialVersionUID時,系統會預設使用當前類的Hash值,當java物件改變時其Hash值也改變了,所以反序列化時就找不到對應的Java類了。

  1. Parcelable Parcelable也是一個介面,他是Android提供的在記憶體中更高效的序列化方式,使用方法是實現介面,重寫其中方法即可,當然也可使用外掛自動生成。

對於Parcelable和Serializable的選擇使用:Serializable是Java的序列化介面,使用時開銷大,需要大量的IO操作,Parcelable是Android提供的序列化介面,適合Android效率更高,對於兩者的選擇,如果只是在記憶體上序列化使用Parcelable,如果需要在磁碟上序列化使用Serializable即可。

Binder

在網上看了需對關於Binder的文章,有的深入Binder原始碼和底層去分析Binder的原始碼和實現,當然這裡面的程式碼我是看不懂,本文主要從Android開發的角度,對Binder的通訊的模型和方式做一個介紹,

  • Binder模型 Binder框架定義了四個角色:Server,Client,ServiceManager(簡稱SMgr)以及Binder驅動。其中Server,Client,SMgr執行於使用者空間,驅動執行於核心空間
  1. Server:服務的真正提供者,不過它會先向ServiceManager註冊自己Binder表明自己可以提供服務,驅動會為這個BInder建立位於核心中的實體和ServiceManager中的引用,並將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查詢表
  2. Client:服務的需求者和使用者,它向ServiceManager申請需要的服務;ServiceManager將表中的引用返回Client,Client拿到服務後即可呼叫服務中的方法;
  3. ServiceManager:Binder實體和引用的中轉站,儲存並分發Binder的引用;
  4. Binder驅動:Binder驅動默默無聞付出,卻是通訊的核心,驅動負責程式之間Binder通訊的建立,Binder在程式之間的傳遞,Binder引用計數管理,資料包在程式之間的傳遞和互動等一系列底層支援,借用網上的一張圖片展示Binder的通訊模型;

在這裡插入圖片描述
如果上面的四個功能難以理解,我們以打電話為例,將整個電話系統的程式比做Binder驅動,通訊錄比作ServiceManager,你本人為Client,現在你要打電話給叫Server人求助:

  1. Server:Server表示你要打電話找的人,它會首先給你留一個手機號,你為了可以找到他,將號碼儲存到通訊錄中,通訊錄相當於ServiceManager(Server向ServiceManager註冊服務)
  2. client:相當於你本人,發起打電話請求
  3. ServiceManager:通訊錄儲存電話號碼,你需要的時候首先向通訊錄去查詢號碼,它會返回Server手機號
  4. Binder驅動:打電話的系統,根據你輸入的號碼呼叫對應的人 對於Binder的通訊模型如上述所述,簡單的說就是Server先註冊並登記表示可以提供服務功能,當有需求時向登記處查詢可以提供服務的Service,登記處會給你詳細的地址,然後你就可以和服務商之間合作,只是整個過程在Binder驅動作用下完成;
  • Binder代理機制 通過上面的Binder通訊機制的理解,相信已經瞭解Binder是如何跨程式通訊的,可是具體的資料和物件都存在不同的程式中,那麼程式間是如何相互獲取的呢?比如A程式要獲取B程式中的物件,它是如何實現的呢?此時就需要Binder的代理機制;

當Binder收到A程式的請求後,Binder驅動並不會真的把 object 返回給 A,而是返回了一個跟 object 看起來一模一樣的代理物件 objectProxy,這個 objectProxy 具有和 object 一摸一樣的方法,但是這些方法並沒有 B 程式中 object 物件那些方法的能力,這些方法只需要把把請求引數交給驅動即可;

而對於程式A卻傻傻不知道它以為拿到了B 程式中 object 物件,所以直接呼叫了Object的方法,當 Binder 驅動接收到 A 程式的訊息後,發現這是個 objectProxy 就去查詢自己維護的表單,一查發現這是 B 程式 object 的代理物件。於是就會去通知 B 程式呼叫 object 的方法,並要求 B 程式把返回結果發給自己。當驅動拿到 B 程式的返回結果後就會轉發給 A 程式,一次通訊就完成了,所以中間的代理就只是一個面具和傳輸的媒介。

在這裡插入圖片描述

  • Binder使用 Messenger 一種輕量級的IPC方案,它的底層實現是AIDL,Messenger通過對AIDL的封裝是我們可以更簡單的使用程式通訊,它的建構函式如下:
public Messenger(Handler target) {
        mTarget = target.getIMessenger();
    }

public Messenger(IBinder target) {
        mTarget = IMessenger.Stub.asInterface(target);
    }
複製程式碼

實現一個Messenger分為兩步,即服務端和客戶端的實現

  1. 服務端 首先建立一個Service來連線客戶端的請求,在Service中建立Handler例項,並使用此Handler的例項建立一個Messenger例項,並在Service的onBind()中返回Messenger例項。
//建立Handler
class HandlerService : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MSG_WHAT -> {
                Log.e("MyService", "MyServer")
            }
            else -> super.handleMessage(msg)
        }
    }
}
//使用Handler例項建立Messenger例項
private val messenger = Messenger(HandlerService())

//服務通過 onBind() 使其返回客戶端
override fun onBind(intent: Intent): IBinder {
    return messenger.binder
}
複製程式碼
  1. 客戶端 客戶端在繫結Service後,會在onServiceConnected中獲取IBinder的例項,客戶端使用此例項建立Messenger例項,這個Messenger就可以和服務端進行通訊了,傳送Message資訊服務端就會收到;
private var messenger: Messenger? = null

private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(p0: ComponentName?, iBinder: IBinder?) {
        messenger = Messenger(iBinder)  // 繫結service後初始化 Messenger 
    }
    override fun onServiceDisconnected(p0: ComponentName?) {
        messenger = null
    }
}

var message = Message.obtain(null, MSG_WHAT, 100,0) // 建立Message
messenger?.send(message)  // 傳送Message

//輸出結果
07-24 14:00:38.604 18962-18962/com.example.administrator.memory E/MyService:MyServer 100
複製程式碼

若服務端想回應客戶端,那客戶端就要像服務端一樣建立一個接受資訊的Handler和Messenger例項,在傳送Message時使用msg.replyTo將Messenger例項傳送給服務端,服務端就可以使用此例項迴應客戶端資訊;

//客戶端傳送Messenger到Service
msg.replyTo = mGetReplyMessenger;

// 在Service端接收客戶端的Messenger
Messenger msg = msg.replyTo;
複製程式碼

AIDL 對於程式通訊來說,可能實際在專案中使用的可能更多的還是AIDL,所以作為本文的最後也是重點講解,並結合實際的程式碼分析多程式的使用,Aidl支援的資料型別:

  1. 基本資料型別
  2. String和CharSequence
  3. List:只支援ArrayList
  4. Map:只支援HashMap
  5. Parcelable:所有實現Parcelable介面的例項
  6. AIDL:所有宣告的AIDL檔案

AIDL的使用分為三步:AIDL介面建立、服務端、客戶端實現,下面實際程式碼分析,我們做一個簡單的Demo,在主程式中輸入賬戶密碼,然後在服務程式中驗證登陸,並將結果返回撥用程式;

  • AIDL介面建立 建立登陸ILoginBinder的AIDL介面檔案,並宣告登陸方法:
import com.alex.kotlin.myapplication.User;
interface ILoginBinder {
 void login(String name ,String pasd);
 boolean isLogin();
 User getUser();
}
複製程式碼

上面的Aidl檔案中使用了User類,所以在Java程式碼中建立User類,但初次之外也要建立User.aidl檔案且包名要和Java中的一樣,並在ILoginBinder中匯入User檔案的包;

package com.alex.kotlin.myapplication;
parcelable User ;
複製程式碼

此時點選MakePeoject系統會自動編譯出AIDL檔案對應的java程式碼 ILoginBinder類,可以在build包相應的路徑下可以檢視此類,程式碼結構如下:

public interface ILoginBinder extends android.os.IInterface{
.....
public static abstract class Stub extends android.os.Binder implements com.alex.kotlin.myapplication.binder.ILoginBinder{
......
private static class Proxy implements com.alex.kotlin.myapplication.binder.ILoginBinder{
.....
}
......
}
複製程式碼
  1. ILoginBinder:繼承android.os.IInterface的介面,並宣告瞭AIDL檔案中的方法
  2. Stub:編譯AIdl檔案後自動生成的檔案,繼承Binder並實現ILoginBinder介面,Stub是一個抽象類,所以它的子類要實現AIDL檔案中的方法;Stub中有個重要的方法asInterface(android.os.IBinder obj),它的傳入引數是Binder例項,根據判斷Binder是否為當前程式,若為當前執行緒返回BInder的例項,若為其他程式則返回Stub.Proxy(obj)的代理類
  3. Proxy:它是Stub中的一個內部類,也實現了ILoginBinder介面和所有方法,不過它的方法最後的執行還是交給傳入的mRemote中執行,而mRemote就是IBinder的例項,所以方法的最終呼叫還是在Stub的子類中
  • 服務端的實現 服務端的實現也分為兩步:
  1. 建立Stub類的子類並實現方法
class LoginBinder : ILoginBinder.Stub() {

    override fun login(name: String?, pasd: String?) {
    
     Log.e("======","name = $name ; pasd = $pasd")
            user = User(name)
    }

    override fun isLogin(): Boolean {
       return user != null
    }

    override fun getUser(): User? {
        return user
    }
    }
複製程式碼
  1. 建立Service端並在onBind方法中返回Stub的子類
class BinderMangerService : Service() {

    val binder = LoginBinder()
    
     override fun onBind(intent: Intent) : IBinder?{
     return  binder
    }
    
}
複製程式碼

設定Service的程式

 <service
                android:name=".binder.BinderMangerService"
                android:process=":service">
</service>
複製程式碼
  • 客戶端的實現 客戶端的實現和服務端一樣遵循者Service的使用方式,首先繫結Service服務在後的回撥中獲取IBinder例項,也是Stub的實現類的例項,客戶端拿到此類後呼叫Stub中的asInterface()方法獲取代理類,到此即可實現程式間的通訊
runOnThread {
       val intent = Intent(contextWrapper, BinderMangerService::class.java)
       contextWrapper.bindService(intent, serviceConnect,Context.BIND_AUTO_CREATE)
       binderSuccessCallback?.success()
}
......
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        iBinderManger = IBinderManger.Stub.asInterface(service)
}
複製程式碼

此時獲取到IBinderManger的代理類後即可呼叫方法,下面我們呼叫login()方法登陸,檢視輸出資訊:

2018-12-08 22:24:26.675 349-363/com.alex.kotlin.myapplication:service E/======: name = AAAAA ; pasd = 11111
複製程式碼
  • AIDL的斷開監聽

此時在service程式中收到了預設進成傳送的登陸資訊,即二者之間的通訊完成,但服務的連線會在某個時機因為某種原因時斷開,為了獲取斷開的時間或保持連線的穩定性Android提供了Binder連線的死亡監聽類IBinder.DeathRecipient,在繫結成功時給獲取的Ibinder繫結IBinder.DeathRecipient例項,在連線斷開時會收到死亡回撥,我們可以斷開連線後繼續重連,使用如下:

  //建立IBinder.DeathRecipient例項
  var deathRecipient : IBinder.DeathRecipient? = null
  deathRecipient = IBinder.DeathRecipient {
           //斷開連線
            iBinderManger?.asBinder()?.unlinkToDeath(deathRecipient,0)
            iBinderManger = null
            //重新連線
        }
        
 override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            iBinderManger = IBinderManger.Stub.asInterface(service)
            //設定死亡監聽
            service?.linkToDeath(deathRecipient,0)
            countDownLatch.countDown()
        }
複製程式碼
  • AIDLBinder介面回撥

但此時的通訊是單向的,如果想在登陸成功或失敗的時候通知預設程式,即程式間的回撥,以為二者處於不同程式間,所以普通的介面回撥不能滿足,此時的介面也必須是跨程式的AIDl介面,所以建立ILoginCallback檔案:

interface ILoginCallback {
  void  loginSuccess();
  void loginFailed();
}
複製程式碼

在ILoginBinder的檔案中新增註冊和解除監聽的方法:

void registerListener(ILoginCallback iLoginCallback);

 void unregisterListener(ILoginCallback iLoginCallback);
複製程式碼

在ILoginBinder的實現類中實現這兩個方法,這裡需要說明的是Android為多程式中的介面註冊問題提供了專門的類:RemoteCallbackList,所以在Stub的實現類中建立RemoteCallbackList,並在兩個方法中新增和刪除ILoginCallback的例項

private val remoteCallbackList = RemoteCallbackList<ILoginCallback>()

    override fun registerListener(iLoginCallback: ILoginCallback?) {
         remoteCallbackList.register(iLoginCallback)
    }

    override fun unregisterListener(iLoginCallback: ILoginCallback?) {
         remoteCallbackList.unregister(iLoginCallback)
    }
複製程式碼

對於RemoteCallbackList的遍歷也有所不同,必須beginBroadcast()和finishBroadcast()的成對使用,下面在登陸成功或失敗後回撥介面:

f (name != null && pasd != null){
            user = User(name)
            val number = remoteCallbackList.beginBroadcast()
            for (i in 0 until number){
                remoteCallbackList.getBroadcastItem(i).loginSuccess()
            }
            remoteCallbackList.finishBroadcast()
        }else{
            val number = remoteCallbackList.beginBroadcast()
            for (i in 0 until number){
                remoteCallbackList.getBroadcastItem(i).loginFailed()
            }
            remoteCallbackList.finishBroadcast()
}
複製程式碼

在LoginActivity中建立ILoginCallback.Stub的子類,並呼叫方法註冊介面,

 private val loginCallback = object : ILoginCallback.Stub(){
        override fun loginSuccess() {
            Log.e("======","登陸成功")
        }
        override fun loginFailed() {
        }
    }
  loginBinder?.registerListener(loginCallback)
複製程式碼

此時再次執行結果:

2018-12-08 22:46:48.366 792-810/com.alex.kotlin.myapplication:service E/======: name = AAAAA ; pasd = 11111
2018-12-08 22:46:48.367 747-747/com.alex.kotlin.myapplication:login E/======: 登陸成功
複製程式碼

到這裡程式間的相互通訊已經完成了,現在可以在二者之間實現資料或邏輯的相互呼叫,是不是很happy,但是你可以呼叫別人也可以呼叫,那怎麼讓只有自己才能呼叫呢?那就用到最後的一點就是Binder的許可權驗證

  • Binder許可權驗證

預設情況下遠端服務任何人都可以連線,許可權驗證也就是阻攔那些不想讓他連線的人,驗證的地方有兩處:

  1. onBind()方法中
  2. 服務端的onTransact()

驗證的方式也有兩種:

  1. 自定義許可權驗證
  2. 包名驗證

下面分別使用兩者進行服務端的驗證,首先在清單檔案中新增自定義許可權,並預設宣告此許可權

<uses-permission android:name="com.alex.kotlin.myapplication.permissions.BINDER_SERVICE"/>

 <permission android:name="com.alex.kotlin.myapplication.permissions.BINDER_SERVICE"
    android:protectionLevel="normal"/>
複製程式碼

在onBind()中判斷此許可權,如果通過則返回Binder例項,否則返回null

override fun onBind(intent: Intent) : IBinder?{
        val check = checkCallingOrSelfPermission("com.alex.kotlin.myapplication.permissions.BINDER_SERVICE")
        if (check == PackageManager.PERMISSION_DENIED){
            return  null
        }
      return  binder
 }
複製程式碼

另一中就是在服務端的onTransact()中驗證許可權和包名,只有二者都通過返回true,否則返回false

override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {

           val check = checkCallingOrSelfPermission("com.alex.kotlin.myapplication.permissions.BINDER_SERVICE")
           if (check == PackageManager.PERMISSION_DENIED){
               return false
           }

          val packages = packageManager.getPackagesForUid(Binder.getCallingUid())
           if (packages != null && !packages.isEmpty()){
               val packageName = packages[0]
               if (!packageName.startsWith("com.alex")){
                   return false
               }
           }
           return super.onTransact(code, data, reply, flags)
}
複製程式碼
  • Binder連線池 上面過程只使用了一個Aidl檔案,那如果10個呢?不可能建立和繫結10個Service,所以此時就休要使用Binder連線池,在Binder連線池中提供查詢Binder功能,根據傳入引數的不同獲取響應Stub子類的例項,只需建立一個用於繫結和返回Binder連線池的Service即可,詳細使用見文末的Demo;

到此本文的所有內容都介紹完畢了,從安卓開發和使用來說已能滿足工作中的需求,文末附上一個Aidl的Demo,以商店購買商品為例,使用Binder連線池實現登陸、售貨員、商店、和消費者四個程式的通訊;

<activity android:name=".ConsumeActivity"
        android:process=":consume">
        </activity>
        <activity
                android:name=".LoginActivity"
                android:process=":login">
        </activity>
        <service
                android:name=".binder.BinderMangerService"
                android:process=":service">
        </service>
        <activity
                android:name=".ProfuctActivity"
                android:process=":product">
</activity>
複製程式碼

AIdlDemo地址

相關文章