這是即時(shí)通訊系列文章的第一篇卵贱,正式開(kāi)始對(duì)IM開(kāi)發(fā)技術(shù)的講解之前滥沫,我們先來(lái)談?wù)効蛻?hù)端在完整聊天系統(tǒng)中所扮演的角色,為此键俱,我們必須先明確客戶(hù)端的職責(zé)兰绣。
現(xiàn)今主流的IM應(yīng)用幾乎都是采用服務(wù)器中轉(zhuǎn)的方式來(lái)進(jìn)行消息傳輸?shù)模瑸榈氖歉玫刂С蛛x線编振、群組等業(yè)務(wù)缀辩。在這種模式下,所有客戶(hù)端都需連接到服務(wù)端踪央,服務(wù)端將不同客戶(hù)端發(fā)給自己的消息根據(jù)消息里攜帶的用戶(hù)標(biāo)識(shí)進(jìn)行轉(zhuǎn)發(fā)或廣播臀玄。
因此,作為消息收發(fā)的終端設(shè)備畅蹂,客戶(hù)端的重要職責(zé)之一就是保持與服務(wù)端的連接健无,該連接的穩(wěn)定性直接決定消息收發(fā)的實(shí)時(shí)性和可靠性。而在上篇文章我們講過(guò)液斜,移動(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)用戶(hù)切換到其他應(yīng)用時(shí),系統(tǒng)會(huì)將原有應(yīng)用的進(jìn)程保留在緩存中,稍后如果用戶(hù)返回該應(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)存泄漏問(wèn)題鞋怀,并且只有一個(gè)主進(jìn)程,勢(shì)必會(huì)隨著應(yīng)用使用時(shí)間的延長(zhǎng)而逐步增大內(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)存消耗過(guò)高等情況贫导,該服務(wù)仍可正常運(yùn)行抛猫,甚至可以在適當(dāng)?shù)臅r(shí)機(jī)通過(guò)廣播等方式重新喚起主進(jìn)程。
但是孩灯,給應(yīng)用劃分進(jìn)程闺金,往往就意味著需要編寫(xiě)額外的進(jìn)程通訊代碼,特別是對(duì)于消息服務(wù)這種需要高度交互的場(chǎng)景钱反。而由于各個(gè)進(jìn)程都運(yùn)行在相對(duì)獨(dú)立的內(nèi)存空間,因而是無(wú)法直接通訊的匣距。為此面哥,Android提供了AIDL(Android Interface Definition Language,Android接口定義語(yǔ)言)用于實(shí)現(xiàn)進(jìn)程間通信毅待,其本質(zhì)就是實(shí)現(xiàn)對(duì)象的序列化尚卫、傳輸、接收和反序列化尸红,得到可操作的對(duì)象后再進(jìn)行常規(guī)的方法調(diào)用吱涉。
接下來(lái),就讓我們來(lái)一步步實(shí)現(xiàn)跨進(jìn)程的通訊吧外里。
Step1 創(chuàng)建服務(wù)
由于連接保持的工作是需要在后臺(tái)執(zhí)行長(zhǎng)時(shí)間執(zhí)行的操作怎爵,通常不提供操作界面,符合這個(gè)特性的組件就是Service了盅蝗,因此我們選用Service作為與遠(yuǎn)程進(jìn)程進(jìn)行進(jìn)程間通信(IPC)的組件鳖链。創(chuàng)建Service的子類(lèi)時(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ù)是用戶(hù)主動(dòng)意識(shí)到的一種服務(wù)灌侣,進(jìn)程優(yōu)先級(jí)較高推捐,因此在內(nèi)存不足時(shí),系統(tǒng)也不會(huì)考慮將其終止侧啼。
使用前臺(tái)服務(wù)唯一的缺點(diǎn)就是必須在抽屜式通知欄提供一條不可移除的通知牛柒,對(duì)于用戶(hù)體驗(yàn)極不友好,但是我們可以通過(guò)定制通知樣式進(jìn)行協(xié)調(diào)慨菱,后續(xù)的文章中會(huì)講到焰络。
step2 指定進(jìn)程
默認(rèn)情況下,同一應(yīng)用的所有組件均在相同的進(jìn)程中運(yùn)行符喝。如需控制某個(gè)組件所屬的進(jìn)程闪彼,可通過(guò)在清單文件中設(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ì)象茉稠,客戶(hù)端可使用該對(duì)象定義好的接口與服務(wù)進(jìn)行通信描馅。IBinder是遠(yuǎn)程對(duì)象的基礎(chǔ)接口,該接口描述了與遠(yuǎn)程對(duì)象交互的抽象協(xié)議而线,但不建議直接實(shí)現(xiàn)此接口铭污,而應(yīng)從Binder擴(kuò)展。通常做法是是使用.aidl文件來(lái)描述所需的接口膀篮,使其生成適當(dāng)?shù)腂inder子類(lèi)嘹狞。
那么,這個(gè)最關(guān)鍵的.aidl文件該如何創(chuàng)建誓竿,又該定義哪些接口呢磅网?
創(chuàng)建.aidl文件很簡(jiǎn)單,Android Studio本身就提供了創(chuàng)建AIDL文件方法:項(xiàng)目右鍵 -> New -> AIDL -> AIDL File
前面講過(guò)筷屡,客戶(hù)端是消息收發(fā)的終端設(shè)備涧偷,而接入服務(wù)則是為客戶(hù)端提供了消息收發(fā)的出入口”兴溃客戶(hù)端發(fā)出的消息經(jīng)由接入服務(wù)發(fā)送到服務(wù)端燎潮,同時(shí)客戶(hù)端會(huì)委托接入服務(wù)幫忙收取消息,當(dāng)服務(wù)端有消息推送過(guò)來(lái)時(shí)通知自己扼倘。
如此一來(lái)便很清晰了跟啤,我們要定義的接口總共有三個(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è)類(lèi)隅肥,該類(lèi)是多進(jìn)程通訊中作為數(shù)據(jù)傳輸?shù)膶?shí)體類(lèi)竿奏。AIDL支持的數(shù)據(jù)類(lèi)型除了基本數(shù)據(jù)類(lèi)型、String和CharSequence腥放,還有就是實(shí)現(xiàn)了Parcelable接口的對(duì)象泛啸,以及其中元素為以上幾種的List和Map。
Envelope.java
**
* 用于多進(jìn)程通訊的信封類(lèi)
* <p>
* 在AIDL中傳遞的對(duì)象秃症,需要在類(lèi)文件相同路徑下候址,創(chuàng)建同名、但是后綴為.aidl的文件种柑,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類(lèi)岗仑;
* 但實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類(lèi)往往分散在不同的模塊,所以通過(guò)構(gòu)建一個(gè)包裝類(lèi)來(lái)包含真正需要被傳遞的對(duì)象(必須也實(shí)現(xiàn)Parcelable接口)
*/
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
val noticeVo: NoticeVo? = null) : Parcelable {
}
另外聚请,在AIDL中傳遞的對(duì)象荠雕,需要在上述類(lèi)文件的相同包路徑下,創(chuàng)建同名驶赏、但是后綴為.aidl的文件炸卑,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類(lèi),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類(lèi)而不直接是MessageVO類(lèi)(消息視圖對(duì)象)呢盖文?這是由于考慮到實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類(lèi)往往分散在不同的模塊(MessageVO從屬于另外一個(gè)模塊,需要被其他模塊引用)蚯姆,所以通過(guò)構(gòu)建一個(gè)包裝類(lèi)來(lái)包含真正需要被傳遞的對(duì)象(該對(duì)象必須也實(shí)現(xiàn)Parcelable接口)五续,這也是該類(lèi)命名為Envelope(信封)的含義。
MessageReceiver ->
跨進(jìn)程的消息收取回調(diào)接口龄恋,用于將消息接入服務(wù)收取到的服務(wù)端消息傳遞到客戶(hù)端疙驾。但這里使用的回調(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/目錄中偏竟。生成文件的名稱(chēng)與.aidl 文件的名稱(chēng)保持一致煮落,區(qū)別在于其使用.java 擴(kuò)展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)踊谋。此接口擁有一個(gè)名為Stub的內(nèi)部抽象類(lèi)蝉仇,用于擴(kuò)展 Binder 類(lèi)并實(shí)現(xiàn) AIDL 接口中的方法。
/** 根據(jù)MessageCarrier.aidl文件自動(dòng)生成的Binder對(duì)象,需要返回給客戶(hù)端 */
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)可以通過(guò)調(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)聽(tīng)與服務(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ì)銷(xiāo)毀該服務(wù)底洗。為了使服務(wù)能夠無(wú)限期運(yùn)行腋么,可同時(shí)調(diào)用startService()和bindService(),創(chuàng)建同時(shí)具有已啟動(dòng)和已綁定兩種狀態(tài)的服務(wù)亥揖。這樣珊擂,即使所有組件均解綁服務(wù),系統(tǒng)也不會(huì)銷(xiāo)毀該服務(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直接通過(guò)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)致收到用戶(hù)下線的通知
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)聽(tīng)接口
if (messageCarrier?.asBinder()?.isBinderAlive == true) {
messageCarrier?.unregisterReceiveListener(messageReceiver)
messageCarrier = null
}
appContext.unbindService(serviceConnection)
isBound = false
}
總結(jié)
通過(guò)以上代碼的實(shí)踐,最終我們得以將應(yīng)用拆分為主進(jìn)程和遠(yuǎn)程進(jìn)程昼激。主進(jìn)程主要負(fù)責(zé)用戶(hù)交互庇绽、界面展示,而遠(yuǎn)程進(jìn)程則主要負(fù)責(zé)消息收發(fā)橙困、連接保持等瞧掺。由于遠(yuǎn)程進(jìn)程僅保持了最小限度的業(yè)務(wù)邏輯處理,內(nèi)存增長(zhǎng)相對(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 接口定義語(yǔ)言 (AIDL)