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

概述

想當初在第一次拜讀《Android藝術開發探索》時,深感真的是一本很「藝術」的書(由於當初菜的看不懂..),隨着本身的成長和屢次閱讀,從開始的徹底不懂到如今的有所理解、使用和總結,才體會到其中探索的奧妙,如今跟着安卓高級開發的學習路線,進一步學習、總結和梳理知識。java

多進程做爲Android開發者邁向高級開發者的第一關,也使許多初級開發者望而卻步,這也是每一個開發者必經階段,正好筆者在公司的開發項目中也一直使用了多進程,以前只是在使用階段、和平時零散的知識點,而對Binder的原理和理解並不深刻,本文結合最近所看的文章和實際使用,從開發者的角度總結多進程和Binder的使用,即爲本身梳理知識也但願幫助有須要的人。android

在這裏插入圖片描述

  • 定義 提到多進程就會想到多線程,這也是不少初級的面試問題,兩者對比着可能更好理解:
  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,在此以前來看看咱們實際開發中使用的、或者能夠跨進程通訊的機制git

  • 序列化
  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實例,使用過程以下:github

//序列化
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便可。bash

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 對象那些方法的能力,這些方法只須要把把請求參數交給驅動便可;app

而對於進程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分爲兩步,即服務端和客戶端的實現ide

  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地址

相關文章
相關標籤/搜索