這是即時(shí)通訊系列文章的第一篇,正式開始對(duì)IM開發(fā)技術(shù)的講解之前,我們先來談?wù)効蛻舳嗽谕暾奶煜到y(tǒng)中所扮演的角色,為此,我們必須先明確客戶端的職責(zé)。
現(xiàn)今主流的IM應(yīng)用幾乎都是采用服務(wù)器中轉(zhuǎn)的方式來進(jìn)行消息傳輸?shù)模瑸榈氖歉玫刂С蛛x線、群組等業(yè)務(wù)。在這種模式下,所有客戶端都需連接到服務(wù)端,服務(wù)端將不同客戶端發(fā)給自己的消息根據(jù)消息里攜帶的用戶標(biāo)識(shí)進(jìn)行轉(zhuǎn)發(fā)或廣播。
因此,作為消息收發(fā)的終端設(shè)備,客戶端的重要職責(zé)之一就是保持與服務(wù)端的連接,該連接的穩(wěn)定性直接決定消息收發(fā)的實(shí)時(shí)性和可靠性。而在上篇文章我們講過,移動(dòng)設(shè)備是資源受限的,這對(duì)連接的穩(wěn)定性提出了極大的挑戰(zhàn),具體可體現(xiàn)在以下兩個(gè)方面:
- 為了維持多任務(wù)環(huán)境的正常運(yùn)行,Android為每個(gè)應(yīng)用的堆大小設(shè)置了硬性上限,不同設(shè)備的確切堆大小取決于設(shè)備的總體可用RAM大小,如果應(yīng)用在達(dá)到堆容量上限后嘗試分配更多內(nèi)容,則可能引發(fā)OOM。
- 當(dāng)用戶切換到其他應(yīng)用時(shí),系統(tǒng)會(huì)將原有應(yīng)用的進(jìn)程保留在緩存中,稍后如果用戶返回該應(yīng)用,系統(tǒng)就會(huì)重復(fù)使用該進(jìn)程,以便加快應(yīng)用切換速度。但當(dāng)系統(tǒng)資源(如內(nèi)存)不足時(shí),系統(tǒng)會(huì)考慮終止占用最多內(nèi)存的、優(yōu)先級(jí)較低的進(jìn)程以釋放RAM。
雖然ART和Dalvik虛擬機(jī)會(huì)例行執(zhí)行垃圾回收任務(wù),但如果應(yīng)用存在內(nèi)存泄漏問題,并且只有一個(gè)主進(jìn)程,勢必會(huì)隨著應(yīng)用使用時(shí)間的延長而逐步增大內(nèi)存使用量,從而增加引發(fā)OOM的概率和緩存進(jìn)程被系統(tǒng)終止的風(fēng)險(xiǎn)。
因此,為了保證連接的穩(wěn)定性,可考慮將負(fù)責(zé)連接保持工作的消息服務(wù)放入一個(gè)獨(dú)立的進(jìn)程中,分離之后即使主進(jìn)程退出、崩潰或者出現(xiàn)內(nèi)存消耗過高等情況,該服務(wù)仍可正常運(yùn)行,甚至可以在適當(dāng)?shù)臅r(shí)機(jī)通過廣播等方式重新喚起主進(jìn)程。
但是,給應(yīng)用劃分進(jìn)程,往往就意味著需要編寫額外的進(jìn)程通訊代碼,特別是對(duì)于消息服務(wù)這種需要高度交互的場景。而由于各個(gè)進(jìn)程都運(yùn)行在相對(duì)獨(dú)立的內(nèi)存空間,因而是無法直接通訊的。為此,Android提供了AIDL(Android Interface Definition Language,Android接口定義語言)用于實(shí)現(xiàn)進(jìn)程間通信,其本質(zhì)就是實(shí)現(xiàn)對(duì)象的序列化、傳輸、接收和反序列化,得到可操作的對(duì)象后再進(jìn)行常規(guī)的方法調(diào)用。
接下來,就讓我們來一步步實(shí)現(xiàn)跨進(jìn)程的通訊吧。
Step1 創(chuàng)建服務(wù)
由于連接保持的工作是需要在后臺(tái)執(zhí)行長時(shí)間執(zhí)行的操作,通常不提供操作界面,符合這個(gè)特性的組件就是Service了,因此我們選用Service作為與遠(yuǎn)程進(jìn)程進(jìn)行進(jìn)程間通信(IPC)的組件。創(chuàng)建Service的子類時(shí),必須實(shí)現(xiàn)onBind回調(diào)方法,此處我們暫時(shí)返回空實(shí)現(xiàn)。
class MessageAccessService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
另外使用Service還有一個(gè)好處就是,我們可以在適當(dāng)?shù)臅r(shí)機(jī)將其升級(jí)為前臺(tái)服務(wù),前臺(tái)服務(wù)是用戶主動(dòng)意識(shí)到的一種服務(wù),進(jìn)程優(yōu)先級(jí)較高,因此在內(nèi)存不足時(shí),系統(tǒng)也不會(huì)考慮將其終止。
使用前臺(tái)服務(wù)唯一的缺點(diǎn)就是必須在抽屜式通知欄提供一條不可移除的通知,對(duì)于用戶體驗(yàn)極不友好,但是我們可以通過定制通知樣式進(jìn)行協(xié)調(diào),后續(xù)的文章中會(huì)講到。
step2 指定進(jìn)程
默認(rèn)情況下,同一應(yīng)用的所有組件均在相同的進(jìn)程中運(yùn)行。如需控制某個(gè)組件所屬的進(jìn)程,可通過在清單文件中設(shè)置android:process屬性實(shí)現(xiàn):
<manifest ...>
<application ...>
<service
android:name=".service.MessageAccessService"
android:exported="true"
android:process=":remote" />
</application>
</manifest>
另外,為使其他進(jìn)程的組件能調(diào)用服務(wù)或與之交互,還需設(shè)置android:exported屬性為true。
step3 創(chuàng)建.aidl 文件
讓我們重新把目光放回onBind回調(diào)方法,該方法要求返回IBinder對(duì)象,客戶端可使用該對(duì)象定義好的接口與服務(wù)進(jìn)行通信。IBinder是遠(yuǎn)程對(duì)象的基礎(chǔ)接口,該接口描述了與遠(yuǎn)程對(duì)象交互的抽象協(xié)議,但不建議直接實(shí)現(xiàn)此接口,而應(yīng)從Binder擴(kuò)展。通常做法是是使用.aidl文件來描述所需的接口,使其生成適當(dāng)?shù)腂inder子類。
那么,這個(gè)最關(guān)鍵的.aidl文件該如何創(chuàng)建,又該定義哪些接口呢?
創(chuàng)建.aidl文件很簡單,Android Studio本身就提供了創(chuàng)建AIDL文件方法:項(xiàng)目右鍵 -> New -> AIDL -> AIDL File
前面講過,客戶端是消息收發(fā)的終端設(shè)備,而接入服務(wù)則是為客戶端提供了消息收發(fā)的出入口??蛻舳税l(fā)出的消息經(jīng)由接入服務(wù)發(fā)送到服務(wù)端,同時(shí)客戶端會(huì)委托接入服務(wù)幫忙收取消息,當(dāng)服務(wù)端有消息推送過來時(shí)通知自己。
如此一來便很清晰了,我們要定義的接口總共有三個(gè),分別為:
- 發(fā)送消息
- 注冊(cè)消息接收器
- 反注冊(cè)消息接收器
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);
}
這里解釋一下上述接口中攜帶的參數(shù)的含義:
Envelope ->
解釋這個(gè)參數(shù)之前,得先介紹Envelope.java這個(gè)類,該類是多進(jìn)程通訊中作為數(shù)據(jù)傳輸?shù)膶?shí)體類。AIDL支持的數(shù)據(jù)類型除了基本數(shù)據(jù)類型、String和CharSequence,還有就是實(shí)現(xiàn)了Parcelable接口的對(duì)象,以及其中元素為以上幾種的List和Map。
Envelope.java
**
* 用于多進(jìn)程通訊的信封類
* <p>
* 在AIDL中傳遞的對(duì)象,需要在類文件相同路徑下,創(chuàng)建同名、但是后綴為.aidl的文件,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類;
* 但實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類往往分散在不同的模塊,所以通過構(gòu)建一個(gè)包裝類來包含真正需要被傳遞的對(duì)象(必須也實(shí)現(xiàn)Parcelable接口)
*/
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
val noticeVo: NoticeVo? = null) : Parcelable {
}
另外,在AIDL中傳遞的對(duì)象,需要在上述類文件的相同包路徑下,創(chuàng)建同名、但是后綴為.aidl的文件,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類,Envelope.aidl就是對(duì)應(yīng)Envelope.java而創(chuàng)建的;
Envelope.aidl
package com.xxx.imsdk.comp.remote.bean;
parcelable Envelope;
兩個(gè)文件對(duì)應(yīng)的路徑比較如下:

