大神推薦的頭像緩存策略
許多 App 都有用戶系統(tǒng),不論是自己實(shí)現(xiàn)還是使用第三方,大概都需要顯示用戶的頭像。比較常見的情景下,頭像會(huì)在某些列表里出現(xiàn),例如聯(lián)系人列表、消息列表等。
雖然頭像也是圖像,但相比于普通圖片,我們對頭像有更高的要求。
頭像的原始圖片可能有各種尺寸,但在 App 里,我們很可能需要某種固定樣式的頭像,例如正方形或者圓形。如果我們使用通用的圖片緩存工具如 SDWebImage、Kingfisher 等,那么還需要自己做圖片的裁剪和加工。如果直接用 UIImageView 來縮小,圖片細(xì)節(jié)就會(huì)變得過于“銳利”,影響觀看。
進(jìn)一步,一個(gè) App 里可能不只有一種頭像樣式。比如某些場景里要有大頭像,某些要用小頭像,某些要用原始尺寸的頭像;或者某些場景里要用正方形頭像,某些場景里要用圓角矩形頭像,不一而足。注意:單純用 layer.cornerRadius 或 CALayer 來 mask 都會(huì)導(dǎo)致列表的滑動(dòng)性能問題,因此不考慮。
如果要做優(yōu)化,我們當(dāng)然希望這些不同樣式的小頭像能夠存儲(chǔ)在本地,不用再從網(wǎng)絡(luò)獲取再裁減或處理樣式。一來減少不需要的流量消耗,二來提高頭像載入速度,用戶體驗(yàn)自然會(huì)更好。
基于上述分析,我們來設(shè)計(jì)一個(gè)頭像緩存系統(tǒng)。它的目標(biāo)是快速地獲取并緩存有樣式的頭像圖片,并能比較容易地集成到已有項(xiàng)目中。
一些前提:
頭像的圖片 URL 唯一,即不同的頭像有不同的 URL。如果用戶換了頭像,那么新頭像 URL 和舊的不一樣;
頭像是公開資源,不需要做驗(yàn)證即可下載。
好了,我們來做一做思維游戲。
首先,已有的用戶模型可能為:
- struct User {
- let userID: String
- let username: String
- let avatarURLString: String
- //...
- }
其中 avatarURLString 表示遠(yuǎn)端的頭像鏈接。如果我們要下載它,最簡單的話,直接用 NSData 的一個(gè)構(gòu)造方法即可:
- if let
- URL = NSURL(string: avatarURLString),
- data = NSData(contentsOfURL: URL),
- image = UIImage(data: data) {
- // TODO
- }
假如某個(gè)列表里有 5 個(gè)條目都是同一個(gè)用戶產(chǎn)生的,那這個(gè)用戶的頭像要同時(shí)顯示 5 次,我們難道要下載 5 次嗎?
為了解決這個(gè)問題,我們可以將獲取頭像這一行為當(dāng)作一個(gè)“請求”:
- typealias Completion = UIImage -> Void
- struct Request: Equatable {
- let avatarURLString: String
- let completion: Completion
- }
我們可將請求用一個(gè)數(shù)組(即請求池)紀(jì)錄下來:
- var requests: [Request]
這樣,每次要下載某個(gè)頭像時(shí),構(gòu)造一個(gè)請求。先將其放入請求池,然后檢查請求池中包含此 avatarURLString 的請求的個(gè)數(shù),如果數(shù)量大于1,那說明之前已經(jīng)有過同樣的請求,我們就不執(zhí)行下載的操作,靜靜等待***個(gè)請求的幫助。
Navi AvatarPod
過一會(huì)兒,之前的請求下載結(jié)束,那這時(shí)只需要執(zhí)行全部有同樣 avatarURLString 的請求的 completion,后一個(gè)(或幾個(gè))就“免費(fèi)”得到了服務(wù)。
Navi Completion
以上是初次需要下載的情況。若我們已經(jīng)下載了頭像圖片,我們依然可以構(gòu)造請求,只不過之后就不執(zhí)行下載操作,而是去文件系統(tǒng)里尋找而已。同理,我們的請求里可以包含樣式,假如同一個(gè)用戶的五個(gè)頭像有不同的樣式,我們只需要在***執(zhí)行每個(gè)請求的 completion 時(shí)再分別處理樣式。
既然下載后要保存,那怎么保存比較好呢?
有人傾向于直接用文件系統(tǒng)的 API 將數(shù)據(jù)存在文件系統(tǒng)里;有人已經(jīng)使用的 Core Data,那他會(huì)傾向于放在 Core Data 里,況且 Core Data 實(shí)體的某些屬性類型有 External 特性,勾選后,既不占用內(nèi)存空間,也免去了直接操作文件系統(tǒng)的繁瑣;還有人使用 Realm 或直接用 SQLite 等,反正都有類似的存儲(chǔ)過程,只不過細(xì)節(jié)不同。
我的建議是將原始圖片保存在文件系統(tǒng)中(或 Core Data 勾選 External),小的有樣式頭像可以直接以屬性保存在數(shù)據(jù)庫中(通常例如 Core Data 或 Realm 都支持 NSData 的屬性,但不能過大,因?yàn)樗鼈儠?huì)待在內(nèi)存里)。
可惜,如果現(xiàn)在我們要制作一個(gè)框架來緩存頭像,那我們就不可能非常具體地去幫用戶存儲(chǔ)文件。因?yàn)榧?xì)節(jié)千差萬別,實(shí)在顧不過來。而且就算有了,用戶也很難集成這一步到已有代碼。若不考慮用戶的喜好,那我們實(shí)際上做出的是一個(gè)“通用”的圖片緩存系統(tǒng)。這里的通用以“不夠優(yōu)化”來理解。
好在,我們可以定義協(xié)議,在協(xié)議中聲明存儲(chǔ) API,用戶自行實(shí)現(xiàn)存儲(chǔ)過程。我們的緩存系統(tǒng)只需要調(diào)用 API 即可,不用關(guān)心存儲(chǔ)過程的細(xì)節(jié)。
再把頭像樣式考慮進(jìn)去,于是有:
- protocol Avatar {
- var URL: NSURL { get }
- var style: AvatarStyle { get }
- var placeholderImage: UIImage? { get }
- var localOriginalImage: UIImage? { get }
- var localStyledImage: UIImage? { get }
- func saveOriginalImage(originalImage: UIImage, styledImage: UIImage)
- }
稍微說明一下:URL 自然表示頭像的原始鏈接;style 表示本次要顯示的頭像的樣式,稍微會(huì)進(jìn)一步討論;placeholderImage 很好理解,在顯示真正的頭像之前,需要一個(gè)占位符,不同的 App 有不同的設(shè)計(jì),所以也交給用戶;localOriginalImage 表示本地存儲(chǔ)的原始圖片,既然存儲(chǔ)已由用戶控制,那讀取自然由用戶控制;localStyledImage 表示本地有樣式的頭像,用戶可以根據(jù)樣式的不同來提供;***是saveOriginalImage(originalImage:styledImage:),由用戶控制存儲(chǔ)過程,包括原始圖片以及本次的樣式圖片。
其中,頭像樣式大概可以做成下面這樣:
- enum AvatarStyle: Equatable {
- case Original
- case Rectangle(size: CGSize)
- case RoundedRectangle(size: CGSize, cornerRadius: CGFloat, borderWidth: CGFloat)
- typealias Transform= UIImage -> UIImage?
- case Free(name: String, transform: Transform)
- }
有原始樣式(不處理)、固定尺寸的矩形(可生成正方形等)、可帶透明邊的圓角矩形(可生產(chǎn)圓形等),以及一種自由樣式,由用戶自行提供圖片變換函數(shù)??雌饋肀容^齊備,也可再增加。
然后我們再給 UIImageView 擴(kuò)展一個(gè)方法:
- extension UIImageView {
- func navi_setAvatar(avatar: Avatar) {
- // TODO
- }
- }
以方便用戶使用。***的圖解如下:
Navi 的緩存架構(gòu)
用戶有列表要顯示,列表?xiàng)l目包含頭像,于是構(gòu)造一個(gè) Avatar 的描述去 AvatarPod 中喚醒 Avatar。
AvatarPod 避免重復(fù)下載,并實(shí)現(xiàn)整個(gè)邏輯。比如先去內(nèi)存緩存中查詢,沒有就看看是否用戶提供了本地圖片,再?zèng)]有就去下載。下載好了就處理樣式,并將原圖和樣式圖交給用戶存儲(chǔ)。
那么在具體的集成上,用戶只需要讓其 User 實(shí)現(xiàn) Avatar 協(xié)議或者構(gòu)造一個(gè)新的“中間對象”,其包含 User 和 AvatarStyle,并讓此對象實(shí)現(xiàn) Avatar 協(xié)議。我推薦用中間對象的方式,這樣對已有代碼的修改會(huì)很少,而且更好支持多種樣式。
具體請看 Navi 的代碼以及 Demo。文件并不多,應(yīng)該能比較容易地看明白。不過運(yùn)行 Demo 需要 iOS 設(shè)備已在“設(shè)置”里登錄了 Twitter 帳戶,沒有 Twitter 帳號(hào)的同學(xué)可先去 twitter.com 注冊一個(gè)。
***,Navi 的名字來源于電影《阿凡達(dá)》里的納美人(Na'vi),為潘多拉星球上的智慧類人生物。人類到達(dá)后潘多拉后,利用基因改造合成了一種可以由人控制的類似那美人的生物,也就是 Avatar(意為化身或替身),以便更好地和原住的納美人交流。