Android即時通信系列文章(1)多進程:爲何要把消息服務拆分到一個獨立的進程?

「椎鋒陷陳」微信技術號現已開通,爲了得到第一手的技術文章推送,歡迎搜索關注!html

這是即時通信系列文章的第一篇,正式開始對IM開發技術的講解以前,咱們先來談談客戶端在完整聊天系統中所扮演的角色,爲此,咱們必須先明確客戶端的職責。java

現今主流的IM應用幾乎都是採用服務器中轉的方式來進行消息傳輸的,爲的是更好地支持離線、羣組等業務。在這種模式下,全部客戶端都需鏈接到服務端,服務端將不一樣客戶端發給本身的消息根據消息裏攜帶的用戶標識進行轉發或廣播。android

所以,做爲消息收發的終端設備,客戶端的重要職責之一就是保持與服務端的鏈接,該鏈接的穩定性直接決定消息收發的實時性和可靠性。而在上篇文章咱們講過,移動設備是資源受限的,這對鏈接的穩定性提出了極大的挑戰,具體可體如今如下兩個方面:緩存

  • 爲了維持多任務環境的正常運行,Android爲每一個應用的堆大小設置了硬性上限,不一樣設備的確切堆大小取決於設備的整體可用RAM大小,若是應用在達到堆容量上限後嘗試分配更多內容,則可能引起OOM。
  • 當用戶切換到其餘應用時,系統會將原有應用的進程保留在緩存中,稍後若是用戶返回該應用,系統就會重複使用該進程,以便加快應用切換速度。但當系統資源(如內存)不足時,系統會考慮終止佔用最多內存的、優先級較低的進程以釋放RAM。

雖然ART和Dalvik虛擬機會例行執行垃圾回收任務,但若是應用存在內存泄漏問題,而且只有一個主進程,勢必會隨着應用使用時間的延長而逐步增大內存使用量,從而增長引起OOM的機率和緩存進程被系統終止的風險。服務器

所以,爲了保證鏈接的穩定性,可考慮將負責鏈接保持工做的消息服務放入一個獨立的進程中,分離以後即便主進程退出、崩潰或者出現內存消耗太高等狀況,該服務仍可正常運行,甚至能夠在適當的時機經過廣播等方式從新喚起主進程。微信

可是,給應用劃分進程,每每就意味着須要編寫額外的進程通信代碼,特別是對於消息服務這種須要高度交互的場景。而因爲各個進程都運行在相對獨立的內存空間,於是是沒法直接通信的。爲此,Android提供了AIDL(Android Interface Definition Language,Android接口定義語言)用於實現進程間通訊,其本質就是實現對象的序列化、傳輸、接收和反序列化,獲得可操做的對象後再進行常規的方法調用。markdown

接下來,就讓咱們來一步步實現跨進程的通信吧。app

Step1 建立服務

因爲鏈接保持的工做是須要在後臺執行長時間執行的操做,一般不提供操做界面,符合這個特性的組件就是Service了,所以咱們選用Service做爲與遠程進程進行進程間通訊(IPC)的組件。建立Service的子類時,必須實現onBind回調方法,此處咱們暫時返回空實現。ide

class MessageAccessService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}
複製代碼

另外使用Service還有一個好處就是,咱們能夠在適當的時機將其升級爲前臺服務,前臺服務是用戶主動意識到的一種服務,進程優先級較高,所以在內存不足時,系統也不會考慮將其終止。oop

使用前臺服務惟一的缺點就是必須在抽屜式通知欄提供一條不可移除的通知,對於用戶體驗極不友好,可是咱們能夠經過定製通知樣式進行協調,後續的文章中會講到。

step2 指定進程

默認狀況下,同一應用的全部組件均在相同的進程中運行。如需控制某個組件所屬的進程,可經過在清單文件中設置android:process屬性實現:

<manifest ...>
    <application ...>
        <service
            android:name=".service.MessageAccessService"
            android:exported="true"
            android:process=":remote" />
    </application>
</manifest>
複製代碼

另外,爲使其餘進程的組件能調用服務或與之交互,還需設置android:exported屬性爲true。

step3 建立.aidl 文件

讓咱們從新把目光放回onBind回調方法,該方法要求返回IBinder對象,客戶端可以使用該對象定義好的接口與服務進行通訊。IBinder是遠程對象的基礎接口,該接口描述了與遠程對象交互的抽象協議,但不建議直接實現此接口,而應從Binder擴展。一般作法是是使用.aidl文件來描述所需的接口,使其生成適當的Binder子類。

