BBWebImage 设计思路

By | 2019年8月26日

BBWebImage 设计思路

BBWebImage 是高性能 Swift 图片组件,用于图片下载、缓存、编解码、编辑与展示。

GitHub 地址: https://github.com/Silence-GitHub/BBWebImage

效果图

下载、展示并缓存原图

下载、渐进式解码、编辑图片,缓存编辑后的图片至内存 (Memory)、缓存原图数据至磁盘 (Disk)

  • 添加滤镜

  • 绘制圆角、边框

性能对比

测试的图片组件有 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 的内存缓存与磁盘缓存速度很快,针对缩略图的优势明显

  • 加载展示 GIF,BBWebImage 占用很少的 CPU 和内存

为什么写这个图片组件

写 BBWebImage 最开始的目的是要解决现有图片组件中图片编辑与动图的问题。

做过的项目中,图片组件都主要用 SDWebImage,显示 WebP、APNG 等格式动图用 YYWebImage。这些图片组件都非常优秀,能满足大多数使用场景的需求。YYWebImage 支持的图片格式很多,但是功能和可自定义程度不如 SDWebImage (例如自定义图片解码器)。当 BBWebImage 第一版 0.1.0 发布时,SDWebImage 的最新正式版是 4.4.3,还没有图片编辑模块。有些时候需要展示编辑后的图片,例如添加滤镜、绘制圆角和边框 (防止 CALayer 设置圆角造成顿卡) 等,也需要缓存编辑后的图片。如果用 SDWebImage 下载图片并编辑,会有以下问题:

  1. 如果只缓存编辑后的图片,则展示原图需要再次下载。
  2. 假设原图数据缓存至磁盘。如果不缓存编辑后的图片,需要在每次展示前重复编辑原图这个步骤。如果把编辑后的图片缓存至内存和磁盘,为了与原图区分,需要维护 cache key (在缓存中一个 key 对应原图,另一个 key 对应编辑后的图片)。如果把编辑后的图片只缓存至内存,则为了区分从缓存中取出的图片是否经过编辑,需要判断是从内存还是磁盘取到的图片。
  3. 如果用 Core Graphics 框架编辑图片,SDWebImage 的图片解压缩 (Decompress) 是不必要的。编辑和解压缩步骤类似:创建 CGContext、绘制图片、创建新图片。需要在编辑前禁用图片解压缩,完成后再启用。

另外,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

BBImageCache 是图片缓存协议,定义向缓存存取图片的行为。BBLRUImageCache 是默认使用的缓存,遵循 BBImageCache 协议。BBLRUImageCache 里面有内存缓存与磁盘缓存,都采用 LRU 算法 (Least recently used)。这部分设计基本参照 YYCache。内存缓存用字典和双向链表实现 LRU 算法。磁盘缓存用 SQLite 数据库存储数据相关信息 (key、大小、更新时间等),二进制数据本身根据文件大小来决定存储至 SQLite 数据库或者直接写入沙盒目录。

往内存缓存中保存的是 UIImage,取出的也是 UIImage。往磁盘缓存中存储的是 Data 或者是 UIImage,后者会被编码成 Data;取出的只是 Data,这里不会进行解码 (BBWebImageManager 拿到数据,才会用 BBImageCoderManager 进行解码)。

如果默认缓存无法满足需求,可以自定义缓存,遵循 BBImageCache 协议,替换默认缓存。

BBImageDownloader

BBImageDownloader 是图片下载协议,定义图片下载行为。BBMergeRequestImageDownloader 是默认使用的下载器,遵循 BBImageDownloader 协议。BBMergeRequestImageDownloader 会合并对同一 URL 的网络请求,防止对同一 URL 发出重复请求。每一个下载任务封装成 BBImageDownloadTask (是个协议,默认实现是 BBImageDefaultDownloadTask,可自定义实现) ,包含这次下载任务的完成回调等信息。每一个 URL 网络请求 (以下称为 "下载操作") 封装成 BBImageDownloadOperation (也是协议,默认实现是 BBMergeRequestImageDownloadOperation,可自定义实现),包含至少一个下载任务。

下载操作的执行顺序是,一般操作 (下载图片后要立即使用) 优先于预加载操作 (图片不需要在下载后立即使用,只是下载存入缓存),同时先进先出,也就是老的操作优先执行。虽然 SDWebImage 提供了后进先出和设置优先级的功能,但在做过的项目中并没有用到。因此这里没有设计这些功能,以后需要的话可以加上。实现方法原来是用自带的 Operation 和 OperationQueue 实现,但后来想把这一部分也自定义,于是用字典和双向链表实现。一共有两组字典和双向链表的组合,一组代表一般操作队列,另一组代表预加载操作队列。最多同时执行操作数为 6 个。操作数少于 6,有新操作进来就执行;大于等于 6,把新操作插入相应队列尾部。一个操作结束后,先从一般操作队列头部取一般操作来执行,没有的话再从预加载操作队列头部取预加载操作来执行。预加载操作还可以升级为一般操作。如果前面有预加载任务,并且相应的预加载操作进入预加载操作队列等待,后来有一般下载任务是相同的 URL,则之前的预加载操作会被移出预加载操作队列,进入一般操作操作队列而升级为一般操作,把后来的一般下载任务合并进来。

如果需要自定义图片下载行为,例如 MD5 校验等,可以考虑自定义下载任务 (遵循 BBImageDownlaodTask 协议) 或下载操作 (遵循 BBImageDownloadOperation 协议),甚至自定义整个下载器 (遵循 BBImageDownloader 协议)。

BBImageCoderManager

BBImageCoder 是图片编解码协议,定义图片编解码行为。BBImageCoderManager 遵循 BBImageCoder 协议,包含至少一个编解码器 (也遵循 BBImageCoder 协议)。用 BBImageCoderManager 来编解码时,BBImageCoderManager 会遍历其中的编解码器,尝试找到一个能完成操作的编解码器。这个图片组件中的所有图片编解码操作 (包括静图、渐进式解码、动图) 都由遵循 BBImageCoder 协议的编解码器完成,可以通过自定义编解码器来自定义编解码行为,支持不同格式的图片。

BBWebImageEditor

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

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 设置圆角和边框造成顿卡。

UIImageView 扩展

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

转载请注明出处:https://www.cnblogs.com/silence-cnblogs/p/10442984.html

发表评论