swift 简化版SDWebImage(造轮子)

1,054 阅读6分钟

已实现

  • 网络请求获取图片
  • 内存缓存图片
  • 磁盘缓存图片
  • 获取磁盘缓存数据大小
  • 删除指定图片的缓存
  • 删除全部图片缓存
  • UIImageView对应分类(支持占位图以及占位图显示模式更改) 其他类要实现设置网络图片请参考UIImageView分类

extension <#类名#>: RBWebImageProtocol {

    func <#方法名#>(url: URL, <#其他参数#>) { 
        // 1.从缓存中获取图片加载
        if let cacheImage = RBWebImageCach.getCacheImage(urlStr: url.absoluteString) {
            // 缓存中找到图片,更新图片
            <#code#>
            return
        }
        // 2.缓存中没有时通过网络请求图片数据
        getData(url: url!) {[weak self] (data) in
            if data == nil {
                // 网络请求到的数据为空
                <#code#>
            }else {
                if let ima = UIImage(data: data!) {
                    // 网络请求到图片
                    <#code#>
                    // 缓存图片data
                    RBWebImageCach.saveImageData(data: data!, urlStr: (url!.absoluteString))
                }else {
                    // 网络请求到的数据转图片失败
                    <#code#>
                }

            }

        }
    }

}

流程

  1. 通过url设置图片
  2. 通过url到缓存中获取图片(现在内存中去找,内存中没有找到再到磁盘中查找)
  3. 缓存找到图片后就直接设置
  4. 缓存中没有找到就通过网络请求图片
  5. 请求到数据后到主线程中去设置图片

主要文件

RBWebImageProtocol

协议,通过对协议的扩展实现了网络获取图片数据的功能

import Foundation
import UIKit

protocol RBWebImageProtocol {
    /// 通过url获取data
    /// - Parameters:
    ///   - url: url
    ///   - completion: 完成后的回调
    func getData(url: URL, completion: ((Data?) -> Void)?)
    
    
}

extension RBWebImageProtocol {
    /// 通过url获取data
    /// - Parameters:
    ///   - url: url
    ///   - completion: 完成后的回调
    func getData(url: URL, completion: ((Data?) -> Void)? = nil) {
        // 创建并行队列异步执行
        DispatchQueue(label: "com.RBWebImage.concurrentQueue",attributes:.concurrent).async {
            do {
                let data: Data = try Data(contentsOf: url)
                if data.count > 0 {
                    completion?(data)
                }else {
                    completion?(nil)
                }
                
            } catch _ {
                completion?(nil)
            }
        }
    }

}

RBWebImageCach

缓存管理类,缓存相关的所有功能

  • RBWebImageMemoryManager 内存缓存管理类
  • RBWebImageDiskManager 磁盘缓存管理类
import Foundation
import UIKit


fileprivate func showLog(_ msg: String) {
    if isShowRBWebImageLog {
        print(msg)
    }
}

class RBWebImageCach {
    
    /// 从缓存中获取图片
    /// - Parameter urlStr: 图片url
    /// - Returns: 缓存中的图片
    class func getCacheImage(urlStr: String) -> UIImage? {
        let key = getKeyString(urlStr: urlStr)
        // 从内存中获取图片
        showLog("将去内存中获取缓存:\(urlStr)")
        if let cacheImage = RBWebImageMemoryManager.shared.getImage(key: key) {
            showLog("内存中找到图片:\(urlStr)")
            return cacheImage
        }
        showLog("内存中没有找到,将去磁盘中获取缓存:\(urlStr)")
        // 内存中没有时,从磁盘中获取图片数据
        if let data = RBWebImageDiskManager.shared.getData(key: key) {
            if let cacheImage = UIImage(data: data) {
                showLog("磁盘中找到图片:\(urlStr)")
                return cacheImage
            }
            
        }
        
        return nil
    }
    
    /// 保存图片数据
    /// - Parameters:
    ///   - data: 图片数据
    ///   - urlStr: 图片url
    class func saveImageData(data: Data, urlStr: String) {
        let key = getKeyString(urlStr: urlStr)
        
        if let image = UIImage(data: data) {
            // 缓存到内存中
            showLog("存储到内存中:\(urlStr)")
            RBWebImageMemoryManager.shared.saveImage(image: image, key: key)
            // 缓存到磁盘中
            showLog("存储到磁盘中:\(urlStr)")
            RBWebImageDiskManager.shared.saveImage(data: data, key: key)
            
        }else {
            showLog("无效数据:\(urlStr)")
        }
        
        
        
    }
    
