社交場景下iOS消息流交互層實踐

語言: CN / TW / HK

圖片來自:https://unsplash.com/photos/mw6Onwg4frY

本文作者:旭風

背景

一款社交產品的誕生,離不開即時通訊(IM)場景。隨着團隊業務版圖在社交領域的佈局,誕生了多個社交場景APP,涉及的IM場景,包含私聊、羣聊、聊天室等。

這些IM場景,在消息流的展示形式上是極為相似的,同時每個業務又有着自己特殊的交互需求。基於此,我們對IM消息流能力做了標準化的構建,來減少IM功能的業務接入成本;同時也是為了統一各個業務的技術方案,減少跨業務開發的理解和維護成本。本文主要針對iOS端在IM消息流交互層的設計上,提供一些實踐思路。

業界方案

目前業界有各種即時通訊服務商(例如雲信、LeanCloud等)提供的配套交互層解決方案,其大多以犧牲靈活性來滿足快速集成需要,在定製能力上遠不能勝任我們業務需要。再者則是諸如 MessageKit 之類的社區IM框架,其在視覺交互表現上功能完備,能幫助我們快速、靈活搭建消息流結構,但業務需要的是一套完整的攜帶消息交互能力的方案,因此對此類框架,仍需要做不小的改造才能適應我們的業務。

思考

對於一個消息流交互層方案,主要考慮幾個方面:

  1. 規範的消息流結構:提供消息流視圖結構規範化的構建方式
  2. 標準的消息交互能力:統一消息交互能力,業務方按需使用,快速集成
  3. 業務拓展性:針對數據源、消息交互能力提供業務靈活拓展點
  4. 業務接入成本:內置通用交互方案,降低業務接入成本

目前,我們存量業務中的IM場景,底層IM能力主要由雲信引擎提供。同時又存在基於業務服務端,通過HTTP去交互的場景。另外,還需要預留後期切換IM引擎的可能性,因此需要將交互層IM能力抽象出來。此外,為了適應團隊現狀,減小業務接入成本,考慮將雲信提供的交互能力內置在方案中。

整體設計

設計願景:提供標準化的能力,同時對拓展開放。

我們期望一套通用的消息流能力,能夠在方案上標準化。這裏的標準化,主要包含消息流結構構建的標準化,以及消息交互能力的標準化。同時,方案需要在交互能力上適應不同業務場景,因此採用依賴注入的方式,提供業務定製能力。 按照職能劃分,將框架整體分為了兩層:

image-20220914152451655

  • 消息流結構層:負責消息流結構的構建,定義消息視圖、佈局、數據上的規範,提供業務層分別在「消息」、「會話」兩個維度的配置能力。
  • 消息交互層:提供消息能力、消息流、消息數據方面的交互能力,向下依賴交互接口,內置標準交互能力的同時,也支持業務按需注入交互實現。

流結構

消息組件

不同的業務場景,消息流樣式表現必然有所差異。下面列出了我們幾個業務中的消息流界面:

image-20220914163609223

如何設計一套通用的消息流視圖結構,滿足不同業務需要?經過對各個業務以及一些主流IM工具的觀察,將消息視圖結構設計成如下結構,是能夠滿足我們各個IM場景需要的:

image-20220914164113441

我將消息結構拆分成了5部分,對應5個消息組件 MessageView ,每個消息組件都支持業務對其「樣式」、「顯隱」、「佈局」進行配置,從而滿足不同場景定製需要。

MessageView作為基礎消息組件,提供了一些標準能力,例如是否響應菜單動作 canPerformMenuAction、視圖重用回調時機 prepareForReuse 、尺寸策略等。

```swift 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

  1. 自動佈局計算策略:業務方對消息組件使用 AutoLayout 佈局時使用,內部會依據約束自動計算好組件尺寸
  2. SizeThatFit 策略:依據組件 SizeThatFit 方法返回的尺寸進行佈局
  3. 自定義策略:提供自定義尺寸計算方式

```swift 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」計算,從而驅動列表的增量更新。

image-20220915101944057

交互層

