序言--感谢好心大神分享
Kingfisher 是由 @onevcat 编写的用于下载和缓存网络图片的轻量级Swift工具库,其中涉及到了包括GCD、Swift高级语法、缓存、硬盘读写、网络编程、图像编码、图形绘制、Gif数据生成和处理、MD5、Associated Objects的使用等大量iOS开发知识。
本文将详尽的对所涉及到的知识点进行讲解,但由于笔者水平有限,失误和遗漏之处在所难免,恳请前辈们批评指正。
一、Kingfisher的架构
Kingfisher.png
Kingfisher 源码中所包含的12个文件及其关系如上图所示,从左至右,由深及浅。
UIImage+Extension 文件内部对 UIImage 以及 NSData 进行了拓展, 包含判定图片类型、图片解码以及Gif数据处理等操作。
String+MD5 负责图片缓存时对文件名进行MD5加密操作。
ImageCache 主要负责将加载过的图片缓存至本地。
ImageDownloader 负责下载网络图片。
KingfisherOptions 内含配置 Kingfisher 行为的部分参数,包括是否设置下载低优先级、是否强制刷新、是否仅缓存至内存、是否允许图像后台解码等设置。
Resource 中的 Resource 结构体记录了图片的下载地址和缓存Key。
ImageTransition 文件中的动画效果将在使用 UIImageView 的拓展 API 时被采用,其底层为UIViewAnimationOptions,此外你也可以自己传入相应地动画操作、完成闭包来配置自己的动画效果。
ThreadHelper 中的 dispatch_async_safely_main_queue 函数接受一个闭包,利用 NSThread.isMainThread 判定并将其放置在主线程中执行。
KingfisherManager 是 Kingfisher 的主控制类,整合了图片下载及缓存操作。
KingfisherOptionsInfoItem 被提供给开发者对 Kingfisher 的各种行为进行控制,包含下载设置、缓存设置、动画设置以及 KingfisherOptions 中的全部配置参数。
UIImage+Kingfisher 以及 UIButton+Kingfisher 对 UIImageView 和 UIButton 进行了拓展,即主要用于提供 Kingfisher 的外部接口。
二、UIImage+Extension
图片格式识别
Magic Number 是用于区分不同文件格式,被放置于文件首的标记数据。Kingfisher 中用它来区分不同的图片格式,如PNG、JPG、GIF。代码如下:
private let pngHeader: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
private let jpgHeaderSOI: [UInt8] = [0xFF, 0xD8]
private let jpgHeaderIF: [UInt8] = [0xFF]
private let gifHeader: [UInt8] = [0x47, 0x49, 0x46]
// MARK: - Image format
enum ImageFormat {
case Unknown, PNG, JPEG, GIF
}
extension NSData {
var kf_imageFormat: ImageFormat {
var buffer = [UInt8](count: 8, repeatedValue: 0)
self.getBytes(&buffer, length: 8)
if buffer == pngHeader {
return .PNG
} else if buffer[0] == jpgHeaderSOI[0] &&
buffer[1] == jpgHeaderSOI[1] &&
buffer[2] == jpgHeaderIF[0]
{
return .JPEG
}else if buffer[0] == gifHeader[0] &&
buffer[1] == gifHeader[1] &&
buffer[2] == gifHeader[2]
{
return .GIF
}
return .Unknown
}
}
代码上部定义的 imageHeader,就是不同格式图片放置在文件首的对应 Magic Number 数据,我们通过 NSData 的 getBytes: 方法得到图片数据的 Magic Number,通过比对确定图片格式。
图片解码
我们知道 PNG 以及 JPEG 等格式的图片对原图进行了压缩,必须要将其图片数据解码成位图之后才能使用,这是原因,Kingfisher 里提供了用于解码的函数,代码如下:
// MARK: - Decode
extension UIImage {
func kf_decodedImage() -> UIImage? {
return self.kf_decodedImage(scale: self.scale)
}
func kf_decodedImage(scale scale: CGFloat) -> UIImage? {
let imageRef = self.CGImage
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue).rawValue
let contextHolder = UnsafeMutablePointer<Void>()
let context = CGBitmapContextCreate(contextHolder, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef), 8, 0, colorSpace, bitmapInfo)
if let context = context {
let rect = CGRectMake(0, 0, CGFloat(CGImageGetWidth(imageRef)), CGFloat(CGImageGetHeight(imageRef)))
CGContextDrawImage(context, rect, imageRef)
let decompressedImageRef = CGBitmapContextCreateImage(context)
return UIImage(CGImage: decompressedImageRef!, scale: scale, orientation: self.imageOrientation)
} else {
return nil
}
}
}
这段代码的主要含义是通过 CGBitmapContextCreate 以及 CGContextDrawImage 函数,将被压缩的图片画在 context 上,再通过调用 CGBitmapContextCreateImage 函数,即可完成对被压缩图片的解码。但通过测试后续代码发现,包含 decode 函数的分支从来没被调用过,据本人推测,UIImage 在接收 NSData 数据进行初始化的时候,其本身很可能包含有通过 Magic Number 获知图片格式后,解码并展示的功能,并不需要外部解码。
图片正立
使用 Core Graphics 绘制图片时,图片会倒立显示,Kingfisher 中使用了这个特性创建了一个工具函数来确保图片的正立,虽然该函数并未在后续的文件中使用到,代码如下:
// MARK: - Normalization
extension UIImage {
public func kf_normalizedImage() -> UIImage {
if imageOrientation == .Up {
return self
}
UIGraphicsBeginImageContextWithOptions(size, false, scale)
drawInRect(CGRect(origin: CGPointZero, size: size))
let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return normalizedImage;
}
}
如果该图片方向为正立,返回自身;否则将其用 Core Graphics 绘制,返回正立后的图片。
GIF数据的保存
我们知道,UIImage 并不能直接保存,需要先将其转化为 NSData 才能写入硬盘以及内存中缓存起来,UIKit 提供了两个 C 语言函数:UIImageJPEGRepresentation 和 UIImagePNGRepresentation,以便于将 JPG 及 PNG 格式的图片转化为 NSData 数据,但却并没有提供相应的 UIImageGIFRepresentation,所以我们需要自己编写这个函数以完成对Gif数据的保存,代码如下:
import ImageIO
import MobileCoreServices
// MARK: - GIF
func UIImageGIFRepresentation(image: UIImage) -> NSData? {
return UIImageGIFRepresentation(image, duration: 0.0, repeatCount: 0)
}
func UIImageGIFRepresentation(image: UIImage, duration: NSTimeInterval, repeatCount: Int) -> NSData? {
guard let images = image.images else {
return nil
}
let frameCount = images.count
let gifDuration = duration <= 0.0 ? image.duration / Double(frameCount) : duration / Double(frameCount)
let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: gifDuration]]
let imageProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: repeatCount]]
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data, kUTTypeGIF, frameCount, nil) else {
return nil
}
CGImageDestinationSetProperties(destination, imageProperties)
for image in images {
CGImageDestinationAddImage(destination, image.CGImage!, frameProperties)
}
return CGImageDestinationFinalize(destination) ? NSData(data: data) : nil
}
为实现这个功能,我们首先需要在文件头部添加 ImageIO 和 MobileCoreServices 这两个系统库。
第一部分 guard 语句用于确保GIF数据存在,images 即GIF动图中每一帧的静态图片,类型为 [UIImage]? ;第二部分取得图片总张数以及每一帧的持续时间。
CGImageDestination 对象是对数据写入操作的抽象,Kingfisher 用其实现对GIF数据的保存。
CGImageDestinationCreateWithData 指定了图片数据的保存位置、数据类型以及图片的总张数,最后一个参数现需传入 nil。
CGImageDestinationSetProperties 用于传入包含静态图的通用配置参数,此处传入了Gif动图的重复播放次数。
CGImageDestinationAddImage 用于添加每一张静态图片的数据以及对应的属性(可选),此处添加了每张图片的持续时间。
CGImageDestinationFinalize 需要在所有数据被写入后调用,成功返回 true,失败则返回 false。
GIF数据的展示
我们并不能像其他格式的图片一样直接传入 NSData 给 UIImage 来创建一个GIF动图,而是需要使用 UIImage 的 animatedImageWithImages 方法,但此函数所需的参数是 [UIImage],所以我们需要首先将 NSData 格式的图片数据拆分为每一帧的静态图片,再将其传入上述函数之中,代码如下:
extension UIImage {
static func kf_animatedImageWithGIFData(gifData data: NSData) -> UIImage? {
return kf_animatedImageWithGIFData(gifData: data, scale: UIScreen.mainScreen().scale, duration: 0.0)
}
static func kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval) -> UIImage? {
let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(bool: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF]
guard let imageSource = CGImageSourceCreateWithData(data, options) else {
return nil
}
let frameCount = CGImageSourceGetCount(imageSource)
var images = [UIImage]()
var gifDuration = 0.0
for i in 0 ..< frameCount {
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else {
return nil
}
guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil),
gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
frameDuration = (gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber) else
{
return nil
}
gifDuration += frameDuration.doubleValue
images.append(UIImage(CGImage: imageRef, scale: scale, orientation: .Up))
}
if (frameCount == 1) {
return images.first
} else {
return UIImage.animatedImageWithImages(images, duration: duration <= 0.0 ? gifDuration : duration)
}
}
}
与 CGImageDestination 相对应的,CGImageSource 对象是对数据读出操作的抽象,Kingfisher 用其实现GIF数据的读出。
与 CGImageDestination 的写入操作相类似,这里我们通过 CGImageSourceCreateWithData、CGImageSourceGetCount 以及循环执行相应次数的 CGImageSourceCreateImageAtIndex 来得到 [UIImage],并通过 CGImageSourceCopyPropertiesAtIndex 等相关操作取得每张图片的原持续时间,将其求和,最后将两个对应参数传入 UIImage.animatedImageWithImages 中,即可得到所需的GIF动图。
三、String+MD5
MD5加密
MD5加密在 Kingfisher 中被用于缓存时对文件名的加密,由于其内部实现较为复杂,此处仅提供成品代码以备不时之需,代码如下:
import Foundation
extension String {
func kf_MD5() -> String {
if let data = dataUsingEncoding(NSUTF8StringEncoding) {
let MD5Calculator = MD5(data)
let MD5Data = MD5Calculator.calculate()
let resultBytes = UnsafeMutablePointer<CUnsignedChar>(MD5Data.bytes)
let resultEnumerator = UnsafeBufferPointer<CUnsignedChar>(start: resultBytes, count: MD5Data.length)
var MD5String = ""
for c in resultEnumerator {
MD5String += String(format: "%02x", c)
}
return MD5String
} else {
return self
}
}
}
/** array of bytes, little-endian representation */
func arrayOfBytes<T>(value:T, length:Int? = nil) -> [UInt8] {
let totalBytes = length ?? (sizeofValue(value) * 8)
let valuePointer = UnsafeMutablePointer<T>.alloc(1)
valuePointer.memory = value
let bytesPointer = UnsafeMutablePointer<UInt8>(valuePointer)
var bytes = [UInt8](count: totalBytes, repeatedValue: 0)
for j in 0..<min(sizeof(T),totalBytes) {
bytes[totalBytes - 1 - j] = (bytesPointer + j).memory
}
valuePointer.destroy()
valuePointer.dealloc(1)
return bytes
}
extension Int {
/** Array of bytes with optional padding (little-endian) */
func bytes(totalBytes: Int = sizeof(Int)) -> [UInt8] {
return arrayOfBytes(self, length: totalBytes)
}
}
extension NSMutableData {
/** Convenient way to append bytes */
func appendBytes(arrayOfBytes: [UInt8]) {
appendBytes(arrayOfBytes, length: arrayOfBytes.count)
}
}
class HashBase {
var message: NSData
init(_ message: NSData) {
self.message = message
}
/** Common part for hash calculation. Prepare header data. */
func prepare(len:Int = 64) -> NSMutableData {
let tmpMessage: NSMutableData = NSMutableData(data: self.message)
// Step 1. Append Padding Bits
tmpMessage.appendBytes([0x80]) // append one bit (UInt8 with one bit) to message
// append "0" bit until message length in bits ≡ 448 (mod 512)
var msgLength = tmpMessage.length;
var counter = 0;
while msgLength % len != (len - 8) {
counter++
msgLength++
}
let bufZeros = UnsafeMutablePointer<UInt8>(calloc(counter, sizeof(UInt8)))
tmpMessage.appendBytes(bufZeros, length: counter)
bufZeros.destroy()
bufZeros.dealloc(1)
return tmpMessage
}
}
func rotateLeft(v:UInt32, n:UInt32) -> UInt32 {
return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n))
}
class MD5 : HashBase {
/** specifies the per-round shift amounts */
private let s: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]
/** binary integer part of the sines of integers (Radians) */
private let k: [UInt32] = [0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,
0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,
0xd62f105d,0x2441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,
0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,
0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x4881d05,
0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,
0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,
0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391]
private let h:[UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
func calculate() -> NSData {
let tmpMessage = prepare()
// hash values
var hh = h
// Step 2. Append Length a 64-bit representation of lengthInBits
let lengthInBits = (message.length * 8)
let lengthBytes = lengthInBits.bytes(64 / 8)
tmpMessage.appendBytes(Array(lengthBytes.reverse()));
// Process the message in successive 512-bit chunks:
let chunkSizeBytes = 512 / 8 // 64
var leftMessageBytes = tmpMessage.length
for (var i = 0; i < tmpMessage.length; i = i + chunkSizeBytes, leftMessageBytes -= chunkSizeBytes) {
let chunk = tmpMessage.subdataWithRange(NSRange(location: i, length: min(chunkSizeBytes,leftMessageBytes)))
// break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15
var M:[UInt32] = [UInt32](count: 16, repeatedValue: 0)
let range = NSRange(location:0, length: M.count * sizeof(UInt32))
chunk.getBytes(UnsafeMutablePointer<Void>(M), range: range)
// Initialize hash value for this chunk:
var A:UInt32 = hh[0]
var B:UInt32 = hh[1]
var C:UInt32 = hh[2]
var D:UInt32 = hh[3]
var dTemp:UInt32 = 0
// Main loop
for j in 0..<k.count {
var g = 0
var F:UInt32 = 0
switch (j) {
case 0...15:
F = (B & C) | ((~B) & D)
g = j
break
case 16...31:
F = (D & B) | (~D & C)
g = (5 * j + 1) % 16
break
case 32...47:
F = B ^ C ^ D
g = (3 * j + 5) % 16
break
case 48...63:
F = C ^ (B | (~D))
g = (7 * j) % 16
break
default:
break
}
dTemp = D
D = C
C = B
B = B &+ rotateLeft((A &+ F &+ k[j] &+ M[g]), n: s[j])
A = dTemp
}
hh[0] = hh[0] &+ A
hh[1] = hh[1] &+ B
hh[2] = hh[2] &+ C
hh[3] = hh[3] &+ D
}
let buf: NSMutableData = NSMutableData();
hh.forEach({ (item) -> () in
var i:UInt32 = item.littleEndian
buf.appendBytes(&i, length: sizeofValue(i))
})
return buf.copy() as! NSData;
}
}
上述代码为 String 添加了 kf_MD5 拓展方法,返回值也为 String,只需对需要加密的 String 调用该拓展方法,即可得到对应的加密字符串。
四、ImageCache
缓存功能的架构以及主要属性介绍
缓存功能分为两部分:一是内存缓存,二是硬盘缓存。
我们需要实现的主要功能有:
- 缓存路径管理
- 缓存的添加与删除
- 缓存的读取
- 缓存的清理
- 缓存状态监控
缓存管理类所包含的主要属性如下所示:
public class ImageCache {
//Memory
private let memoryCache = NSCache()
/// The largest cache cost of memory cache. The total cost is pixel count of all cached images in memory.
public var maxMemoryCost: UInt = 0 {
didSet {
self.memoryCache.totalCostLimit = Int(maxMemoryCost)
}
}
//Disk
private let ioQueue: dispatch_queue_t
private let diskCachePath: String
private var fileManager: NSFileManager!
/// The longest time duration of the cache being stored in disk. Default is 1 week.
public var maxCachePeriodInSecond = defaultMaxCachePeriodInSecond
/// The largest disk size can be taken for the cache. It is the total allocated size of cached files in bytes. Default is 0, which means no limit.
public var maxDiskCacheSize: UInt = 0
private let processQueue: dispatch_queue_t
/// The default cache.
public class var defaultCache: ImageCache {
return defaultCacheInstance
}
public init(name: String) {
if name.isEmpty {
fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
}
let cacheName = cacheReverseDNS + name
memoryCache.name = cacheName
let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true)
diskCachePath = (paths.first! as NSString).stringByAppendingPathComponent(cacheName)
ioQueue = dispatch_queue_create(ioQueueName + name, DISPATCH_QUEUE_SERIAL)
processQueue = dispatch_queue_create(processQueueName + name, DISPATCH_QUEUE_CONCURRENT)
dispatch_sync(ioQueue, { () -> Void in
self.fileManager = NSFileManager()
})
NSNotificationCenter.defaultCenter().addObserver(self, selector: "clearMemoryCache", name: UIApplicationDidReceiveMemoryWarningNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "cleanExpiredDiskCache", name: UIApplicationWillTerminateNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundCleanExpiredDiskCache", name: UIApplicationDidEnterBackgroundNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
其中,memoryCache: NSCache 用于管理内存缓存。
ioQueue: dispatch_queue_t 为单独的硬盘操作队列,由于硬盘存取操作极为耗时,使其与主线程并行执行以免造成阻塞。
diskCachePath: String 用于设置文件的存储路径。
fileManager: NSFileManager 用于文件管理。
processQueue: dispatch_queue_t 用于执行图片的 decode 操作,不过并不会被调用。
defaultCache 为 ImageCache 类的单例,Swift 中,采用 static let 即可直接创建一个单例,系统会自动调用 dispatch_once。
缓存路径管理
为了方便对硬盘的存取操作,我们需要这样几个工具函数,来帮我们实现通过缓存Key获得某特定缓存的:
- 对应 UIImage 图片
- 对应 NSData 数据
- 硬盘存储路径
- 加密后的文件名
代码如下:
extension ImageCache {
func diskImageForKey(key: String, scale: CGFloat) -> UIImage? {
if let data = diskImageDataForKey(key) {
return UIImage.kf_imageWithData(data, scale: scale)
} else {
return nil
}
}
func diskImageDataForKey(key: String) -> NSData? {
let filePath = cachePathForKey(key)
return NSData(contentsOfFile: filePath)
}
func cachePathForKey(key: String) -> String {
let fileName = cacheFileNameForKey(key)
return (diskCachePath as NSString).stringByAppendingPathComponent(fileName)
}
func cacheFileNameForKey(key: String) -> String {
return key.kf_MD5()
}
}
由下及上,由深入浅,每个函数都用到了上一个函数的结果并进行了进一步加工。
字典按值排序
在缓存的管理当中,有时候我们需要依照缓存的修改时间进行排序,以确定缓存是否过期,而缓存时间往往位于字典键值对中值的位置,通常情况下对其排序并不是太容易,这里提供一个工具函数,代码如下:
extension Dictionary {
func keysSortedByValue(isOrderedBefore:(Value, Value) -> Bool) -> [Key] {
var array = Array(self)
array.sortInPlace {
let (_, lv) = $0
let (_, rv) = $1
return isOrderedBefore(lv, rv)
}
return array.map {
let (k, _) = $0
return k
}
}
}
接受排序规则闭包,若返回值为 true,则第一个参数在第二个的前面,函数返回排序过后的Key值数组。
函数体的第一句是亮点,这里直接用 Array 的初始化方法将 Dictionary 转成了一个元组数组,每个元组包含两个值,第一个为原字典Key,第二个为原字典Value。
sortInPlace 为在当前数组内存位置上进行排序,闭包里先用两个 let 取到字典Value,将其送入排序规则中比对并返回比对结果,该函数执行过后,我们就能得到一个按字典Value排好序的元组数组。
接着,我们调用 map 函数,将每个元组的第一个值(即原字典Key),取出并覆盖原元组,最后得到有序的字典Key值数组。
缓存的添加与删除
缓存的添加
缓存的添加分为三步:写入内存、写入硬盘、执行 completionHandler,其中写入硬盘操作略复杂,代码如下:
public func storeImage(image: UIImage, originalData: NSData? = nil, forKey key: String, toDisk: Bool, completionHandler: (() -> ())?) {
memoryCache.setObject(image, forKey: key, cost: image.kf_imageCost)
func callHandlerInMainQueue() {
if let handler = completionHandler {
dispatch_async(dispatch_get_main_queue()) {
handler()
}
}
}
if toDisk {
dispatch_async(ioQueue, { () -> Void in
let imageFormat: ImageFormat
if let originalData = originalData {
imageFormat = originalData.kf_imageFormat
} else {
imageFormat = .Unknown
}
let data: NSData?
switch imageFormat {
case .PNG: data = UIImagePNGRepresentation(image)
case .JPEG: data = UIImageJPEGRepresentation(image, 1.0)
case .GIF: data = UIImageGIFRepresentation(image)
case .Unknown: data = originalData
}
if let data = data {
if !self.fileManager.fileExistsAtPath(self.diskCachePath) {
do {
try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
self.fileManager.createFileAtPath(self.cachePathForKey(key), contents: data, attributes: nil)
callHandlerInMainQueue()
} else {
callHandlerInMainQueue()
}
})
} else {
callHandlerInMainQueue()
}
}
写入内存操作非常简单,直接调用 NSCache 实例的 setObject 即可,kf_imageCost 为图片的宽乘高乘比例平方的整数值。
callHandlerInMainQueue 为定义的嵌套函数,调用后在主线程上执行 completionHandler。
我们利用 GCD 将硬盘的读写操作放置于 ioQueue 中执行,这里我要多说两句,其实在这个地方我是很不理解的,我的观点是这样的:
首先,对于 PNG、JPEG 格式的图片:
当需要展示的时候,我们从网络中获取到的数据直接可以被 UIImage 的初始化方法识别,所以上文中所提到的 decode 函数并不必要,取到的数据可以直接用来展示;
当需要保存的时候,我们可以直接保存当初下载到的网络数据,而不是像上述代码一样,先根据网络数据判断图片类型,再调用对应的 UIImageRepresentation 将成品图片拆分成数据再保存,我个人认为这是多此一举的。
在测试中,我截取了同一张图片的 originalData 与拆分成品图片后的 data 作对比,结果如下:
originalData.png
data.png
可以看到内容基本相同,而且我将两个 UIImageRepresentation 函数替换为 originalData 后,结果并无不同;
此外,若 originalData 为 nil,imageFormat = .Unknown,在后续的代码中 case .Unknown: data = originalData 语句直接将 data 置空,说明了在有图片传入的情况下,originalData 不可为空,不存在只有图片没有数据,必须通过解码得到数据的情况。所以我依此认为,这段代码中 UIImagePNGRepresentation 以及 UIImageJPEGRepresentation 的使用是完全没有必要的,如果前辈们有不同意见,本人愿意接受批评指正。
其次,对于 GIF 格式的图片:
当需要展示的时候,我认为 kf_animatedImageWithGIFData(gifData data: NSData, scale: CGFloat, duration: NSTimeInterval) -> UIImage? 函数的存在还是有必要的,因为 UIImage 的初始化方法并不能直接处理GIF数据,而 UIImage.animatedImageWithImages 方法所接受的参数也并不是 NSData 而是 [UIImage]。
但当需要保存的时候,我同样质疑 UIImageGIFRepresentation 函数存在的必要性,我在翻阅 ImageDownloader 源码的时候发现,若所获得的是GIF数据,kf_animatedImageWithGIFData 也可以直接处理将其转换成GIF动图,即表示,得到的网络数据(也就是这里的 originalData),是可以被直接识别的,并不需要调用 UIImageGIFRepresentation 来把依照 originalData 生成的GIF动图拆分成 data 再保存,而是直接保存 originalData 即可。
继续回到代码中,剩下的操作就非常简单了,在取到数据的情况下,若文件目录不存在,先生成目录再保存文件,最后调用 completionHandler。
缓存的删除
删除操作十分简单,同样分三步:删除内存缓存、依照路径删除硬盘缓存、执行 completionHandler,代码如下:
public func removeImageForKey(key: String, fromDisk: Bool, completionHandler: (() -> ())?) {
memoryCache.removeObjectForKey(key)
func callHandlerInMainQueue() {
if let handler = completionHandler {
dispatch_async(dispatch_get_main_queue()) {
handler()
}
}
}
if fromDisk {
dispatch_async(ioQueue, { () -> Void in
do {
try self.fileManager.removeItemAtPath(self.cachePathForKey(key))
} catch _ {}
callHandlerInMainQueue()
})
} else {
callHandlerInMainQueue()
}
}
memoryCache.removeObjectForKey 删除内存缓存,self.fileManager.removeItemAtPath 删除硬盘缓存。
缓存的读取
缓存的读取所完成的操作也十分简单,首先确保 completionHandler 不为空,之后分别尝试从内存和硬盘中读取缓存,若缓存只存在于硬盘中,读取后,我们将其添加到内存中,代码如下:
extension ImageCache {
/**
Get an image for a key from memory or disk.
- parameter key: Key for the image.
- parameter options: Options of retrieving image.
- parameter completionHandler: Called when getting operation completes with image result and cached type of this image. If there is no such key cached, the image will be `nil`.
- returns: The retrieving task.
*/
public func retrieveImageForKey(key: String, options:KingfisherManager.Options, completionHandler: ((UIImage?, CacheType!) -> ())?) -> RetrieveImageDiskTask? {
// No completion handler. Not start working and early return.
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
if let image = self.retrieveImageInMemoryCacheForKey(key) {
//Found image in memory cache.
if options.shouldDecode {
dispatch_async(self.processQueue, { () -> Void in
let result = image.kf_decodedImage(scale: options.scale)
dispatch_async(options.queue, { () -> Void in
completionHandler(result, .Memory)
})
})
} else {
completionHandler(image, .Memory)
}
} else {
var sSelf: ImageCache! = self
block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
// Begin to load image from disk
dispatch_async(sSelf.ioQueue, { () -> Void in
if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scale) {
if options.shouldDecode {
dispatch_async(sSelf.processQueue, { () -> Void in
let result = image.kf_decodedImage(scale: options.scale)
sSelf.storeImage(result!, forKey: key, toDisk: false, completionHandler: nil)
dispatch_async(options.queue, { () -> Void in
completionHandler(result, .Memory)
sSelf = nil
})
})
} else {
sSelf.storeImage(image, forKey: key, toDisk: false, completionHandler: nil)
dispatch_async(options.queue, { () -> Void in
completionHandler(image, .Disk)
sSelf = nil
})
}
} else {
// No image found from either memory or disk
dispatch_async(options.queue, { () -> Void in
completionHandler(nil, nil)
sSelf = nil
})
}
})
}
dispatch_async(dispatch_get_main_queue(), block!)
}
return block
}
/**
Get an image for a key from memory.
- parameter key: Key for the image.
- returns: The image object if it is cached, or `nil` if there is no such key in the cache.
*/
public func retrieveImageInMemoryCacheForKey(key: String) -> UIImage? {
return memoryCache.objectForKey(key) as? UIImage
}
/**
Get an image for a key from disk.
- parameter key: Key for the image.
- param scale: The scale factor to assume when interpreting the image data.
- returns: The image object if it is cached, or `nil` if there is no such key in the cache.
*/
public func retrieveImageInDiskCacheForKey(key: String, scale: CGFloat = KingfisherManager.DefaultOptions.scale) -> UIImage? {
return diskImageForKey(key, scale: scale)
}
}
这里主要说两点,第一,若提交的 block 将异步执行的话,DISPATCH_BLOCK_INHERIT_QOS_CLASS 需要被传入,同步执行则应传入 DISPATCH_BLOCK_ENFORCE_QOS_CLASS。
第二,dispatch_async(dispatch_get_main_queue(), block!) 之中,若将 async 改为 sync, 而这段代码又执行于主线程上时,必然会导致死锁,或者说,在当前线程上调用 dispatch_sync 方法给自身线程分配任务,则必然会导致死锁。
因为 dispatch_sync 需要等待内部操作执行完成后才会返回,进而释放当前线程,而如果内部操作又分配在自身线程上时,若自身不释放,内部的操作就会一直等待,就会出现不返回不释放,不释放不执行更不会返回的死锁。
而使用 dispatch_async 则不会出现这种问题,因为 dispatch_async 方法不必等待内部操作完成便直接返回,释放当前线程后,block 内部的操作便可以开始执行。
缓存清理
我们在缓存清理方面的需求一般有两个:清理所有硬盘内存缓存、后台自动删除过期超量硬盘缓存。
清理所有缓存
这部分操作相较于下部分简单一些,代码如下:
@objc public func clearMemoryCache() {
memoryCache.removeAllObjects()
}
/**
Clear disk cache. This is an async operation.
*/
public func clearDiskCache() {
clearDiskCacheWithCompletionHandler(nil)
}
/**
Clear disk cache. This is an async operation.
- parameter completionHander: Called after the operation completes.
*/
public func clearDiskCacheWithCompletionHandler(completionHander: (()->())?) {
dispatch_async(ioQueue, { () -> Void in
do {
try self.fileManager.removeItemAtPath(self.diskCachePath)
} catch _ {
}
do {
try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {
}
if let completionHander = completionHander {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completionHander()
})
}
})
}
这里需要注意的是,我们使用 self.fileManager.removeItemAtPath 删除所有硬盘缓存之后,需要使用 self.fileManager.createDirectoryAtPath 重建缓存目录。
后台自动删除过期超量硬盘缓存
这部分的重点有如下几个:
- 遍历所有缓存文件
- 判断缓存文件是否过期
- 将缓存文件按日期排序,逐步清理直到所占空间小于预定大小
- 后台自动清理缓存
解决前三个问题,代码如下:
public func cleanExpiredDiskCacheWithCompletionHander(completionHandler: (()->())?) {
// Do things in cocurrent io queue
dispatch_async(ioQueue, { () -> Void in
let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath)
let resourceKeys = [NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]
let expiredDate = NSDate(timeIntervalSinceNow: -self.maxCachePeriodInSecond)
var cachedFiles = [NSURL: [NSObject: AnyObject]]()
var URLsToDelete = [NSURL]()
var diskCacheSize: UInt = 0
if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL,
includingPropertiesForKeys: resourceKeys,
options: NSDirectoryEnumerationOptions.SkipsHiddenFiles,
errorHandler: nil) {
for fileURL in fileEnumerator.allObjects as! [NSURL] {
do {
let resourceValues = try fileURL.resourceValuesForKeys(resourceKeys)
// If it is a Directory. Continue to next file URL.
if let isDirectory = resourceValues[NSURLIsDirectoryKey] as? NSNumber {
if isDirectory.boolValue {
continue
}
}
// If this file is expired, add it to URLsToDelete
if let modificationDate = resourceValues[NSURLContentModificationDateKey] as? NSDate {
if modificationDate.laterDate(expiredDate) == expiredDate {
URLsToDelete.append(fileURL)
continue
}
}
if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
diskCacheSize += fileSize.unsignedLongValue
cachedFiles[fileURL] = resourceValues
}
} catch _ {
}
}
}
for fileURL in URLsToDelete {
do {
try self.fileManager.removeItemAtURL(fileURL)
} catch _ {
}
}
if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
let targetSize = self.maxDiskCacheSize / 2
// Sort files by last modify date. We want to clean from the oldest files.
let sortedFiles = cachedFiles.keysSortedByValue({ (resourceValue1, resourceValue2) -> Bool in
if let date1 = resourceValue1[NSURLContentModificationDateKey] as? NSDate {
if let date2 = resourceValue2[NSURLContentModificationDateKey] as? NSDate {
return date1.compare(date2) == .OrderedAscending
}
}
// Not valid date information. This should not happen. Just in case.
return true
})
for fileURL in sortedFiles {
do {
try self.fileManager.removeItemAtURL(fileURL)
} catch {
}
URLsToDelete.append(fileURL)
if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
diskCacheSize -= fileSize.unsignedLongValue
}
if diskCacheSize < targetSize {
break
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if URLsToDelete.count != 0 {
let cleanedHashes = URLsToDelete.map({ (url) -> String in
return url.lastPathComponent!
})
NSNotificationCenter.defaultCenter().postNotificationName(KingfisherDidCleanDiskCacheNotification, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
}
if let completionHandler = completionHandler {
completionHandler()
}
})
})
}
其中,关于第一个问题:我们利用 NSFileManager 的实例方法 enumeratorAtURL,来获得 NSDirectoryEnumerator 的实例 fileEnumerator,再利用 for in 遍历 fileEnumerator.allObjects 来获取每个缓存文件的 fileURL: NSURL。
第二个问题:我们通过 fileURL.resourceValuesForKeys[NSURLContentModificationDateKey] 来得到对应文件的最近修改日期属性,将其与过期时间比较,即可确定其是否过期。
第三个问题:我们通过之前的字典按值排序拓展方法来对缓存文件按最近修改日期进行排序,随即对其遍历,按顺序删除,直到小于预定大小。
除以上三个问题之外,我们还希望,当应用程序在进入后台的时候,可以自动检测过期超量缓存,并在后台完成清理操作,实现代码如下:
/**
Clean expired disk cache when app in background. This is an async operation.
In most cases, you should not call this method explicitly.
It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
*/
@objc public func backgroundCleanExpiredDiskCache() {
func endBackgroundTask(inout task: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(task)
task = UIBackgroundTaskInvalid
}
var backgroundTask: UIBackgroundTaskIdentifier!
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
endBackgroundTask(&backgroundTask!)
}
cleanExpiredDiskCacheWithCompletionHander { () -> () in
endBackgroundTask(&backgroundTask!)
}
}
该函数会在应用进入运行时自动调用,实现方法是利用 NSNotificationCenter.defaultCenter 来监听系统的 UIApplicationDidEnterBackgroundNotification 广播;
beginBackgroundTaskWithExpirationHandler 以及 endBackgroundTask 之间的操作会在后台执行,不止可以做自动缓存清理,你也可以将一些比较耗时的下载操作放在后台进行;backgroundTask 作为任务开始和结束的标识,endBackgroundTask 函数参数列表中的 inout 是为了使 backgroundTask 在函数体内部的修改有效化,类似于传入指针。
GCD相关
我们在 Kingfisher 的源码中,经常可以看到 completionHandler 的存在,这个闭包将在所有操作完成后调用,多用于做结果的处理;但有时候我们为了调用 completionHandler 所要面对的问题要比 Kingfisher 中所涉及到的要复杂的多,比如,我们需要在一个线程执行完分多次提交的多个异步闭包之后调用某个函数或者执行另一个闭包,这样的话,我们就不能像 Kingfisher 里这样单纯的将 completionHandler 放在闭包尾了事了,GCD 提供了先进的特性来解决我们的这种需求,代码如下:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
// block1
NSLog(@"Block1");
[NSThread sleepForTimeInterval:5.0];
NSLog(@"Block1 End");
});
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
// block2
NSLog(@"Block2");
[NSThread sleepForTimeInterval:8.0];
NSLog(@"Block2 End");
});
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
// block3
NSLog(@"Block3");
});
dispatch_release(group);
这是其中一种比较简洁的实现方法,我们先创建一个 dispatch_group_t 实例,通过使用 dispatch_group_async 为其提交异步闭包任务,当这个 group 处理完所有的闭包任务之后,dispatch_group_notify 才会被调用,你就可以把你需要最后执行的 completionHandler 放置在这个地方。
缓存状态监控
缓存状态监控是缓存管理当中很有用的工具,主要包含:
- 查询某图片是否存在于缓存中
- 查询某图片的缓存文件名
- 查询当前缓存所占硬盘空间大小
代码如下:
// MARK: - Check cache status
public extension ImageCache {
/**
* Cache result for checking whether an image is cached for a key.
*/
public struct CacheCheckResult {
public let cached: Bool
public let cacheType: CacheType?
}
/**
Check whether an image is cached for a key.
- parameter key: Key for the image.
- returns: The check result.
*/
public func isImageCachedForKey(key: String) -> CacheCheckResult {
if memoryCache.objectForKey(key) != nil {
return CacheCheckResult(cached: true, cacheType: .Memory)
}
let filePath = cachePathForKey(key)
if fileManager.fileExistsAtPath(filePath) {
return CacheCheckResult(cached: true, cacheType: .Disk)
}
return CacheCheckResult(cached: false, cacheType: nil)
}
/**
Get the hash for the key. This could be used for matching files.
- parameter key: The key which is used for caching.
- returns: Corresponding hash.
*/
public func hashForKey(key: String) -> String {
return cacheFileNameForKey(key)
}
/**
Calculate the disk size taken by cache.
It is the total allocated size of the cached files in bytes.
- parameter completionHandler: Called with the calculated size when finishes.
*/
public func calculateDiskCacheSizeWithCompletionHandler(completionHandler: ((size: UInt) -> ())?) {
dispatch_async(ioQueue, { () -> Void in
let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath)
let resourceKeys = [NSURLIsDirectoryKey, NSURLTotalFileAllocatedSizeKey]
var diskCacheSize: UInt = 0
if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL,
includingPropertiesForKeys: resourceKeys,
options: NSDirectoryEnumerationOptions.SkipsHiddenFiles,
errorHandler: nil) {
for fileURL in fileEnumerator.allObjects as! [NSURL] {
do {
let resourceValues = try fileURL.resourceValuesForKeys(resourceKeys)
// If it is a Directory. Continue to next file URL.
if let isDirectory = resourceValues[NSURLIsDirectoryKey]?.boolValue {
if isDirectory {
continue
}
}
if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
diskCacheSize += fileSize.unsignedLongValue
}
} catch _ {
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let completionHandler = completionHandler {
completionHandler(size: diskCacheSize)
}
})
})
}
}
前两个都是简单的方法调用,不需赘述;计算当前缓存所占空间与前面删除过期超量缓存的相关操作极为相似,基本流程为:一、切换线程,二、遍历文件,三、累加文件大小,四、切回主线程执行 completionHandler。
五、KingfisherOptions
KingfisherOptions 文件包含了对 Kingfisher 操作部分设置参数,其本身并没有太多可讲的,但其代码内用到了 Swift2.0 中所引入的一个新类型 OptionSetType,这段代码可以看做是 OptionSetType 的基本用法引导,具体内容如下:
public struct KingfisherOptions : OptionSetType {
public let rawValue: UInt
public init(rawValue: UInt) {
self.rawValue = rawValue
}
/// None options. Kingfisher will keep its default behavior.
public static let None = KingfisherOptions(rawValue: 0)
/// Download in a low priority.
public static let LowPriority = KingfisherOptions(rawValue: 1 << 0)
/// Try to send request to server first. If response code is 304 (Not Modified), use the cached image. Otherwise, download the image and cache it again.
public static var ForceRefresh = KingfisherOptions(rawValue: 1 << 1)
/// Only cache downloaded image to memory, not cache in disk.
public static var CacheMemoryOnly = KingfisherOptions(rawValue: 1 << 2)
/// Decode the image in background thread before using.
public static var BackgroundDecode = KingfisherOptions(rawValue: 1 << 3)
/// If set it will dispatch callbacks asynchronously to the global queue DISPATCH_QUEUE_PRIORITY_DEFAULT. Otherwise it will use the queue defined at KingfisherManager.DefaultOptions.queue
public static var BackgroundCallback = KingfisherOptions(rawValue: 1 << 4)
/// Decode the image using the same scale as the main screen. Otherwise it will use the same scale as defined on the KingfisherManager.DefaultOptions.scale.
public static var ScreenScale = KingfisherOptions(rawValue: 1 << 5)
}
除第一位以外,rawValue的每一个二进制位都代表一个单独的配置参数,这样直接通过判断 rawValue 的值,就能知道哪些选项是被选中的,比如,若 rawValue == 10,其二进制位后四位为1010,即可知道 BackgroundDecode 以及 ForceRefresh 被选中,当然你也可以直接使用 OptionSetType 协议所提供的 contains 函数来判定某选项是否被包含。
另外说一点,Kingfisher 这里的 OptionSetType 的用法并不标准,更为常见的用法是不设置 rawValue == 0 时所对应的参数,这样每个配置参数就正好对应一位二进制位了,也更容易理解。
六、ImageDownloader
下载功能的架构以及主要属性介绍
在 Kingfisher 内,该类负责网络图片的下载,是对底层 URLSession 的封装,通过设置 URLSession 并成为 NSURLSessionDataDelegate 来得到图片数据,其主要属性如下所示:
public class ImageDownloader: NSObject {
class ImageFetchLoad {
var callbacks = [CallbackPair]()
var responseData = NSMutableData()
var shouldDecode = false
var scale = KingfisherManager.DefaultOptions.scale
}
// MARK: - Public property
/// This closure will be applied to the image download request before it being sent. You can modify the request for some customizing purpose, like adding auth token to the header or do a url mapping.
public var requestModifier: (NSMutableURLRequest -> Void)?
/// The duration before the download is timeout. Default is 15 seconds.
public var downloadTimeout: NSTimeInterval = 15.0
/// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site.
public var trustedHosts: Set<String>?
/// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
public var sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
/// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
public weak var delegate: ImageDownloaderDelegate?
// MARK: - Internal property
let barrierQueue: dispatch_queue_t
let processQueue: dispatch_queue_t
typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)
var fetchLoads = [NSURL: ImageFetchLoad]()
// MARK: - Public method
/// The default downloader.
public class var defaultDownloader: ImageDownloader {
return instance
}
/**
Init a downloader with name.
- parameter name: The name for the downloader. It should not be empty.
- returns: The downloader object.
*/
public init(name: String) {
if name.isEmpty {
fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
}
barrierQueue = dispatch_queue_create(downloaderBarrierName + name, DISPATCH_QUEUE_CONCURRENT)
processQueue = dispatch_queue_create(imageProcessQueueName + name, DISPATCH_QUEUE_CONCURRENT)
}
func fetchLoadForKey(key: NSURL) -> ImageFetchLoad? {
var fetchLoad: ImageFetchLoad?
dispatch_sync(barrierQueue, { () -> Void in
fetchLoad = self.fetchLoads[key]
})
return fetchLoad
}
}
这段代码的重点在于:
这里定义了一个嵌套类 ImageFetchLoad,用于处理每一个 NSURL 的对应下载数据;
每一个 URL 的 ImageFetchLoad 里都包含一个 callbacks: [CallbackPair],而 CallbackPair 是一个元组,其中又包含两个闭包,一个是 progressBlock,一个是 completionHander,progressBlock 在每次接收到数据时都会调用,当下载任务较大时用于展示进度条,completionHander 当最后数据接收完成之后会被调用,只被调用一次。
每次获得的新数据都会被添加入 responseData: NSMutableData 中,最后完整的图片数据也会保存在其中。
通常情况下,我们的 ImageDownloader 往往需要处理多个 URL,也就对应多个 ImageFetchLoad,fetchLoads 是 [NSURL: ImageFetchLoad] 类型的字典,用于存储不同 URL 及其 ImageFetchLoad 之间的对应关系,这就牵扯到了一个问题,当读取 ImageFetchLoad 的时候,我们希望该 ImageFetchLoad 不在被写,写的同时不能进行读操作,我们使用 barrierQueue 来完成该需求,利用 dispatch_sync 阻塞当前线程,完成 ImageFetchLoad 读操作后再返回。
其实针对这种需求 GCD 提供了专门的特性来处理,即 dispatch_barrier_async 方法,使用此方法提交的任务,会等待先于它提交的任务执行完成之后才开始执行,只有当该任务执行完成之后,晚于它提交的任务才会开始执行,确保一段时间内该队列只执行该任务。
下载方法以及 NSURLSession 的设置
这段主要是为 KingfisherManager 提供封装好下载方法以及设置用于下载的 NSURLSession,代码如下:
internal func downloadImageWithURL(URL: NSURL,
retrieveImageTask: RetrieveImageTask?,
options: KingfisherManager.Options,
progressBlock: ImageDownloaderProgressBlock?,
completionHandler: ImageDownloaderCompletionHandler?)
{
if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled {
return
}
let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
// We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
let request = NSMutableURLRequest(URL: URL, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: timeout)
request.HTTPShouldUsePipelining = true
self.requestModifier?(request)
// There is a possiblility that request modifier changed the url to `nil`
if request.URL == nil {
completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
return
}
setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
let task = session.dataTaskWithRequest(request)
task.priority = options.lowPriority ? NSURLSessionTaskPriorityLow : NSURLSessionTaskPriorityDefault
task.resume()
fetchLoad.shouldDecode = options.shouldDecode
fetchLoad.scale = options.scale
retrieveImageTask?.downloadTask = task
}
}
// A single key may have multiple callbacks. Only download once.
internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {
dispatch_barrier_sync(barrierQueue, { () -> Void in
var create = false
var loadObjectForURL = self.fetchLoads[URL]
if loadObjectForURL == nil {
create = true
loadObjectForURL = ImageFetchLoad()
}
let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
loadObjectForURL!.callbacks.append(callbackPair)
self.fetchLoads[URL] = loadObjectForURL!
if create {
let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
started(session, loadObjectForURL!)
}
})
}
func cleanForURL(URL: NSURL) {
dispatch_barrier_sync(barrierQueue, { () -> Void in
self.fetchLoads.removeValueForKey(URL)
return
})
}
}
这里首先值得一提的是 NSMutableURLRequest 的实例属性 HTTPShouldUsePipelining,首先一图流解释这个属性的价值:
HTTPShouldUsePipelining.png
将该属性设置为 true 可以极大的提高网络性能,但是 HTTPShouldUsePipelining 也有其局限性,就是服务器必须按照收到请求的顺序返回对应的数据,详细内容在这里。
从某 URL 处下载图片时,通过 setupProgressBlock 以及传入的 started 闭包生成对应的 NSURLSession,并依据生成的 session 和之前的 request 生成 NSURLSessionDataTask,并保留引用在 retrieveImageTask?.downloadTask 里,为 KingfisherManager 提供任务终止方法。
当生成 NSURLSession 时所传入的 ephemeralSessionConfiguration() 配置参数意在不保留下载缓存,因为缓存操作已在我们的 ImageCache 文件中处理,所以此处需做如此设置以保证 ImageDownloader 正常工作。
NSURLSessionDataDelegate
let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
在之前的这行代码里,我们将自身设为了生成的 NSURLSession 的 delegate,所以接下来我们要通过实现 NSURLSessionDataDelegate 来得到返回的图片数据。
代码如下:
// MARK: - NSURLSessionTaskDelegate
extension ImageDownloader: NSURLSessionDataDelegate {
/**
This method is exposed since the compiler requests. Do not call it.
*/
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
completionHandler(NSURLSessionResponseDisposition.Allow)
}
/**
This method is exposed since the compiler requests. Do not call it.
*/
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if let URL = dataTask.originalRequest?.URL, fetchLoad = fetchLoadForKey(URL) {
fetchLoad.responseData.appendData(data)
for callbackPair in fetchLoad.callbacks {
callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
}
}
}
private func callbackWithImage(image: UIImage?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {
self.cleanForURL(imageURL)
for callbackPair in callbackPairs {
callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
}
}
}
/**
This method is exposed since the compiler requests. Do not call it.
*/
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if let URL = task.originalRequest?.URL {
if let error = error { // Error happened
callbackWithImage(nil, error: error, imageURL: URL, originalData: nil)
} else { //Download finished without error
// We are on main queue when receiving this.
dispatch_async(processQueue, { () -> Void in
if let fetchLoad = self.fetchLoadForKey(URL) {
if let image = UIImage.kf_imageWithData(fetchLoad.responseData, scale: fetchLoad.scale) {
self.delegate?.imageDownloader?(self, didDownloadImage: image, forURL: URL, withResponse: task.response!)
if fetchLoad.shouldDecode {
self.callbackWithImage(image.kf_decodedImage(scale: fetchLoad.scale), error: nil, imageURL: URL, originalData: fetchLoad.responseData)
} else {
self.callbackWithImage(image, error: nil, imageURL: URL, originalData: fetchLoad.responseData)
}
} else {
// If server response is 304 (Not Modified), inform the callback handler with NotModified error.
// It should be handled to get an image from cache, which is response of a manager object.
if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
return
}
self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
}
} else {
self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
}
})
}
}
}
/**
This method is exposed since the compiler requests. Do not call it.
*/
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let trustedHosts = trustedHosts where trustedHosts.contains(challenge.protectionSpace.host) {
let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
completionHandler(.UseCredential, credential)
return
}
}
completionHandler(.PerformDefaultHandling, nil)
}
}
其中最重要的是这两个函数:
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData)
前一个函数当每一次下载到数据的时候都会被调用,我们在该函数中,将每次得到的数据添加在当前 URL 所对应的 fetchLoad 的 responseData 中,之后,我们为传入的 progressBlock 提供当前下载进度。
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)
该函数在下载任务完成时会被调用,Kingfisher 在其中进行了各种错误处理,若数据成功下载,callbackWithImage 方法会被调用,返回下载到的图片、图片的 URL、以及下载到的原始数据。
这里我们注意到在图片下载成功之后,自身 ImageDownloaderDelegate 的代理方法会被调用,但我通过翻阅源码发现,并没有其他类接受了这个代理,delegate 始终为 nil,所以不对其进行讲解。
而且大概因为图片的解码操作也比较费时,Kingfisher 将函数主体放在了 processQueue 中执行以避免阻塞主线程。
KingfisherOptionsInfo
该文件主要用于接收配置 Kingfisher 行为的各种参数,包括缓存、下载、加载动画以及之前 KingfisherOptions 所包含的所有属性,代码如下:
/**
* KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. You can use the enum of option item with value to control some behaviors of Kingfisher.
*/
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
/**
Item could be added into KingfisherOptionsInfo
- Options: Item for options. The value of this item should be a KingfisherOptions.
- TargetCache: Item for target cache. The value of this item should be an ImageCache object. Kingfisher will use this cache when handling the related operation, including trying to retrieve the cached images and store the downloaded image to it.
- Downloader: Item for downloader to use. The value of this item should be an ImageDownloader object. Kingfisher will use this downloader to download the images.
- Transition: Item for animation transition when using UIImageView.
*/
public enum KingfisherOptionsInfoItem {
case Options(KingfisherOptions)
case TargetCache(ImageCache)
case Downloader(ImageDownloader)
case Transition(ImageTransition)
}
func ==(a: KingfisherOptionsInfoItem, b: KingfisherOptionsInfoItem) -> Bool {
switch (a, b) {
case (.Options(_), .Options(_)): return true
case (.TargetCache(_), .TargetCache(_)): return true
case (.Downloader(_), .Downloader(_)): return true
case (.Transition(_), .Transition(_)): return true
default: return false
}
}
extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
func kf_findFirstMatch(target: Generator.Element) -> Generator.Element? {
let index = indexOf {
e in
return e == target
}
return (index != nil) ? self[index!] : nil
}
}
这段代码之中有两个亮点,其一是借助了对 == 运算符的重载实现了判断传入参数类型的作用;其二是通过对 CollectionType 的拓展来为其添加迅速找出对应类型配置参数的功能,该函数中出现的第二个 == 运算符即使用到了上方对 == 的拓展用法,第一个 == 运算符用于判断 Generator.Element 的类型,其重载在 CollectionType 内部实现。
KingfisherManager
该类是 Kingfisher 的核心类,封装了之前讲到的 ImageCache、ImageDownloader 与 KingfisherOptionsInfo,集成了缓存以及下载两大功能,并直接为 UIImageView+Kingfisher 以及 UIButton+Kingfisher 提供操作方法。
该类的功能主要可以分为两部分:一是根据传入的 URL 返回对应的网络图片,二是解析传入的配置参数并对相关功能模块进行配置。
其中第二个部分又是第一个功能的组成部分,我们先来看第二部分,代码如下:
func parseOptionsInfo(optionsInfo: KingfisherOptionsInfo?) -> (Options, ImageCache, ImageDownloader) {
var options = KingfisherManager.DefaultOptions
var targetCache = self.cache
var targetDownloader = self.downloader
guard let optionsInfo = optionsInfo else {
return (options, targetCache, targetDownloader)
}
if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem {
let queue = optionsInOptionsInfo.contains(KingfisherOptions.BackgroundCallback) ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : KingfisherManager.DefaultOptions.queue
let scale = optionsInOptionsInfo.contains(KingfisherOptions.ScreenScale) ? UIScreen.mainScreen().scale : KingfisherManager.DefaultOptions.scale
options = (forceRefresh: optionsInOptionsInfo.contains(KingfisherOptions.ForceRefresh),
lowPriority: optionsInOptionsInfo.contains(KingfisherOptions.LowPriority),
cacheMemoryOnly: optionsInOptionsInfo.contains(KingfisherOptions.CacheMemoryOnly),
shouldDecode: optionsInOptionsInfo.contains(KingfisherOptions.BackgroundDecode),
queue: queue, scale: scale)
}
if let optionsItem = optionsInfo.kf_findFirstMatch(.TargetCache(self.cache)), case .TargetCache(let cache) = optionsItem {
targetCache = cache
}
if let optionsItem = optionsInfo.kf_findFirstMatch(.Downloader(self.downloader)), case .Downloader(let downloader) = optionsItem {
targetDownloader = downloader
}
return (options, targetCache, targetDownloader)
}
我们可以看到三个 if let 语句分别对应 KingfisherOptionsInfoItem 的前三种枚举配置类型,而配置 KingfisherOptions 相关参数的时候又用到了 OptionSetType 的协议拓展方法 contains,来获取对应属性的配置参数。对最后一种 ImageTransition 的配置, Kingfisher 放在了 UIImageView+Kingfisher 中进行。
接下来我们来讲第一部分,第一部分的主要功能函数有两个,代码如下:
func downloadAndCacheImageWithURL(URL: NSURL,
forKey key: String,
retrieveImageTask: RetrieveImageTask,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?,
options: Options,
targetCache: ImageCache,
downloader: ImageDownloader)
{
downloader.downloadImageWithURL(URL, retrieveImageTask: retrieveImageTask, options: options, progressBlock: { (receivedSize, totalSize) -> () in
progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
}) { (image, error, imageURL, originalData) -> () in
if let error = error where error.code == KingfisherError.NotModified.rawValue {
// Not modified. Try to find the image from cache.
// (The image should be in cache. It should be guaranteed by the framework users.)
targetCache.retrieveImageForKey(key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
completionHandler?(image: cacheImage, error: nil, cacheType: cacheType, imageURL: URL)
})
return
}
if let image = image, originalData = originalData {
targetCache.storeImage(image, originalData: originalData, forKey: key, toDisk: !options.cacheMemoryOnly, completionHandler: nil)
}
completionHandler?(image: image, error: error, cacheType: .None, imageURL: URL)
}
}
该函数负责下载传入URL所对应的网络图片并将其缓存,主要调用了 downloader.downloadImageWithURL 来下载所需图片数据,之后调用 targetCache.storeImage 来缓存数据。
public func retrieveImageWithResource(resource: Resource,
optionsInfo: KingfisherOptionsInfo?,
progressBlock: DownloadProgressBlock?,
completionHandler: CompletionHandler?) -> RetrieveImageTask
{
let task = RetrieveImageTask()
// There is a bug in Swift compiler which prevents to write `let (options, targetCache) = parseOptionsInfo(optionsInfo)`
// It will cause a compiler error.
let parsedOptions = parseOptionsInfo(optionsInfo)
let (options, targetCache, downloader) = (parsedOptions.0, parsedOptions.1, parsedOptions.2)
if options.forceRefresh {
downloadAndCacheImageWithURL(resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: completionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
} else {
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
// Break retain cycle created inside diskTask closure below
task.diskRetrieveTask = nil
completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
}
let diskTask = targetCache.retrieveImageForKey(resource.cacheKey, options: options, completionHandler: { (image, cacheType) -> () in
if image != nil {
diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: resource.downloadURL)
} else {
self.downloadAndCacheImageWithURL(resource.downloadURL,
forKey: resource.cacheKey,
retrieveImageTask: task,
progressBlock: progressBlock,
completionHandler: diskTaskCompletionHandler,
options: options,
targetCache: targetCache,
downloader: downloader)
}
})
task.diskRetrieveTask = diskTask
}
return task
}
首先我们对传入的配置参数使用 parseOptionsInfo 方法进行解析,如果 options.forceRefresh,被设置为 true,便直接调用 downloadAndCacheImageWithURL 方法下载并缓存该 URL 所对应的图片,若被设置为 false,则先调用 targetCache.retrieveImageForKey 尝试从缓存中取出所需图片,如果取不到,说明缓存中没有对应图片,则调用 downloadAndCacheImageWithURL 下载并缓存对应图片。
UIImageView+Kingfisher 以及 UIButton+Kingfisher
这两个类主要是对 UIImageView 和 UIButton进行拓展,功能的实现部分均为对 KingfisherManager 内相应函数的调用,Kingfisher 的文档内详细介绍了这两个类的对外拓展接口,这里就不赘述了。
不过其内部仍包含一个值得我们学习的知识点。
Associated Objects
Associated Objects(关联对象)或者叫作关联引用(Associative References),是作为Objective-C 2.0 运行时功能被引入到 Mac OS X 10.6 Snow Leopard(及iOS4)系统。与它相关在<objc/runtime.h>中有3个C函数,它们可以让对象在运行时关联任何值:
- objc_setAssociatedObject
- objc_getAssociatedObject
- objc_removeAssociatedObjects
我们并不能在类型拓展中放置存储属性,所以需要使用 Associated Objects 来向某些系统类(NSObject 的子类)中增添所需的属性。
这里将介绍其最简单的用法,代码如下:
private var lastURLKey: Void?
public extension UIImageView {
/// Get the image URL binded to this image view.
public var kf_webURL: NSURL? {
get {
return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
}
}
private func kf_setWebURL(URL: NSURL) {
objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
这里为 UIImageView 成功添加了 kf_webURL 属性,我们通过 kf_setWebURL 对其赋值,通过 kf_webURL 获取其值。
如果你想知道更多 Associated Objects 的相关内容,可以看这里。