那為什么是Envelope類而不直接是MessageVO類(消息視圖對(duì)象)呢?這是由于考慮到實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類往往分散在不同的模塊(MessageVO從屬于另外一個(gè)模塊,需要被其他模塊引用),所以通過構(gòu)建一個(gè)包裝類來包含真正需要被傳遞的對(duì)象(該對(duì)象必須也實(shí)現(xiàn)Parcelable接口),這也是該類命名為Envelope(信封)的含義。
MessageReceiver ->
跨進(jìn)程的消息收取回調(diào)接口,用于將消息接入服務(wù)收取到的服務(wù)端消息傳遞到客戶端。但這里使用的回調(diào)接口有點(diǎn)不一樣,在AIDL中傳遞的接口,不能是普通的接口,只能是AIDL接口,因此我們還需要新建多一個(gè).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);
}
包目錄結(jié)構(gòu)如下圖:

step4 返回IBinder接口
構(gòu)建應(yīng)用時(shí),Android SDK會(huì)生成基于.aidl 文件的IBinder接口文件,并將其保存到項(xiàng)目的gen/目錄中。生成文件的名稱與.aidl 文件的名稱保持一致,區(qū)別在于其使用.java 擴(kuò)展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)。此接口擁有一個(gè)名為Stub的內(nèi)部抽象類,用于擴(kuò)展 Binder 類并實(shí)現(xiàn) AIDL 接口中的方法。
/** 根據(jù)MessageCarrier.aidl文件自動(dòng)生成的Binder對(duì)象,需要返回給客戶端 */
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 綁定服務(wù)
組件(例如 Activity)可以通過調(diào)用bindService方法綁定到服務(wù),該方法必須提供ServiceConnection 的實(shí)現(xiàn)以監(jiān)控與服務(wù)的連接。當(dāng)組件與服務(wù)之間的連接建立成功后, ServiceConnection上的 onServiceConnected()方法將被回調(diào),該方法包含上一步返回的IBinder對(duì)象,隨后便可使用該對(duì)象與綁定的服務(wù)進(jìn)行通信。
/**
* ## 綁定消息接入服務(wù)
* 同時(shí)調(diào)用bindService和startService, 可以使unbind后Service仍保持運(yùn)行
* @param context 上下文
*/
@Synchronized
fun setupService(context: Context? = null) {
if (!::appContext.isInitialized) {
appContext = context!!.applicationContext
}
val intent = Intent(appContext, MessageAccessService::class.java)
// 記錄綁定服務(wù)的結(jié)果,避免解綁服務(wù)時(shí)出錯(cuò)
if (!isBound) {
isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
startService(intent)
}
/** 監(jiān)聽與服務(wù)連接狀態(tài)的接口 */
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 取得MessageCarrier.aidl對(duì)應(yīng)的操作接口
messageCarrier = MessageCarrier.Stub.asInterface(service)
...
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
可以同時(shí)將多個(gè)組件綁定到同一個(gè)服務(wù),但當(dāng)最后一個(gè)組件取消與服務(wù)的綁定時(shí),系統(tǒng)會(huì)銷毀該服務(wù)。為了使服務(wù)能夠無限期運(yùn)行,可同時(shí)調(diào)用startService()和bindService(),創(chuàng)建同時(shí)具有已啟動(dòng)和已綁定兩種狀態(tài)的服務(wù)。這樣,即使所有組件均解綁服務(wù),系統(tǒng)也不會(huì)銷毀該服務(wù),直至調(diào)用 stopSelf() 或 stopService() 才會(huì)顯式停止該服務(wù)。
/**
* 啟動(dòng)消息接入服務(wù)
* @param intent 意圖
* @param action 操作
*/
private fun startService(
intent: Intent = Intent(appContext, MessageAccessService::class.java),
action: String? = null
) {
// Android8.0不再允許后臺(tái)service直接通過startService方式去啟動(dòng),將引發(fā)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)
}
}
/**
* 停止消息接入服務(wù)
*/
fun stopService() {
// 立即清除緩存的WebSocket服務(wù)器地址,防止登錄時(shí)再次使用舊的WebSocket服務(wù)器地址(帶的會(huì)話已失效),導(dǎo)致收到用戶下線的通知
GlobalScope.launch {
DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
}
unbindService()
appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}
/**
* 解綁消息接入服務(wù)
*/
@Synchronized
fun unbindService() {
if (!isBound) return // 必須判斷服務(wù)是否已解除綁定,否則會(huì)報(bào)java.lang.IllegalArgumentException: Service not registered
// 解除消息監(jiān)聽接口
if (messageCarrier?.asBinder()?.isBinderAlive == true) {
messageCarrier?.unregisterReceiveListener(messageReceiver)
messageCarrier = null
}
appContext.unbindService(serviceConnection)
isBound = false
}
總結(jié)
通過以上代碼的實(shí)踐,最終我們得以將應(yīng)用拆分為主進(jìn)程和遠(yuǎn)程進(jìn)程。主進(jìn)程主要負(fù)責(zé)用戶交互、界面展示,而遠(yuǎn)程進(jìn)程則主要負(fù)責(zé)消息收發(fā)、連接保持等。由于遠(yuǎn)程進(jìn)程僅保持了最小限度的業(yè)務(wù)邏輯處理,內(nèi)存增長相對(duì)穩(wěn)定,因此會(huì)大大降低系統(tǒng)內(nèi)存緊張時(shí)遠(yuǎn)端進(jìn)程被終止的概率,即使主進(jìn)程因?yàn)橐馔馇闆r退出了,遠(yuǎn)程進(jìn)程仍可保持運(yùn)行,從而保證連接的穩(wěn)定性。
參考
WebSocket詳解(一):初步認(rèn)識(shí)WebSocket技術(shù)
http://www.52im.net/thread-331-1-1.html
內(nèi)存管理概覽
https://developer.android.google.cn/topic/performance/memory-overview
進(jìn)程和應(yīng)用生命周期
https://developer.android.google.cn/guide/components/activities/process-lifecycle
服務(wù)概覽
https://developer.android.google.cn/guide/components/services
綁定服務(wù)概覽
https://developer.android.google.cn/guide/components/bound-services
Android 接口定義語言 (AIDL)