那麼,這個最關鍵的.aidl文件該如何建立,又該定義哪些接口呢?

建立.aidl文件很簡單,Android Studio自己就提供了建立AIDL文件方法:項目右鍵 -> New -> AIDL -> AIDL File

前面講過,客戶端是消息收發的終端設備,而接入服務則是爲客戶端提供了消息收發的出入口。客戶端發出的消息經由接入服務發送到服務端,同時客戶端會委託接入服務幫忙收取消息,當服務端有消息推送過來時通知本身。

如此一來便很清晰了,咱們要定義的接口總共有三個,分別爲:

  • 發送消息
  • 註冊消息接收器
  • 反註冊消息接收器

MessageCarrier.aidl

package com.xxx.imsdk.comp.remote;
import com.xxx.imsdk.comp.remote.bean.Envelope;
import com.xxx.imsdk.comp.remote.listener.MessageReceiver;

interface MessageCarrier {
    void sendMessage(in Envelope envelope);
    void registerReceiveListener(MessageReceiver messageReceiver);
    void unregisterReceiveListener(MessageReceiver messageReceiver);
}
複製代碼

這裏解釋一下上述接口中攜帶的參數的含義:

Envelope ->

解釋這個參數以前,得先介紹Envelope.java這個類,該類是多進程通信中做爲數據傳輸的實體類。AIDL支持的數據類型除了基本數據類型、String和CharSequence,還有就是實現了Parcelable接口的對象,以及其中元素爲以上幾種的List和Map。

Envelope.java

**
 * 用於多進程通信的信封類
 * <p>
 * 在AIDL中傳遞的對象,須要在類文件相同路徑下,建立同名、可是後綴爲.aidl的文件,並在文件中使用parcelable關鍵字聲明這個類;
 * 但實際業務中須要傳遞的對象所屬的類每每分散在不一樣的模塊,因此經過構建一個包裝類來包含真正須要被傳遞的對象(必須也實現Parcelable接口)
 */
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
                    val noticeVo: NoticeVo? = null) : Parcelable {
}
複製代碼

另外,在AIDL中傳遞的對象,須要在上述類文件的相同包路徑下,建立同名、可是後綴爲.aidl的文件,並在文件中使用parcelable關鍵字聲明這個類,Envelope.aidl就是對應Envelope.java而建立的;

Envelope.aidl

package com.xxx.imsdk.comp.remote.bean;

parcelable Envelope;
複製代碼

兩個文件對應的路徑比較以下:

clipboard.png

那爲何是Envelope類而不直接是MessageVO類(消息視圖對象)呢?這是因爲考慮到實際業務中須要傳遞的對象所屬的類每每分散在不一樣的模塊(MessageVO從屬於另一個模塊,須要被其餘模塊引用),因此經過構建一個包裝類來包含真正須要被傳遞的對象(該對象必須也實現Parcelable接口),這也是該類命名爲Envelope(信封)的含義。

MessageReceiver ->

跨進程的消息收取回調接口,用於將消息接入服務收取到的服務端消息傳遞到客戶端。但這裏使用的回調接口有點不同,在AIDL中傳遞的接口,不能是普通的接口,只能是AIDL接口,所以咱們還須要新建多一個.aidl文件:

MessageReceiver.aidl

package com.xxx.imsdk.comp.remote.listener;
import com.xxx.imsdk.comp.remote.bean.Envelope;

interface MessageReceiver {
    void onMessageReceived(in Envelope envelope);
}
複製代碼

包目錄結構以下圖:

FE55B9D0FFFC48829667C01C212B2668.jpg

step4 返回IBinder接口

構建應用時,Android SDK會生成基於.aidl 文件的IBinder接口文件,並將其保存到項目的gen/目錄中。生成文件的名稱與.aidl 文件的名稱保持一致,區別在於其使用.java 擴展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)。此接口擁有一個名爲Stub的內部抽象類,用於擴展 Binder 類並實現 AIDL 接口中的方法。

/** 根據MessageCarrier.aidl文件自動生成的Binder對象,須要返回給客戶端 */
private val messageCarrier: IBinder = object : MessageCarrier.Stub() {

    override fun sendMessage(envelope: Envelope?) {

    }

    override fun registerReceiveListener(messageReceiver: MessageReceiver?) {
        remoteCallbackList.register(messageReceiver)
    }

    override fun unregisterReceiveListener(messageReceiver: MessageReceiver?) {
        remoteCallbackList.unregister(messageReceiver)
    }

}

