圖片來自:https://unsplash.com/photos/m...
本文作者:旭風
背景
一款社交產品的誕生,離不開即時通訊(IM)場景。隨著團隊業務版圖在社交領域的佈局,誕生了多個社交場景APP,涉及的IM場景,包含私聊、群聊、聊天室等。
這些IM場景,在訊息流的展示形式上是極為相似的,同時每個業務又有著自己特殊的互動需求。基於此,我們對IM訊息流能力做了標準化的構建,來減少IM功能的業務接入成本;同時也是為了統一各個業務的技術方案,減少跨業務開發的理解和維護成本。本文主要針對iOS端在IM訊息流互動層的設計上,提供一些實踐思路。
業界方案
目前業界有各種即時通訊服務商(例如雲信、LeanCloud等)提供的配套互動層解決方案,其大多以犧牲靈活性來滿足快速整合需要,在定製能力上遠不能勝任我們業務需要。再者則是諸如 MessageKit
之類的社群IM框架,其在視覺互動表現上功能完備,能幫助我們快速、靈活搭建訊息流結構,但業務需要的是一套完整的攜帶訊息互動能力的方案,因此對此類框架,仍需要做不小的改造才能適應我們的業務。
思考
對於一個訊息流互動層方案,主要考慮幾個方面:
- 規範的訊息流結構:提供訊息流檢視結構規範化的構建方式
- 標準的訊息互動能力:統一訊息互動能力,業務方按需使用,快速整合
- 業務擴充性:針對資料來源、訊息互動能力提供業務靈活擴充點
- 業務接入成本:內建通用互動方案,降低業務接入成本
目前,我們存量業務中的IM場景,底層IM能力主要由雲信引擎提供。同時又存在基於業務服務端,透過HTTP去互動的場景。另外,還需要預留後期切換IM引擎的可能性,因此需要將互動層IM能力抽象出來。此外,為了適應團隊現狀,減小業務接入成本,考慮將雲信提供的互動能力內建在方案中。
整體設計
設計願景:提供標準化的能力,同時對擴充開放。
我們期望一套通用的訊息流能力,能夠在方案上標準化。這裡的標準化,主要包含訊息流結構構建的標準化,以及訊息互動能力的標準化。同時,方案需要在互動能力上適應不同業務場景,因此採用依賴注入的方式,提供業務定製能力。 按照職能劃分,將框架整體分為了兩層:
- 訊息流結構層:負責訊息流結構的構建,定義訊息檢視、佈局、資料上的規範,提供業務層分別在「訊息」、「會話」兩個維度的配置能力。
- 訊息互動層:提供訊息能力、訊息流、訊息資料方面的互動能力,向下依賴互動介面,內建標準互動能力的同時,也支援業務按需注入互動實現。
流結構
訊息元件
不同的業務場景,訊息流樣式表現必然有所差異。下面列出了我們幾個業務中的訊息流介面:
如何設計一套通用的訊息流檢視結構,滿足不同業務需要?經過對各個業務以及一些主流IM工具的觀察,將訊息檢視結構設計成如下結構,是能夠滿足我們各個IM場景需要的:
我將訊息結構拆分成了5部分,對應5個訊息元件 MessageView
,每個訊息元件都支援業務對其「樣式」、「顯隱」、「佈局」進行配置,從而滿足不同場景定製需要。
MessageView
作為基礎訊息元件,提供了一些標準能力,例如是否響應選單動作 canPerformMenuAction
、檢視重用回撥時機 prepareForReuse
、尺寸策略等。
open class MessageView: MessageAbstractView {
public var canPerformMenuAction = false
open func refresh(with message: Message) {}
open func prepareForReuse() {}
open class func createSizeStrategy(message: Message, fittingSize: CGSize) -> MessageLayoutSizeStrategy? {
// ...
}
}
尺寸策略
訊息元件尺寸作為訊息流佈局上不可或缺的要素,方案提供了多種尺寸計算策略 MessageLayoutSizeStrategy
:
- 自動佈局計算策略:業務方對訊息元件使用 AutoLayout 佈局時使用,內部會依據約束自動計算好元件尺寸
- SizeThatFit 策略:依據元件
SizeThatFit
方法返回的尺寸進行佈局 - 自定義策略:提供自定義尺寸計算方式
public protocol MessageLayoutSizeStrategy {
func caclulateSize(_ sizeViewType: MessageView.Type,
message: Message,
fittingSize: CGSize) -> CGSize
}
public struct MessageAutoLayoutSizeStrategy: MessageLayoutSizeStrategy {
public func caclulateSize(_ sizeViewType: MessageView.Type,
message: Message,
fittingSize: CGSize) -> CGSize {
// ...省略其他程式碼
return sizeView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
}
}
public struct MessageSizeThatFitsStrategy: MessageLayoutSizeStrategy {
public func caclulateSize(_ sizeViewType: MessageView.Type,
message: Message,
fittingSize: CGSize) -> CGSize {
// ...省略其他程式碼
return sizeView.sizeThatFits(fittingSize)
}
}
佈局快照
我們還針對訊息元件維度支援了佈局快照。通常當一個訊息元件尺寸固定,在互動過程中尺寸不會發生的情況下,開啟佈局快照,以減少佈局計算消耗。同時也提供了快照清除的能力。我們對多個訊息流在快速滾動過程中的CPU峰值做了統計,在使用自動佈局尺寸策略的情況下,開啟佈局快照,峰值降低了10%~20%。
互動事件
另外在手勢互動上,對外暴露了各個訊息元件的一系列互動事件。常見的場景例如單擊瀏覽訊息內容,長按展示訊息選單等。方案內部提供了基於系統樣式的長按選單,並提供上層選單配置能力,同時也可以基於暴露的長按手勢事件來自定義選單。
流
一個會話對應一個流,方案也提供了訊息流在會話維度上的一些標準化配置。例如訊息分頁數量、是否自動拉取歷史訊息、是否開啟增量重新整理,以及在時間展示上的樣式配置等。
此外為了減少列表重繪,訊息流也支援增量重新整理。通常情況下業務層不需要主動重新整理列表,只需對訊息資料進行增刪改操作,內部會觸發對資料來源的「diff-update」計算,從而驅動列表的增量更新。
互動層
對於業務方而言,在訊息互動上通常關心這麼幾點:
- 提供了哪些標準化的互動能力
- 如何擴充自定義的互動實現
- 如何對互動流程進行干預
結合團隊現狀,我們在方案內部內建了基於雲信的IM互動能力,同時定義了相關互動介面,供業務方按需注入實現。在實際業務中,一個APP內可能存在多個IM場景,因此互動能力支援按會話維度進行注入,各個會話之間的互動是相互隔離的。
訊息源
不同的IM場景,訊息資料來源可能存在差異。例如我們私聊、群聊的資料來源來自雲信資料同步服務,聊天室資料需要透過雲信提供的歷史訊息介面拉取,另外也存在諸如透過業務服務端介面來拉取訊息資料的場景。因此方案上設定了資料來源介面 SessionMessageProvider
,提供不同場景訊息源的定製能力。
public protocol SessionMessageProvider {
func messages(in session: Session,
anchorMessage: Message?,
limit: Int,
completion: @escaping ([Message]) -> Void)
}
方案設定了一個負責管理訊息資料來源的 DataManager 例項, 其依賴 SessionMessageProvider
提供的資料來源。同時內建了基於雲信的資料來源獲取實現,能夠根據當前會話型別,獲取私聊、群聊、聊天室的資料來源。如果當前場景是透過HTTP拉取訊息的,則需要業務上層手動注入一個從介面獲取資料來源的 SessionMessageProvider
例項。
互動源
方案提供了IM標準互動能力,例如訊息收發、訊息撤回、儲存等,以統一各業務互動姿勢。具體的互動源除了要考慮目前包含的雲信及業務服務端,也要適應其他互動源,因此將互動實現部分也抽象出了介面 MessageServiceInterface
。業務根據當前實際場景,注入具體的互動實現即可。下面列出了一些互動申明:
public protocol MessageServiceInterface {
func send(message: Message, in session: Session, completion: @escaping MessageServiceInterfaceCompletion)
func resend(message: Message, completion: @escaping MessageServiceInterfaceCompletion)
func forward(message: Message, to session: Session, completion: @escaping MessageServiceInterfaceCompletion)
func revoke(message: Message, completion: @escaping MessageServiceInterfaceCompletion)
func save(message: Message, in session: Session, completion: @escaping MessageServiceInterfaceCompletion)
func delete(message: Message, completion: @escaping MessageServiceInterfaceCompletion)
}
同樣,我們也內建了一些通用互動方案,例如支援雲信提供的私聊群聊互動能力,以及由中臺提供的通用聊天室服務互動能力,以支援相關場景下快速接入。
互動鉤子
在實際IM業務開發過程中,往往需要對互動流程做一些干預,或是在互動過程中做一些定製化的動作。因此方案也提供了一些互動鉤子,支援「互動前置校驗」、「互動前準備」。以訊息傳送流程為例,提供了「傳送前校驗」、「傳送準備」兩個訊息傳送過程的回撥鉤子:
public protocol MessageServicePrechecker {
// 訊息傳送前置校驗
func shouldSend(message: Message, in session: Session) -> Bool
// ...省略其他程式碼
}
public protocol MessageServicePreparation {
/// 準備傳送準備
func prepareSend(message: Message, in session: Session, callback: @escaping MessageServicePreparationCallback)
// ...省略其他程式碼
}
整體的傳送流程如圖所示:
前置校驗階段,用來作訊息傳送前的校驗工作,根據實際狀態決定訊息是否可以傳送。傳送準備階段,則可以在訊息投遞前做最後的準備工作,例如海外業務可以在這裡處理訊息資源附件上傳Amazon,或是在此處對訊息塞入一些客戶端資訊、反作弊Token等,支援非同步操作。
業務接入
業務只需要在上層提供針對訊息以及會話兩個維度的配置,就能基於內建的互動能力,構建出一套基礎的IM訊息流能力。在具體的訊息樣式呈現上,則通常需要業務層維護一組關於「訊息型別-訊息元件型別-訊息結構」的對映關係,具體關聯如下:
在互動能力上,提供了IM場景的標準能力,業務可以按需使用。
另外,實際IM場景可能需要一些更為豐富的定製能力,則可以依據方案提供的訊息資料來源介面、訊息互動介面來對具體互動實現進行定製。同時也可以使用相關的互動鉤子對互動過程進行干預,以適應自己的業務。
總結
本文對團隊IM場景的現狀做了簡單介紹,撇開具體實現細節,就如何搭建一套能夠適應多業務需要的通用IM訊息流互動層方案,提供了一些思考和實踐經驗。從結果來看,該方案穩定支撐了團隊多個IM場景,抹除各場景實現差異,有效降低了維護成本和新業務接入成本。
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!