有时候简单写一个demo需要对UIImageView来一个根据url设置image的操作,单独pod install一下或者找个spm感觉太麻烦,隧根据URLSession写一个简单的extension,如下:
extension UIImageView {func sg_setImage(_ urlString: String, placeholderName placeholder: String? = nil) {if let placeholder = placeholder, let placeholderImage = UIImage(named: placeholder) {self.image = placeholderImage}DispatchQueue.global().async {guard let url = URL(string: urlString) else { return }URLSession.shared.dataTask(with: url) { data, response, error inif let data = data, let image = UIImage(data: data) {DispatchQueue.main.async {self.image = image}} else {print(error ?? "")}}.resume()}}}
使用时,调用UIImageView实例对象的sg_setImage()
方法即可,还额外实现了placeholder功能。
但是每次调用都会重新请求一下,如果一个CollectionView里面的cell有不少内容是重复的,那岂不是做了重复的工作?这是不可接受的,因此,自然而言地想到了使用hash字典来充当缓存。
因为加载重复image的首要条件就是url相同,那么就可以把url当作hash字段的key,这样,每次调用sg_setImage()
方法时候根据url作为key先判断缓存里面是否存在image对象,如果有,直接显示即可,而没有的话,先走placeholder的路径,然后异步执行Session的任务,下载完后将image对象放入缓存里面,再设置image即可。内容如下:
extension UIImageView {private static var imageCaches: [String: Any] = { [: ] }()func sg_setImage(_ urlString: String, placeholderName placeholder: String? = nil) {if UIImageView.imageCaches[urlString] != nil {self.image = UIImageView.imageCaches[urlString] as? UIImagereturn}if let placeholder = placeholder, let placeholderImage = UIImage(named: placeholder) {self.image = placeholderImage}DispatchQueue.global().async {guard let url = URL(string: urlString) else { return }URLSession.shared.dataTask(with: url) { data, response, error inif let data = data, let image = UIImage(data: data) {DispatchQueue.main.async {UIImageView.imageCaches[urlString] = imageself.image = image}} else {print(error ?? "")}}.resume()}}}
然而随着深入使用发现了另一个问题,每个不同url的请求都会将image放入缓存中,那要不了多久内存就会被撑爆,这同样也是不可接受的。在<<操作系统>>课程里面有个很经典的算法叫做LRU(Latest Recently Used),叫做“最近最久使用”。该算法就是解决页面里面置换问题。操作系统里面的页面置换是有数组来管理的(实际上比数组复杂,可能为双向链表),那这里也可以用LRU去管理,YYCache我记得也是有LRU的应用的,这里不再赘述。
extension UIImageView {private static var sgImageCaches: Cache = { Cache(limitCount: 100) }()func sg_setImage(_ urlString: String, placeholderName placeholder: String? = nil) {if UIImageView.sgImageCaches.getObject(forKey: urlString) != nil {self.image = UIImageView.sgImageCaches.getObject(forKey: urlString) as? UIImagereturn}if let placeholder = placeholder, let placeholderImage = UIImage(named: placeholder) {self.image = placeholderImage}DispatchQueue.global().async {guard let url = URL(string: urlString) else { return }URLSession.shared.dataTask(with: url) { data, response, error inif let data = data, let image = UIImage(data: data) {DispatchQueue.main.async {UIImageView.sgImageCaches.setObject(image, forKey: urlString)self.image = image}} else {print(error ?? "")}}.resume()}}}class Node: NSObject {// 上一个节点var pre: Node?// 下一个节点var next: Node?var key: AnyHashablevar value: Anyinit(value: Any, key: AnyHashable) {self.value = valueself.key = keysuper.init()}override var description: String {return "\(key):\(value)"}
}class LinkMap: NSObject {var headNode: Node?var tailNode: Node?var dict = [AnyHashable: Node]()var totalCount: UInt64 = 0/// 插入新元素////// - Parameter node: 元素节点func insert(_ node: Node) {totalCount += 1dict[node.key] = nodeif let head = headNode {node.next = headhead.pre = node// 重新赋值头节点headNode = node} else {headNode = nodetailNode = node}}/// 删除元素////// - Parameter node: 元素节点func removeNode(_ node: Node) {totalCount -= 1dict.removeValue(forKey: node.key)if let _ = node.pre {node.pre?.next = node.next}if let _ = node.next {node.next?.pre = node.pre}if headNode == node {headNode = node.next}if tailNode == node {tailNode = node.pre}}/// 把当前节点移动到首部////// - Parameter node: 元素节点func moveNodeToHead(_ node: Node) {if headNode == node {return}// 删除当前节点if tailNode == node {tailNode = node.pretailNode?.next = nil} else {node.next?.pre = node.prenode.pre?.next = node.next}// 把当前节点移动到头部node.next = headNodenode.pre = nilheadNode?.pre = node// 重新赋值头节点headNode = node}/// 删除尾节点func removeTailNode() -> Node? {totalCount -= 1if let tail = tailNode {let key = tail.keydict.removeValue(forKey: key)}if headNode == tailNode {return nil} else {tailNode = tailNode?.pretailNode?.next = nilreturn tailNode}}/// 删除所有元素节点func removeAllNode() {totalCount = 0headNode = niltailNode = nildict = [AnyHashable: Node]()}
}class Cache: NSObject {var lru = LinkMap()var lock = NSLock()let limitCount: UInt64init(limitCount: UInt64 = 100) {self.limitCount = limitCountsuper.init()}func getObject(forKey key: AnyHashable) -> Any? {lock.lock()var node: Node?node = lru.dict[key]if let node = node {lru.moveNodeToHead(node)}lock.unlock()return node?.value}func setObject(_ value: Any, forKey key: AnyHashable) {lock.lock()if let node = lru.dict[key] {// 存在节点,则把节点移到头部// 如果值不相等,则把新值替换进去node.value = valuelru.moveNodeToHead(node)} else {// 不存在节点,则插入新的节点let node = Node(value: value, key: key)lru.insert(node)}if lru.totalCount > limitCount {// 数量超过限制,则删除尾节点let _ = lru.removeTailNode()}lock.unlock()}func removeObjc(forKey key: AnyHashable) {lock.lock()if let node = lru.dict[key] {lru.removeNode(node)}lock.unlock()}override var description: String {var description = ""var node: Node? = lru.headNodewhile let current = node {description.append("\(current.description) ")node = current.next}return description}
}
这里说明一下:LRU的实现从网上直接copy其他博主的文章过来的,本来想放到链接但没找到
有了LRU的实现,就可以把普通的hash字典换成LRU缓存了,在设置完指定最大容量后,新的url请求过来就会把最久远未使用的image对象移除缓存,达到了动态使用、动态平衡的结果。
上一篇:Shell命令——date的用法