override fun onBind(intent: Intent?): IBinder? {
    return messageCarrier
}
複製代碼
step5 綁定服務

組件(例如 Activity)能夠經過調用bindService方法綁定到服務,該方法必須提供ServiceConnection 的實現以監控與服務的鏈接。當組件與服務之間的鏈接創建成功後, ServiceConnection上的 onServiceConnected()方法將被回調,該方法包含上一步返回的IBinder對象,隨後即可使用該對象與綁定的服務進行通訊。

/**
 * ## 綁定消息接入服務
 * 同時調用bindService和startService, 可使unbind後Service仍保持運行
 * @param context   上下文
 */
@Synchronized
fun setupService(context: Context? = null) {
    if (!::appContext.isInitialized) {
        appContext = context!!.applicationContext
    }

    val intent = Intent(appContext, MessageAccessService::class.java)

    // 記錄綁定服務的結果,避免解綁服務時出錯
    if (!isBound) {
        isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    startService(intent)
}

/** 監聽與服務鏈接狀態的接口 */
private val serviceConnection = object : ServiceConnection {

    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        // 取得MessageCarrier.aidl對應的操做接口
        messageCarrier = MessageCarrier.Stub.asInterface(service)
        ...
    }

    override fun onServiceDisconnected(name: ComponentName?) {
    }

}
複製代碼

能夠同時將多個組件綁定到同一個服務,但當最後一個組件取消與服務的綁定時,系統會銷燬該服務。爲了使服務可以無限期運行,可同時調用startService()和bindService(),建立同時具備已啓動和已綁定兩種狀態的服務。這樣,即便全部組件均解綁服務,系統也不會銷燬該服務,直至調用 stopSelf() 或 stopService() 纔會顯式中止該服務。

/**
 * 啓動消息接入服務
 * @param intent    意圖
 * @param action    操做
 */
private fun startService(
    intent: Intent = Intent(appContext, MessageAccessService::class.java),
    action: String? = null
) {
    // Android8.0再也不容許後臺service直接經過startService方式去啓動,將引起IllegalStateException
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
        && !ProcessUtil.isForeground(appContext)
    ) {
        if (!TextUtils.isEmpty(action)) intent.action = action
        intent.putExtra(KeepForegroundService.EXTRA_ABANDON_FOREGROUND, false)
        appContext.startForegroundService(intent)
    } else {
        appContext.startService(intent)
    }
}

/**
 * 中止消息接入服務
 */
fun stopService() {
    // 當即清除緩存的WebSocket服務器地址,防止登陸時再次使用舊的WebSocket服務器地址(帶的會話已失效),致使收到用戶下線的通知
    GlobalScope.launch {
        DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
    }

    unbindService()

    appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}

/**
 * 解綁消息接入服務
 */
@Synchronized
fun unbindService() {
    if (!isBound) return // 必須判斷服務是否已解除綁定,不然會報java.lang.IllegalArgumentException: Service not registered

    // 解除消息監聽接口
    if (messageCarrier?.asBinder()?.isBinderAlive == true) {
        messageCarrier?.unregisterReceiveListener(messageReceiver)
        messageCarrier = null
    }

    appContext.unbindService(serviceConnection)

    isBound = false
}

複製代碼

總結

經過以上代碼的實踐,最終咱們得以將應用拆分爲主進程和遠程進程。主進程主要負責用戶交互、界面展現,而遠程進程則主要負責消息收發、鏈接保持等。因爲遠程進程僅保持了最小限度的業務邏輯處理,內存增加相對穩定,所以會大大下降系統內存緊張時遠端進程被終止的機率,即便主進程由於意外狀況退出了,遠程進程仍可保持運行,從而保證鏈接的穩定性。

「椎鋒陷陳」微信技術號現已開通,爲了得到第一手的技術文章推送,歡迎搜索關注!

參考

WebSocket詳解(一):初步認識WebSocket技術

www.52im.net/thread-331-…

內存管理概覽

developer.android.google.cn/topic/perfo…

進程和應用生命週期

developer.android.google.cn/guide/compo…

服務概覽

developer.android.google.cn/guide/compo…

綁定服務概覽

developer.android.google.cn/guide/compo…

Android 接口定義語言 (AIDL)

developer.android.google.cn/guide/compo…

相關文章
相關標籤/搜索