內(nèi)容簡(jiǎn)介:
近年來(lái)隨著APP應(yīng)用社交化的發(fā)展,越來(lái)越多的應(yīng)用開(kāi)始接入即時(shí)通訊 SDK ,以便快速實(shí)現(xiàn)社交功能。同時(shí)開(kāi)發(fā)者希望有一款通用的 IM UI 來(lái)避免重復(fù)開(kāi)發(fā),提高開(kāi)發(fā)效率。本次分享將會(huì)結(jié)合極光推送公司JChat產(chǎn)品的開(kāi)發(fā)經(jīng)驗(yàn),介紹如何優(yōu)雅地實(shí)現(xiàn)一款通用的 IM UI 庫(kù),并談?wù)勯_(kāi)發(fā)過(guò)程中遇到的坑及相應(yīng)的解決方法,以及如何減少重復(fù)開(kāi)發(fā)和增加代碼的可擴(kuò)展性。
最近半年的時(shí)間里,我從 SDK 開(kāi)發(fā)轉(zhuǎn)到 IM UI 庫(kù)的開(kāi)發(fā)(其實(shí)就是一個(gè)完整 IM APP),也完全過(guò)度到使用 Swift 進(jìn)行開(kāi)發(fā)。直到某天,領(lǐng)導(dǎo)對(duì)我說(shuō):“你去做個(gè)關(guān)于我們極光 IM 的演講吧”,當(dāng)時(shí)我就懵逼了,看來(lái)唯有談?wù)勎易罱肽甑墓ぷ餍牡昧?,我作為一位?zhí)行力比較強(qiáng)的小跟班,領(lǐng)導(dǎo)的話我肯定是服從安排,所以很直接就把主題定為:
如何使用 極光 IM JMessage
嗯...目測(cè)這樣直接打廣告,顯得水平就不太高,主辦方也無(wú)情地拒絕了,雖然我們是同一家公司的,但他們還是很堅(jiān)守原則的,所以只能稍微把演講主題改了一下:
如何打造一款通用的 IM UI 庫(kù)
UI 庫(kù)與 APP 的差別
開(kāi)發(fā)者 VS 用戶
在座的各位,估計(jì)大部分都是 iOS 開(kāi)發(fā)者,我們當(dāng)中,可能較多的人都是從事 APP 開(kāi)發(fā),估計(jì)也有部分是從事 SDK 開(kāi)發(fā)的同學(xué)。我們都知道,APP 主要面向的用戶,用戶更加注重的是應(yīng)用的使用和功能,而SDK 面向的則是開(kāi)發(fā)者,開(kāi)發(fā)者關(guān)注的則是 SDK 具有哪些功能和這些功能是如何去實(shí)現(xiàn)和使用的。同樣,UI 庫(kù)其實(shí)也可以說(shuō)成是 SDK,它只是針對(duì)界面層的 SDK,它面向同樣是開(kāi)發(fā)者,有著和 SDK 類似的特點(diǎn)。
重復(fù)造輪子
“Stop Trying to Reinvent the Wheel”
因?yàn)槲宜诘牟块T本身就是開(kāi)發(fā) IM SDK 的,去寫這么一個(gè) UI 庫(kù)的主要目的還是避免開(kāi)發(fā)者重復(fù)開(kāi)發(fā)輪子,畢竟時(shí)間可貴,珍惜生命,少寫重復(fù)代碼。在我們實(shí)際開(kāi)發(fā)中,我們不應(yīng)該重復(fù)造輪子,聽(tīng)好幾個(gè)朋友說(shuō)過(guò),他們公司不允許使用任何的第三方庫(kù),這個(gè)不知道是出于什么原因,但個(gè)人感覺(jué)這是一種浪費(fèi)生命的行為,對(duì)于那些優(yōu)秀的開(kāi)源框架,比如說(shuō)像 AFNetWorking,當(dāng)我們需要使用相關(guān)的功能時(shí),我們完全有理由去拿來(lái)直接使用,而不是花大量的時(shí)間去開(kāi)發(fā)新的輪子,軟件是有生命周期的,可能待你把所以有輪子造好,你的軟件就已經(jīng)可以和市場(chǎng) say goodbye 了,并且輪子造好時(shí),還需要花費(fèi)大量的人力和時(shí)間去進(jìn)行測(cè)試和驗(yàn)收。
作為一個(gè)開(kāi)發(fā)者,在工作上面壓力很多時(shí)候都不會(huì)輕,在我們有限的開(kāi)發(fā)生涯中,應(yīng)該如何有效利用時(shí)間來(lái)做一些更有價(jià)值的事情,而且不是浪費(fèi)在造輪子上。顯然,羅馬不是一天建成的,也不是一個(gè)人建成的。我們需要學(xué)會(huì)把自己和別人寫的代碼組織起來(lái),高效地利用,并以此為基礎(chǔ)構(gòu)建軟件。如何優(yōu)雅地實(shí)現(xiàn)一款通用的組件,在方便自己工作的同時(shí),給其它開(kāi)發(fā)者帶來(lái)方便,這就是我今天想講的主題,下面都會(huì)以 IM UI 庫(kù)為例進(jìn)行演講。
可兼容性
作為一款通用的 IM UI 庫(kù),首先兼容性是必不可少的,它不是單純的一個(gè) APP,它應(yīng)該更具有通用性,兼容各類型的 IM SDK,而不單單是針對(duì)自己公司的產(chǎn)品,最理想的姿勢(shì)當(dāng)然是支持所有類型的 IM SDK,但理想都是美好的,現(xiàn)實(shí)卻總是會(huì)時(shí)不時(shí)打擊下我們。這里就先不管能不能支持所有的 IM SDK,這是一個(gè) target,前方路的還很漫長(zhǎng),我們尚需努力。下面將從 JChat 的架構(gòu)設(shè)計(jì)來(lái)你介紹整個(gè) UI 庫(kù)的兼容性實(shí)現(xiàn)和解耦過(guò)程。
JChat 架構(gòu)設(shè)計(jì)
舊 JChat 消息處理