    /// 删除所有缓存数据
    class func deleteAllCacheData() {
        // 删除内存中的所有缓存数据
        RBWebImageMemoryManager.shared.deleteAllImage()
        // 删除磁盘中所有缓存数据
        RBWebImageDiskManager.shared.deleteAllCacheData()
        
    }
    class func deleteCacheData(urlStr: String) {
        let key = getKeyString(urlStr: urlStr)
        // 删除内存中的缓存数据
        RBWebImageMemoryManager.shared.deleteImage(key: key)
        
        // 删除磁盘中的缓存数据
        RBWebImageDiskManager.shared.deleteCacheData(key: key)
        
    }
    
    /// 获取缓存图片数据的大小
    /// - Returns: 图片数据的大小
    class func getCacheSize() -> Double {
        return RBWebImageDiskManager.shared.getCacheSize()
    }
    
    /// 根据url字符串获取缓存用的key
    /// - Parameter urlStr: url字符串
    /// - Returns: key
    private class func getKeyString(urlStr: String) -> String {
        var key = urlStr
        for s in ":/\\." {
            key = key.replacingOccurrences(of: String(s), with: "_")
        }
        
        return key
    }
    
    
}


/// 内存缓存管理类
class RBWebImageMemoryManager {
    static let shared = RBWebImageMemoryManager()
    private init() {}
    
    private let cache = NSCache<NSString, UIImage>()
    private var keys = Array<NSString>()
    
    
    /// 保存图片到内存中
    /// - Parameters:
    ///   - image: 图片
    ///   - key: key
    func saveImage(image: UIImage, key: String) {
        let nKey = key as NSString
        if !keys.contains(nKey) {
            keys.append(nKey)
        }
        cache.setObject(image, forKey: nKey)
    }
    
    /// 通过key获取内存中的图片
    /// - Parameter key: key
    /// - Returns: 内存中的图片
    func getImage(key: String) -> UIImage? {
        return cache.object(forKey: key as NSString)
    }
    
    /// 删除内存中的所有图片
    func deleteAllImage() {
        cache.removeAllObjects()
        keys.removeAll()
    }
    
    /// 删除key对应的图片
    /// - Parameter key: key
    func deleteImage(key: String) {
        let nKey = key as NSString
        cache.removeObject(forKey: nKey)
    }
    
    
    
}

/// 磁盘缓存管理类
class RBWebImageDiskManager {
    static let shared = RBWebImageDiskManager()
    private init() {}
    
    private let cachePath = "\(NSHomeDirectory())/Library/Caches/BGWebImageCache"// 缓存路径
    
    
    
    /// 保存数据到磁盘中
    /// - Parameters:
    ///   - data: 数据
    ///   - key: key
    func saveImage(data: Data, key: String) {
        let path: String = getFullCachePath(key: key)
        if FileManager.default.createFile(atPath: path, contents: data, attributes: nil) {
            showLog("保存成功")
        }else {
            showLog("保存失败")
            
        }
    }
    
    /// 通过key获取磁盘中的图片数据
    /// - Parameter key: key
    /// - Returns: 磁盘中的图片数据
    func getData(key: String) -> Data? {
        var data:Data?
        let path: String = getFullCachePath(key: key)
        if FileManager.default.fileExists(atPath: path) {
            data = FileManager.default.contents(atPath: path)
        }
        return data
    }
    
    /// 删除磁盘中的所有图片数据
    func deleteAllCacheData() {
        let fileManager: FileManager = FileManager.default
        if fileManager.fileExists(atPath: cachePath) {
            
            do {
                try fileManager.removeItem(atPath: cachePath)
            } catch  {
               showLog("删除磁盘缓存异常")
            }
            
        }
    }
    
    /// 删除key对应的图片数据
    /// - Parameter key: key
    func deleteCacheData(key: String) {
        let path = getFullCachePath(key: key)
        
        let fileManager: FileManager = FileManager.default
        if fileManager.fileExists(atPath: path) {
            do {
                try fileManager.removeItem(atPath: path)
            } catch  {
                showLog("删除磁盘缓存异常")
                
            }
            
        }
    }
    
