概述
想當初在第一次拜讀《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的通訊模型;
- 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?{
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許可權驗證
預設情況下遠端服務任何人都可以連線,許可權驗證也就是阻攔那些不想讓他連線的人,驗證的地方有兩處:
- 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){
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>
複製程式碼