在接手 JChat Swift 開(kāi)發(fā)之前,有一個(gè)年久失修的 OC 版本的 JChat,它在消息處理層上,是直接使用 SDK里面的 JMSGMessage 作為整個(gè)應(yīng)用的消息體對(duì)象來(lái)使用(這里說(shuō)明下,這里的 SDK 指的是我們極光 IM SDK,下面不重復(fù)說(shuō)明),這樣做,也不是說(shuō)不可以,多么簡(jiǎn)單明了,但是,如果某一天,領(lǐng)導(dǎo)說(shuō):“這個(gè) IM SDK 滿足不了我們當(dāng)前業(yè)務(wù),我們需要更換成 xxxx IM SDK,下周出新版本”。

估計(jì)如果是新來(lái)接手這個(gè)項(xiàng)目的人,肯定懵逼了,我想整個(gè)應(yīng)用的業(yè)務(wù)邏輯層都需要去改,這其中到底有多苦逼,只有自己去真正去體驗(yàn)一把,試過(guò)才能知道其中有多艱辛。希望位都不會(huì)遇到這種神項(xiàng)目,如果真的碰到了,我也只能對(duì)你說(shuō)一句:“兄弟,笑著活下去吧”。

同時(shí)也希望所有人盡量不要寫這種代碼,說(shuō)不定某天剛好與接手你項(xiàng)目的同事或前同事相遇街角,狹路相逢,說(shuō)不定你就需要躺著出來(lái)。
在就里只是和大家開(kāi)個(gè)玩笑,但并不是不可能的,好了,下面回到正題。
JChat Swift 消息處理

在 JChat Swift 里面,不再使用這種高耦合的方式,而是在上層再封裝一層與 IM SDK 無(wú)關(guān)的 JCMessage,只保留消息展示所需的信息,在應(yīng)用的業(yè)務(wù)邏輯層里面,都只依賴于 JCMessage,這樣不管你使用的極光的 IM 也好,還是環(huán)信的 IM 也好,或者其它的 IM SDK,只需要去修改從 xxxMessage -> JCMessage 的解析方法就可以了,其它的業(yè)務(wù)邏輯就基本不需要去改動(dòng)了。
同時(shí),為了提供更好擴(kuò)展性,我們應(yīng)該提供一個(gè) JCMessageType 協(xié)議:
protocol JCMessageType: class {
var msgId: String { get }
var content: JCMessageContentType { get }
var options: JCMessageOptions { get }
var targetType: MessageTargetType { get }
// ...
}