對於業務方而言,在消息交互上通常關心這麼幾點:

  1. 提供了哪些標準化的交互能力
  2. 如何拓展自定義的交互實現
  3. 如何對交互流程進行干預

結合團隊現狀,我們在方案內部內置了基於雲信的IM交互能力,同時定義了相關交互接口,供業務方按需注入實現。在實際業務中,一個APP內可能存在多個IM場景,因此交互能力支持按會話維度進行注入,各個會話之間的交互是相互隔離的。

消息源

不同的IM場景,消息數據來源可能存在差異。例如我們私聊、羣聊的數據源來自雲信數據同步服務,聊天室數據需要通過雲信提供的歷史消息接口拉取,另外也存在諸如通過業務服務端接口來拉取消息數據的場景。因此方案上設置了數據源接口 SessionMessageProvider ,提供不同場景消息源的定製能力。

swift public protocol SessionMessageProvider { func messages(in session: Session, anchorMessage: Message?, limit: Int, completion: @escaping ([Message]) -> Void) }

方案設置了一個負責管理消息數據源的 DataManager 實例, 其依賴 SessionMessageProvider 提供的數據源。同時內置了基於雲信的數據源獲取實現,能夠根據當前會話類型,獲取私聊、羣聊、聊天室的數據源。如果當前場景是通過HTTP拉取消息的,則需要業務上層手動注入一個從接口獲取數據源的 SessionMessageProvider 實例。

image-20220915153237442

交互源

方案提供了IM標準交互能力,例如消息收發、消息撤回、保存等,以統一各業務交互姿勢。具體的交互源除了要考慮目前包含的雲信及業務服務端,也要適應其他交互源,因此將交互實現部分也抽象出了接口 MessageServiceInterface 。業務根據當前實際場景,注入具體的交互實現即可。下面列出了一些交互申明:

swift 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) }

同樣,我們也內置了一些通用交互方案,例如支持雲信提供的私聊羣聊交互能力,以及由中台提供的通用聊天室服務交互能力,以支持相關場景下快速接入。

image-20220915153244248

交互鈎子

在實際IM業務開發過程中,往往需要對交互流程做一些干預,或是在交互過程中做一些定製化的動作。因此方案也提供了一些交互鈎子,支持「交互前置校驗」、「交互前準備」。以消息發送流程為例,提供了「發送前校驗」、「發送準備」兩個消息發送過程的回調鈎子:

```swift public protocol MessageServicePrechecker { // 消息發送前置校驗
func shouldSend(message: Message, in session: Session) -> Bool

// ...省略其他代碼

}

public protocol MessageServicePreparation { /// 準備發送準備 func prepareSend(message: Message, in session: Session, callback: @escaping MessageServicePreparationCallback)

// ...省略其他代碼

} ```

整體的發送流程如圖所示:

image-20220915163725098

前置校驗階段,用來作消息發送前的校驗工作,根據實際狀態決定消息是否可以發送。發送準備階段,則可以在消息投遞前做最後的準備工作,例如海外業務可以在這裏處理消息資源附件上傳Amazon,或是在此處對消息塞入一些客户端信息、反作弊Token等,支持異步操作。

業務接入

業務只需要在上層提供針對消息以及會話兩個維度的配置,就能基於內置的交互能力,構建出一套基礎的IM消息流能力。在具體的消息樣式呈現上,則通常需要業務層維護一組關於「消息類型-消息組件類型-消息結構」的映射關係,具體關聯如下:

image-20220915171500598

在交互能力上,提供了IM場景的標準能力,業務可以按需使用。

另外,實際IM場景可能需要一些更為豐富的定製能力,則可以依據方案提供的消息數據源接口、消息交互接口來對具體交互實現進行定製。同時也可以使用相關的交互鈎子對交互過程進行干預,以適應自己的業務。

總結

本文對團隊IM場景的現狀做了簡單介紹,撇開具體實現細節,就如何搭建一套能夠適應多業務需要的通用IM消息流交互層方案,提供了一些思考和實踐經驗。從結果來看,該方案穩定支撐了團隊多個IM場景,抹除各場景實現差異,有效降低了維護成本和新業務接入成本。

本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!