许多 App 都有用户系统,不论是自己实现还是使用第三方,大概都需要显示用户的头像。比较常见的情景下,头像会在某些列表里出现,例如联系人列表、消息列表等。
虽然头像也是图像,但相比于普通图片,我们对头像有更高的要求。
头像的原始图片可能有各种尺寸,但在 App 里,我们很可能需要某种固定样式的头像,例如正方形或者圆形。如果我们使用通用的图片缓存工具如 SDWebImage、Kingfisher 等,那么还需要自己做图片的裁剪和加工。如果直接用 UIImageView 来缩小,图片细节就会变得过于“锐利”,影响观看。
进一步,一个 App 里可能不只有一种头像样式。比如某些场景里要有大头像,某些要用小头像,某些要用原始尺寸的头像;或者某些场景里要用正方形头像,某些场景里要用圆角矩形头像,不一而足。注意:单纯用 layer.cornerRadius 或 CALayer 来 mask 都会导致列表的滑动性能问题,因此不考虑。
如果要做优化,我们当然希望这些不同样式的小头像能够存储在本地,不用再从网络获取再裁减或处理样式。一来减少不需要的流量消耗,二来提高头像载入速度,用户体验自然会更好。
基于上述分析,我们来设计一个头像缓存系统。它的目标是快速地获取并缓存有样式的头像图片,并能比较容易地集成到已有项目中。
一些前提:
头像的图片 URL 唯一,即不同的头像有不同的 URL。如果用户换了头像,那么新头像 URL 和旧的不一样;
头像是公开资源,不需要做验证即可下载。
好了,我们来做一做思维游戏。
首先,已有的用户模型可能为:
- struct User {
- let userID: String
- let username: String
- let avatarURLString: String
- //...
- }
其中 avatarURLString 表示远端的头像链接。如果我们要下载它,最简单的话,直接用 NSData 的一个构造方法即可:
- if let
- URL = NSURL(string: avatarURLString),
- data = NSData(contentsOfURL: URL),
- image = UIImage(data: data) {
- // TODO
- }
假如某个列表里有 5 个条目都是同一个用户产生的,那这个用户的头像要同时显示 5 次,我们难道要下载 5 次吗?
为了解决这个问题,我们可以将获取头像这一行为当作一个“请求”:
- typealias Completion = UIImage -> Void
- struct Request: Equatable {
- let avatarURLString: String
- let completion: Completion
- }
我们可将请求用一个数组(即请求池)纪录下来:
- var requests: [Request]
这样,每次要下载某个头像时,构造一个请求。先将其放入请求池,然后检查请求池中包含此 avatarURLString 的请求的个数,如果数量大于1,那说明之前已经有过同样的请求,我们就不执行下载的操作,静静等待***个请求的帮助。
Navi AvatarPod
过一会儿,之前的请求下载结束,那这时只需要执行全部有同样 avatarURLString 的请求的 completion,后一个(或几个)就“免费”得到了服务。
Navi Completion
以上是初次需要下载的情况。若我们已经下载了头像图片,我们依然可以构造请求,只不过之后就不执行下载操作,而是去文件系统里寻找而已。同理,我们的请求里可以包含样式,假如同一个用户的五个头像有不同的样式,我们只需要在***执行每个请求的 completion 时再分别处理样式。
既然下载后要保存,那怎么保存比较好呢?
有人倾向于直接用文件系统的 API 将数据存在文件系统里;有人已经使用的 Core Data,那他会倾向于放在 Core Data 里,况且 Core Data 实体的某些属性类型有 External 特性,勾选后,既不占用内存空间,也免去了直接操作文件系统的繁琐;还有人使用 Realm 或直接用 SQLite 等,反正都有类似的存储过程,只不过细节不同。
我的建议是将原始图片保存在文件系统中(或 Core Data 勾选 External),小的有样式头像可以直接以属性保存在数据库中(通常例如 Core Data 或 Realm 都支持 NSData 的属性,但不能过大,因为它们会待在内存里)。
可惜,如果现在我们要制作一个框架来缓存头像,那我们就不可能非常具体地去帮用户存储文件。因为细节千差万别,实在顾不过来。而且就算有了,用户也很难集成这一步到已有代码。若不考虑用户的喜好,那我们实际上做出的是一个“通用”的图片缓存系统。这里的通用以“不够优化”来理解。
好在,我们可以定义协议,在协议中声明存储 API,用户自行实现存储过程。我们的缓存系统只需要调用 API 即可,不用关心存储过程的细节。
再把头像样式考虑进去,于是有:
- 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 表示本次要显示的头像的样式,稍微会进一步讨论;placeholderImage 很好理解,在显示真正的头像之前,需要一个占位符,不同的 App 有不同的设计,所以也交给用户;localOriginalImage 表示本地存储的原始图片,既然存储已由用户控制,那读取自然由用户控制;localStyledImage 表示本地有样式的头像,用户可以根据样式的不同来提供;***是saveOriginalImage(originalImage:styledImage:),由用户控制存储过程,包括原始图片以及本次的样式图片。
其中,头像样式大概可以做成下面这样:
- 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)
- }
有原始样式(不处理)、固定尺寸的矩形(可生成正方形等)、可带透明边的圆角矩形(可生产圆形等),以及一种自由样式,由用户自行提供图片变换函数。看起来比较齐备,也可再增加。
然后我们再给 UIImageView 扩展一个方法:
- extension UIImageView {
- func navi_setAvatar(avatar: Avatar) {
- // TODO
- }
- }
以方便用户使用。***的图解如下:
Navi 的缓存架构
用户有列表要显示,列表条目包含头像,于是构造一个 Avatar 的描述去 AvatarPod 中唤醒 Avatar。
AvatarPod 避免重复下载,并实现整个逻辑。比如先去内存缓存中查询,没有就看看是否用户提供了本地图片,再没有就去下载。下载好了就处理样式,并将原图和样式图交给用户存储。
那么在具体的集成上,用户只需要让其 User 实现 Avatar 协议或者构造一个新的“中间对象”,其包含 User 和 AvatarStyle,并让此对象实现 Avatar 协议。我推荐用中间对象的方式,这样对已有代码的修改会很少,而且更好支持多种样式。
具体请看 Navi 的代码以及 Demo。文件并不多,应该能比较容易地看明白。不过运行 Demo 需要 iOS 设备已在“设置”里登录了 Twitter 帐户,没有 Twitter 帐号的同学可先去 twitter.com 注册一个。
***,Navi 的名字来源于电影《阿凡达》里的纳美人(Na'vi),为潘多拉星球上的智慧类人生物。人类到达后潘多拉后,利用基因改造合成了一种可以由人控制的类似那美人的生物,也就是 Avatar(意为化身或替身),以便更好地和原住的纳美人交流。
网站标题:大神推荐的头像缓存策略
标题网址:http://www.shufengxianlan.com/qtweb/news30/448230.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联