這樣不管是 JCMessage 還是 XMessage, 只需要實(shí)現(xiàn) JCMessageType 協(xié)議:
class JCMessage: NSObject, JCMessageType {
init(content: JCMessageContentType) {
self.content = content
self.options = JCMessageOptions(with: content)
super.init()
}
open var msgId = ""
open var targetType: MessageTargetType = .single
}
那么在原來(lái)的邏輯上都不需要改動(dòng),開(kāi)發(fā)者還可以自定義一些字段或者做一些其它的擴(kuò)展,使用的自由度更大。
這里雖然是 IM UI 庫(kù)的實(shí)現(xiàn)為,但其實(shí)在其它地方上也是同理的,比如使用某些第三方閉源包時(shí),在上層提供一層穩(wěn)定的 api,使上層的業(yè)務(wù)邏輯保持穩(wěn)定,當(dāng) SDK Api 或者內(nèi)部實(shí)現(xiàn)發(fā)生變動(dòng)時(shí),我們只需要在底層的實(shí)現(xiàn)去做適配就可以了,上層業(yè)務(wù)層就不會(huì)受影響,把受影響范圍控制在最小。
消息類型的擴(kuò)展
在做 IM 應(yīng)用的時(shí)候,變動(dòng)最多的莫過(guò)于各種類型的消息添加了,比如今天只需要最簡(jiǎn)單的文本消息、語(yǔ)音消息和圖片消息,過(guò)兩天就需要你添加片名消息、閱后即焚消息等。所以在 IM UI 庫(kù)中,如何設(shè)計(jì)各種消息體的實(shí)現(xiàn)就很重要了。
protocol JCMessageContentType: class {
// 消息體展示的大小
func sizeThatFits(_ size: CGSize) -> CGSize
// 消息類型
static var viewType: JCMessageContentViewType.Type { get }
}
protocol JCMessageContentViewType: class {
init()
// 渲染消息
func apply(_ message: JCMessageType)
}
消息的展示,其實(shí)只需要知道消息內(nèi)容和類型就可以繪制出來(lái),所以在這里定義了 JCMessageContentType 協(xié)議和 JCMessageContentViewType 協(xié)議,消息 Content 實(shí)現(xiàn) JCMessageContentType 時(shí)需要實(shí)現(xiàn) sizeThatFits 方法來(lái)返回 content 的 size,來(lái)確定它在界面上顯示的大小,同時(shí)需要定義它的 ContentViewType,就是它的類型。消息的展示 View 實(shí)現(xiàn) JCMessageContentViewType 時(shí),需要實(shí)現(xiàn) apply 方法,通過(guò) apply 方法來(lái)把 message 的信息渲染到界面上。
在 ChatViewLayout(MessageCell 布局文件) 中,通過(guò) JCMessageContentType 的 sizeThatFits 來(lái)獲取 MessageCell 的大小,在 MessageCell 中,則是通過(guò) JCMessageContentViewType 的 apply 來(lái)設(shè)置展示的內(nèi)容,不管你是什么類型的消息,只要你符合協(xié)議的要求,ChatView 就可以把 Message 渲染出來(lái),這樣就可以降低 ChatView 與 Message Type 的耦合,使用者就可以更快更方便地實(shí)現(xiàn)各種類型的消息,并且不需要原來(lái)的代碼進(jìn)行改動(dòng)。
API 設(shè)計(jì)
最小化原則
盡可能少的接口來(lái)完成任務(wù)
盡可能少的訪問(wèn)權(quán)限
ChatView
下面說(shuō)下整個(gè) UI 庫(kù)最復(fù)雜的界面 ChatView 的 API。