    /// 获取磁盘中图片的总大小
    /// - Returns: size
    func getCacheSize() -> Double {
        let manage = FileManager.default
        if !manage.fileExists(atPath: cachePath) {
            return 0
        }
        let childFilePath = manage.subpaths(atPath: cachePath)
        var size:Double = 0
        for path in childFilePath! {
            let fileAbsoluePath = cachePath+"/"+path
            size += getFileSize(filePath: fileAbsoluePath)
        }
        return size
    }
    
    
    /// 通过key获取完整的缓存路径
    /// - Parameter key: key
    /// - Returns: 完整缓存路径
    private func getFullCachePath(key: String) -> String {
        let path = "\(cachePath)/\(key)"
        let fileManager: FileManager = FileManager.default
        if !(fileManager.fileExists(atPath: cachePath)) {
            do {
                try fileManager.createDirectory(atPath: cachePath, withIntermediateDirectories: true, attributes: nil)
                return path
            }catch {
                showLog("缓存完整路径设置失败")
                return ""
            }
        }
        return path
    }
    
    /// 获取文件大小
    /// - Parameter filePath: 文件路径
    /// - Returns: 文件大小
    private func getFileSize(filePath: String) -> Double {
        let manager = FileManager.default
        var fileSize:Double = 0
        do {
            let attr = try manager.attributesOfItem(atPath: filePath)
//            fileSize = Double(attr[FileAttributeKey.size] as! UInt64)
            let dict = attr as NSDictionary
            fileSize = Double(dict.fileSize())
        } catch {
            showLog("\(error)")
        }
        return fileSize
    }
    
}

UIImageView+RBWebImage

UIImageView的扩展,实现UIimageView设置网络图片的功能

import UIKit

let isShowRBWebImageLog = true



extension UIImageView: RBWebImageProtocol {
    
    
    /// 设置网络图片
    /// - Parameters:
    ///   - url: url
    ///   - defaultImage: 占位图
    ///   - isKeepOriginal: 在原来有图片时,是否保持原有的图片(true:不替换占位图,false:每次都替换占位图)
    ///   - completion: 完成后回调
    func rbSetImage(url: URL?, defaultImage: UIImage?, isKeepOriginal: Bool = false, completion: ((UIImage?) -> Void)? = nil) {
        showLog("开始设置网络图片:\(url?.absoluteString ?? "")")
        if !isKeepOriginal {
            self.image = nil
        }
        let originImage = self.image
        if originImage == nil && defaultImage != nil {
            self.image = defaultImage!
        }
        if url == nil {
            completion?(nil)
            showLog("设置失败:url空")
            return
        }
        
        // 从缓存中获取图片加载
        if let cacheImage = RBWebImageCach.getCacheImage(urlStr: url!.absoluteString) {
            self.image = cacheImage
            self.showLog("设置成功:\(url!.absoluteString)")
            return
        }
        showLog("缓存中获取图片失败:\(url!.absoluteString)")
        
        // 缓存中没有时通过网络请求图片数据
        showLog("开始通过网络请求图片数据:\(url!.absoluteString)")
        getData(url: url!) {[weak self] (data) in
            if data == nil {
                completion?(nil)
                self?.showLog("设置失败:网络请求到的数据为空-\(url!.absoluteString)")
            }else {
                if let ima = UIImage(data: data!) {
                    // 在主线程更新图片
                    DispatchQueue.main.async {[weak self] in
                        self?.image = ima
                    }
                    // 缓存图片data
                    RBWebImageCach.saveImageData(data: data!, urlStr: (url!.absoluteString))
                    
                    self?.showLog("设置成功:\(url!.absoluteString)")
                    completion?(ima)
                }else {
                    self?.showLog("设置失败:网络请求到的数据转图片失败-\(url!.absoluteString)")
                    completion?(nil)
                }
            }
        }
    }
    
    
    /// 设置网络图片
    /// - Parameters:
    ///   - urlStr: url字符串
    ///   - defaultImageName: 占位图名称
    func rbSetImage(urlStr: String, defaultImageName: String?) {
        var defauleImage: UIImage? = nil
        if defaultImageName != nil {
            defauleImage = UIImage(named: defaultImageName!)
        }
        var url: URL? = nil
        if let us = urlStr.urlString() {
            url = URL(string: us)
        }
        rbSetImage(url: url, defaultImage: defauleImage, isKeepOriginal: false, completion: nil)
        
    }
    
    
    /// 打印日志
    /// - Parameter msg: msg 日志信息
    private func showLog(_ msg: String) {
        if isShowRBWebImageLog {
            print(msg)
        }
    }
    
    
}


extension String {
    
    /// 格式化url
    /// - Returns: 格式化url字符串
    func urlString() -> String? {
        if self.count == 0 {
            return nil
        }
        let urlStr = CFURLCreateStringByReplacingPercentEscapes(kCFAllocatorDefault, self as CFString, "!$&'()*+,-./:;=?@_~%#[]" as CFString) as String?
        if urlStr == nil || urlStr?.count == 0 {
            return nil
        }
        return urlStr
    }
}

参考代码:github