BBWebImage 是高性能 Swift 圖片組件,用於圖片下載、緩存、編解碼、編輯與展現。html
GitHub 地址: https://github.com/Silence-GitHub/BBWebImagegit
下載、展現並緩存原圖github
下載、漸進式解碼、編輯圖片,緩存編輯後的圖片至內存 (Memory)、緩存原圖數據至磁盤 (Disk)web
測試的圖片組件有 BBWebImage (1.1.0)、SDWebImage (4.4.6 以及 FLAnimatedImage 1.0.12 用於測試 GIF)、YYWebImage (1.0.5) 和 Kingfisher (4.10.1)。測試設備是 iPhone 7,iOS 12.1。算法
寫 BBWebImage 最開始的目的是要解決現有圖片組件中圖片編輯與動圖的問題。數據庫
作過的項目中,圖片組件都主要用 SDWebImage,顯示 WebP、APNG 等格式動圖用 YYWebImage。這些圖片組件都很是優秀,能知足大多數使用場景的需求。YYWebImage 支持的圖片格式不少,可是功能和可自定義程度不如 SDWebImage (例如自定義圖片解碼器)。當 BBWebImage 初版 0.1.0 發佈時,SDWebImage 的最新正式版是 4.4.3,尚未圖片編輯模塊。有些時候須要展現編輯後的圖片,例如添加濾鏡、繪製圓角和邊框 (防止 CALayer 設置圓角形成頓卡) 等,也須要緩存編輯後的圖片。若是用 SDWebImage 下載圖片並編輯,會有如下問題:緩存
另外,SDWebImage 的圖片降採樣 (Downsample) 用了統一處理的方式,圖片分辨率大於固定閾值是降採樣的必要條件。問題就在於閾值是固定的,遇到多張大圖的狀況,這個閾值仍是太大,致使內存佔用過多而崩潰。若是圖片組件中有圖片編輯模塊,能夠把圖片降採樣放入編輯模塊,就能夠自定義降採樣參數,從而解決內存佔用過多問題。網絡
關於動圖,SDWebImage 用 FLAnimatedImage 來展現 GIF,可是有性能問題。緣由是 FLAnimatedImage 沒有繼承 UIImage,SDWebImage 的解碼器沒法直接返回 FLAnimatedImage,只好在主線程中用圖片數據建立 FLAnimatedImage,這一步阻塞主線程致使頓卡。具體代碼分析和解決方案參見 SDWebImage 加載顯示 GIF 與性能問題。解決方案能用,可是從設計的角度看,SDWebImage 使用 FLAnimatedImage 並不合適。FLAnimatedImage 只適用於 GIF,沒法經過自定義解碼器來支持其餘格式的動圖。理想的狀況是,圖片組件搭建好展現動圖的框架,有經常使用動圖的解碼,能夠自定義解碼器來支持其餘格式的動圖。閉包
BBWebImage 的主要結構能夠看下面這幅圖。BBImageCache 管理圖片緩存,BBImageDownloader 管理圖片下載,BBImageCoderManager 管理圖片編解碼,BBWebImageEditor 提供圖片編輯方法。BBWebImageManager 調用前四者的方法實現相應功能,對外提供一個方法實現圖片加載 (緩存讀取與下載)、解碼、編輯和緩存。UIImageView 的擴展方法調用 BBWebImageManager 的方法獲取圖片用於展現。動圖封裝成 BBAnimatedImage,用 BBAnimatedImageView 展現。架構
BBImageCache 是圖片緩存協議,定義向緩存存取圖片的行爲。BBLRUImageCache 是默認使用的緩存,遵循 BBImageCache 協議。BBLRUImageCache 裏面有內存緩存與磁盤緩存,都採用 LRU 算法 (Least recently used)。這部分設計基本參照 YYCache。內存緩存用字典和雙向鏈表實現 LRU 算法。磁盤緩存用 SQLite 數據庫存儲數據相關信息 (key、大小、更新時間等),二進制數據自己根據文件大小來決定存儲至 SQLite 數據庫或者直接寫入沙盒目錄。
往內存緩存中保存的是 UIImage,取出的也是 UIImage。往磁盤緩存中存儲的是 Data 或者是 UIImage,後者會被編碼成 Data;取出的只是 Data,這裏不會進行解碼 (BBWebImageManager 拿到數據,纔會用 BBImageCoderManager 進行解碼)。
若是默認緩存沒法知足需求,能夠自定義緩存,遵循 BBImageCache 協議,替換默認緩存。
BBImageDownloader 是圖片下載協議,定義圖片下載行爲。BBMergeRequestImageDownloader 是默認使用的下載器,遵循 BBImageDownloader 協議。BBMergeRequestImageDownloader 會合並對同一 URL 的網絡請求,防止對同一 URL 發出重複請求。每個下載任務封裝成 BBImageDownloadTask (是個協議,默認實現是 BBImageDefaultDownloadTask,可自定義實現) ,包含此次下載任務的完成回調等信息。每個 URL 網絡請求 (如下稱爲 "下載操做") 封裝成 BBImageDownloadOperation (也是協議,默認實現是 BBMergeRequestImageDownloadOperation,可自定義實現),包含至少一個下載任務。
下載操做的執行順序是,通常操做 (下載圖片後要當即使用) 優先於預加載操做 (圖片不須要在下載後當即使用,只是下載存入緩存),同時先進先出,也就是老的操做優先執行。雖然 SDWebImage 提供了後進先出和設置優先級的功能,但在作過的項目中並無用到。所以這裏沒有設計這些功能,之後須要的話能夠加上。實現方法原來是用自帶的 Operation 和 OperationQueue 實現,但後來想把這一部分也自定義,因而用字典和雙向鏈表實現。一共有兩組字典和雙向鏈表的組合,一組表明通常操做隊列,另外一組表明預加載操做隊列。最多同時執行操做數爲 6 個。操做數少於 6,有新操做進來就執行;大於等於 6,把新操做插入相應隊列尾部。一個操做結束後,先從通常操做隊列頭部取通常操做來執行,沒有的話再從預加載操做隊列頭部取預加載操做來執行。預加載操做還能夠升級爲通常操做。若是前面有預加載任務,而且相應的預加載操做進入預加載操做隊列等待,後來有通常下載任務是相同的 URL,則以前的預加載操做會被移出預加載操做隊列,進入通常操做操做隊列而升級爲通常操做,把後來的通常下載任務合併進來。
若是須要自定義圖片下載行爲,例如 MD5 校驗等,能夠考慮自定義下載任務 (遵循 BBImageDownlaodTask 協議) 或下載操做 (遵循 BBImageDownloadOperation 協議),甚至自定義整個下載器 (遵循 BBImageDownloader 協議)。
BBImageCoder 是圖片編解碼協議,定義圖片編解碼行爲。BBImageCoderManager 遵循 BBImageCoder 協議,包含至少一個編解碼器 (也遵循 BBImageCoder 協議)。用 BBImageCoderManager 來編解碼時,BBImageCoderManager 會遍歷其中的編解碼器,嘗試找到一個能完成操做的編解碼器。這個圖片組件中的全部圖片編解碼操做 (包括靜圖、漸進式解碼、動圖) 都由遵循 BBImageCoder 協議的編解碼器完成,能夠經過自定義編解碼器來自定義編解碼行爲,支持不一樣格式的圖片。
BBWebImageEditor 是個結構體,包含一個字符串 key 和一個閉包 edit。閉包 edit 輸入一個 UIImage, 輸出一個 UIImage,用於編輯圖片。字符串 key 做爲圖片編輯方法的惟一標識符,將與 edit 輸出的 UIImage 動態關聯 (經過擴展屬性 bb_imageEditKey 來訪問,如下稱爲 「edit key」)。例如,定義一個添加濾鏡的圖片編輯器,edit 是添加濾鏡閉包,key 是 "filter",編輯後的圖片的 edit key 是 "filter";定義一個繪製圓角的圖片編輯器,edit 是繪製圓角閉包,key 是 "roundedCorner",編輯後的圖片的 edit key 是 "roundedCorner"。原圖的 edit key 爲 nil。經過圖片的 edit key 就能夠知道圖片是原圖仍是某個編輯器編輯後的圖片。
BBWebImageManager 對外提供加載圖片的方法 loadImage(with:)
。與 SDWebImage 相似,先在內存緩存中找圖片,沒有的話找磁盤緩存,若是沒有就下載並緩存圖片。不一樣的是,圖片解壓縮在這一層才執行 (SDWebImage 在 cache 和 download operation 中都有執行),並且這裏還有圖片編輯步驟。loadImage(with:)
方法的 editor 參數是 BBwebImageEditor? 類型,傳 nil 表示須要原圖,傳某個編輯器表示須要用原圖進行編輯。若是從內存緩存中取到圖片,須要經過 edit key 判斷圖片的編輯狀態 (原圖、或者被編輯),決定後續步驟 (直接使用圖片,直接編輯圖片,須要從磁盤緩存或網絡獲取圖片)。若是傳入了編輯器做爲方法參數,則不進行圖片解壓縮,解壓縮由編輯器負責。原圖數據保存至磁盤緩存,原圖或編輯後的圖片保存至內存緩存,經過圖片的 edit key 來區分編輯狀態。
這個圖片組件內置的圖片編輯器中有一個比較經常使用,經過 bb_imageEditorCommon(with:)
方法建立,傳入的參數有 imageView 的大小和 contentMode、指望最大分辨率、圓角位置和圓角半徑、邊框寬度和顏色、背景色。編輯器裁剪圖片,只保留 imageView 顯示的部分;根據 imageView 的大小與指望最大分辨率計算降採樣分辨率閾值,若是原圖分辨率大於閾值就會進行降採樣;繪製圓角、邊框、背景色。能夠用這個圖片編輯器自定義降採樣分辨率閾值,防止內存佔用過多;繪製好圓角和邊框,防止 CALayer 設置圓角和邊框形成頓卡。
BBWebCache 是圖片加載協議,定義圖片加載行爲。默認實現了圖片加載方法 bb_setImage(with:)
(如下稱爲 "協議加載方法"),用 BBWebImageManager 的單例加載圖片,動態關聯 BBWebCacheOperation 對象用於訪問圖片加載任務 (方便之後取消任務)。UIImageView 遵循 BBWebCache 協議,加載 image 和 highlightedImage 的擴展方法,都直接調用協議加載方法,只是傳入的參數有所不一樣。與此相似,UIButton、CALayer、MKAnnotationView 都有相應的擴展方法用於加載圖片,也是直接調用協議加載方法。若是有自定義的 view 甚至 object 須要加載圖片,也能夠遵循 BBWebCache 協議,調用協議加載方法來實現加載圖片的擴展方法。
動圖封裝成 BBAnimatedImage,繼承 UIImage。初始化方法除了圖片數據還有動圖解碼器,若是沒有指定解碼器,則從 BBWebImageManager 單例的 BBImageCoderManager 的解碼器中尋找,有合適的解碼器才能初始化動圖。動圖向解碼器獲取每一幀圖片以及動畫時間等信息,並管理圖片幀的緩存。根據總內存容量、可用內存容量來動態計算最大緩存容量,以此來清除暫時不用的圖片幀同時保存將要展現的圖片幀。也支持自定義最大緩存容量。動圖有 bb_editor 屬性,是 BBWebImageEditor? 類型。用某個圖片編輯器給這個屬性賦值,則會對圖片幀進行編輯。這個屬性默認爲空,表示使用原始圖片幀。
BBAnimatedImageView 繼承 UIImageView,用來展現動圖。用 CADisplayLink 播放動畫。屏幕刷新時,向動圖獲取當前要展現的圖片幀。這裏只從緩存的圖片幀中獲取,避免解碼阻塞主線程。同時,告訴動圖下一幀要展現的圖片是第幾幀,由動圖進行後臺解碼。動圖會在 App 進入後臺時、從 imageView 上移除時、以及收到內存警告時,清除緩存的圖片幀。
BBAnimatedImageView 除了展現動圖,也能夠展現靜圖。它自己就繼承 UIImageView,能夠看成普通的 UIImageView 來用。BBAnimatedImage 自己繼承 UIImage,與編解碼協議 BBImageCoder 相符,能夠在解碼器中解碼出來,這一點與普通的靜圖相同,不像 SDWebImage + FLAnimatedImage 那樣靜圖與 GIF 不相符 (致使要對 GIF 特殊處理)。在這個框架基礎上,經過自定義圖片編解碼器就能夠支持其餘格式的動圖 (固然也能夠支持其餘格式的靜圖,只是這部分在講動圖)。
BBWebImage 的圖片緩存、下載、編解碼、編輯功能均可以自定義。把動圖封裝成繼承 UIImage 的類,用繼承 UIImageView 的類進行展現,支持編輯動圖的圖片幀。能夠自定義編解碼器支持其餘格式的圖片。如今 BBWebImage 搭建了框架,以後會逐步完善細節。若是有編輯靜圖或動圖的需求,或者其餘相關需求,能夠嘗試 BBWebImage。源碼及使用方法見 GitHub: https://github.com/Silence-GitHub/BBWebImage