有时候简单写一个demo需要对UIImageView来一个根据url设置image的操作,单独pod install一下或者找个spm感觉太麻烦,隧根据URLSession写一个简单的extension,如下:
直接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 in
if 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 + 字典缓存
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? UIImage
return
}
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 in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
UIImageView.imageCaches[urlString] = image
self.image = image
}
} else {
print(error ?? "")
}
}.resume()
}
}
}
然而随着深入使用发现了另一个问题,每个不同url的请求都会将image放入缓存中,那要不了多久内存就会被撑爆,这同样也是不可接受的。在<<操作系统>>课程里面有个很经典的算法叫做LRU(Latest Recently Used),叫做“最近最久使用”。该算法就是解决页面里面置换问题。操作系统里面的页面置换是有数组来管理的(实际上比数组复杂,可能为双向链表),那这里也可以用LRU去管理,YYCache我记得也是有LRU的应用的,这里不再赘述。
extension + 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? UIImage
return
}
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 in
if 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: AnyHashable
var value: Any
init(value: Any, key: AnyHashable) {
self.value = value
self.key = key
super.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 += 1
dict[node.key] = node
if let head = headNode {
node.next = head
head.pre = node
// 重新赋值头节点
headNode = node
} else {
headNode = node
tailNode = node
}
}
/// 删除元素
///
/// - Parameter node: 元素节点
func removeNode(_ node: Node) {
totalCount -= 1
dict.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.pre
tailNode?.next = nil
} else {
node.next?.pre = node.pre
node.pre?.next = node.next
}
// 把当前节点移动到头部
node.next = headNode
node.pre = nil
headNode?.pre = node
// 重新赋值头节点
headNode = node
}
/// 删除尾节点
func removeTailNode() -> Node? {
totalCount -= 1
if let tail = tailNode {
let key = tail.key
dict.removeValue(forKey: key)
}
if headNode == tailNode {
return nil
} else {
tailNode = tailNode?.pre
tailNode?.next = nil
return tailNode
}
}
/// 删除所有元素节点
func removeAllNode() {
totalCount = 0
headNode = nil
tailNode = nil
dict = [AnyHashable: Node]()
}
}
class Cache: NSObject {
var lru = LinkMap()
var lock = NSLock()
let limitCount: UInt64
init(limitCount: UInt64 = 100) {
self.limitCount = limitCount
super.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 = value
lru.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.headNode
while let current = node {
description.append("\(current.description) ")
node = current.next
}
return description
}
}
这里说明一下:LRU的实现从网上直接copy其他博主的文章过来的,本来想放到链接但没找到
有了LRU的实现,就可以把普通的hash字典换成LRU缓存了,在设置完指定最大容量后,新的url请求过来就会把最久远未使用的image对象移除缓存,达到了动态使用、动态平衡的结果。