Android進階知識樹——Android 多程序、Binder 你必須知道的一切
想當初在第一次拜讀《Android藝術開發探索》時,深感真的是一本很“藝術”的書(因為當初菜的看不懂..),隨著自己的成長和多次閱讀,從開始的完全不懂到現在的有所理解、使用和總結,才體會到其中探索的奧妙,現在跟著安卓高階開發的學習路線,進一步學習、總結和梳理知識。
多程序作為Android開發者邁向高階開發者的第一關,也使許多初級開發者望而卻步,這也是每個開發者必經階段,正好筆者在公司的開發專案中也一直使用了多程序,之前只是在使用階段、和平時零散的知識點,而對Binder的原理和理解並不深入,本文結合最近所看的文章和實際使用,從開發者的角度總結多程序和Binder的使用,即為自己梳理知識也希望幫助有需要的人。

- 定義 提到多程序就會想到多執行緒,這也是很多初級的面試問題,二者對比著可能更好理解:
- 執行緒:執行緒是CPU最小的排程單元,是有限的系統資源,也是處理任務的地方
- 程序:是一個執行單元,一般指裝置上的一個程式或一個應用
- 理解:程序和執行緒是包含和被包含的關係;一個程序可以包含多個執行緒
- 開啟方式 Android開啟多程序只有一個方式:註冊清單檔案中,在Android四大元件中指定process屬性,命名方式如下:
- 以“:”命名方式:最終的程序名為在當前的命名前面新增預設的包名
- 完整命名方式:最終的程序名就為設定的名稱
android:process=":consume" android:process="com.alex.kotlin.myapplication.consume" 複製程式碼
- 多程序問題 因為程序開啟時Application都會重新建立,所以很多資料和物件都會產生副本,因此在多程序模式下資料共享就會變得不穩定,多程序模式下會造車如下的問題:
- 靜態成員和單例模式完全失效
- 執行緒同步機制完全失效
- SharePreference可靠性下降
- Application會多次建立
程序間通訊
關於程序間的通訊首先想到的是Binder機制,當然開發中如果使用多程序,那Binder自當是首當其衝要了解和學習的,下文也會重點介紹Binder,在此之前來看看我們實際開發中使用的、或者可以跨程序通訊的機制
- 序列化
- 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類了。
- 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運行於使用者空間,驅動運行於核心空間
- Server:服務的真正提供者,不過它會先向ServiceManager註冊自己Binder表明自己可以提供服務,驅動會為這個BInder建立位於核心中的實體和ServiceManager中的引用,並將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查詢表
- Client:服務的需求者和使用者,它向ServiceManager申請需要的服務;ServiceManager將表中的引用返回Client,Client拿到服務後即可呼叫服務中的方法;
- ServiceManager:Binder實體和引用的中轉站,儲存並分發Binder的引用;
- Binder驅動:Binder驅動默默無聞付出,卻是通訊的核心,驅動負責程序之間Binder通訊的建立,Binder在程序之間的傳遞,Binder引用計數管理,資料包在程序之間的傳遞和互動等一系列底層支援,借用網上的一張圖片展示Binder的通訊模型;

如果上面的四個功能難以理解,我們以打電話為例,將整個電話系統的程式比做Binder驅動,通訊錄比作ServiceManager,你本人為Client,現在你要打電話給叫Server人求助:
- Server:Server表示你要打電話找的人,它會首先給你留一個手機號,你為了可以找到他,將號碼儲存到通訊錄中,通訊錄相當於ServiceManager(Server向ServiceManager註冊服務)
- client:相當於你本人,發起打電話請求
- ServiceManager:通訊錄儲存電話號碼,你需要的時候首先向通訊錄去查詢號碼,它會返回Server手機號
- 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分為兩步,即服務端和客戶端的實現
- 服務端 首先建立一個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 } 複製程式碼
- 客戶端 客戶端在繫結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支援的資料型別:
- 基本資料型別
- String和CharSequence
- List:只支援ArrayList
- Map:只支援HashMap
- Parcelable:所有實現Parcelable介面的例項
- 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{ ..... } ...... } 複製程式碼
- ILoginBinder:繼承android.os.IInterface的介面,並聲明瞭AIDL檔案中的方法
- Stub:編譯AIdl檔案後自動生成的檔案,繼承Binder並實現ILoginBinder介面,Stub是一個抽象類,所以它的子類要實現AIDL檔案中的方法;Stub中有個重要的方法asInterface(android.os.IBinder obj),它的傳入引數是Binder例項,根據判斷Binder是否為當前程序,若為當前執行緒返回BInder的例項,若為其他程序則返回Stub.Proxy(obj)的代理類
- Proxy:它是Stub中的一個內部類,也實現了ILoginBinder介面和所有方法,不過它的方法最後的執行還是交給傳入的mRemote中執行,而mRemote就是IBinder的例項,所以方法的最終呼叫還是在Stub的子類中
- 服務端的實現 服務端的實現也分為兩步:
- 建立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 } } 複製程式碼
- 建立Service端並在onBind方法中返回Stub的子類
class BinderMangerService : Service() { val binder = LoginBinder() override fun onBind(intent: Intent) : IBinder?{ returnbinder } } 複製程式碼
設定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 { voidloginSuccess(); 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許可權驗證
預設情況下遠端服務任何人都可以連線,許可權驗證也就是阻攔那些不想讓他連線的人,驗證的地方有兩處:
- onBind()方法中
- 服務端的onTransact()
驗證的方式也有兩種:
- 自定義許可權驗證
- 包名驗證
下面分別使用兩者進行服務端的驗證,首先在清單檔案中新增自定義許可權,並預設宣告此許可權
<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){ returnnull } returnbinder } 複製程式碼
另一中就是在服務端的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> 複製程式碼