public func insert(_ newMessage: JCMessageType, at index: Int)
public func insert(contentsOf newMessages: Array<JCMessageType>, at index: Int)
public func append(_ newMessage: JCMessageType)
public func append(contentsOf newMessages: Array<JCMessageType>)
public func update(_ newMessage: JCMessageType, at index: Int)
public func removeAll()
public func remove(at index: Int)
public func remove(contentOf indexs: Array<Int>)
基于 UI 庫(kù)的特點(diǎn),相較于 app 開(kāi)發(fā),需要更著重地考慮 API 的設(shè)計(jì)。你標(biāo)記為 public 的內(nèi)容將是使用者能看到的內(nèi)容。提供什么樣的 API 在很大程度上決定了其他的開(kāi)發(fā)者會(huì)如何使用該 UI 庫(kù)。
在 API 設(shè)計(jì)的時(shí)候,從原則上來(lái)說(shuō),我們一開(kāi)始可以提供盡可能少的接口來(lái)完成必要的任務(wù),這有利于控制整個(gè) UI 庫(kù)的復(fù)雜程度。 在 ChatView 中我們只提供必須的添加、刪除和修改消息的接口,只需要向 ChatView 傳遞正確 JCMessageType,ChatView 就會(huì)負(fù)責(zé)在界面上渲染出來(lái),使用者不需要再去關(guān)心 ChatView 的顯示過(guò)程,只需要保證傳遞正確 JCMessageType 序列就可以了。最少的接口也減少了開(kāi)發(fā)者的學(xué)習(xí)成本,減少不必須的歧義,如果后期需要,開(kāi)發(fā)者可以對(duì)其進(jìn)行二次開(kāi)發(fā),添加所需的公共方法,或者把原有的一些私有方法設(shè)置成公有。
OC 與 Swift 命名兼容
JChat 性能優(yōu)化
緩存
在 JChat Swift 實(shí)現(xiàn)中,為了提高性能,很多地方都添加了緩存,就像緩存計(jì)算出來(lái)的 Message Cell 的 size、圖片加載資源加載等,這里以 JChat 主題管理功能為例,詳細(xì)說(shuō)下。

JChat 的主題管理功能是通過(guò) bundle 來(lái)管理圖片,不同的主題皮膚的圖片資源放在對(duì)應(yīng)的 bundle 里面,共同的資源放在默認(rèn)的 bundle 中,當(dāng)監(jiān)聽(tīng)到主題切換時(shí),只需要切換圖片訪問(wèn)路徑并刷新界面就可以了。
關(guān)于 JChat 主題管理功能的實(shí)現(xiàn)的詳細(xì)可以參考:
在 JChat 中,聊天的時(shí)候,較多界面上都有進(jìn)行頻繁的刷新,就如聊天列表或消息列表,這里就會(huì)有大頻率的重復(fù)訪問(wèn)本地的圖片的,特別是當(dāng)用戶長(zhǎng)時(shí)間沒(méi)有登錄,積累了大量離線消息時(shí),下次登錄時(shí),會(huì)一次性收到大量的離線消息,在上層刷新頻繁就會(huì)非常大了,一些應(yīng)用里面的默認(rèn)圖片的訪問(wèn)量可能就會(huì)比較大,我們通過(guò)文件的方式來(lái)加載本地圖片時(shí),就會(huì)存在性能的問(wèn)題,所以在訪問(wèn)圖片資源的時(shí)候,如果該圖片如果已經(jīng)緩存在內(nèi)存中時(shí),我們就從緩存中讀,如果 緩存中沒(méi)有,則從硬盤里面讀取,并把該圖片緩存到內(nèi)存中,這樣的話,資源圖片實(shí)質(zhì)上都只加載一次,而不需要多次去加載。需要注意的是,因?yàn)閳D片一直緩存在內(nèi)存中時(shí),就需要監(jiān)聽(tīng)系統(tǒng)是否有內(nèi)存警告,如果系統(tǒng)發(fā)出內(nèi)存警告時(shí),就需要手動(dòng)去清空緩存,避免應(yīng)用 crash。
其它
- 離屏渲染(Offscreen-Rendered)
- 圖層混合(Blended Layers)
- 復(fù)雜界面不使用 autolayout
- ...
結(jié)束語(yǔ)
簡(jiǎn)單的小結(jié)下,雖然整個(gè)演講都以 IM UI 為例,但實(shí)際上,在其它方面的開(kāi)發(fā)也是類似套路的,以不變應(yīng)萬(wàn)變,萬(wàn)變不離其宗,程序開(kāi)發(fā),最重要的是思路。