关于Kingfisher--备用

序言--感谢好心大神分享

Kingfisher 是由 @onevcat 编写的用于下载和缓存网络图片的轻量级Swift工具库,其中涉及到了包括GCD、Swift高级语法、缓存、硬盘读写、网络编程、图像编码、图形绘制、Gif数据生成和处理、MD五、Associated Objects的使用等大量iOS开发知识。html

本文将详尽的对所涉及到的知识点进行讲解,但因为笔者水平有限,失误和遗漏之处在所不免,恳请前辈们批评指正。ios

1、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 的外部接口。git

2、UIImage+Extension

图片格式识别

Magic Number 是用于区分不一样文件格式,被放置于文件首的标记数据。Kingfisher 中用它来区分不一样的图片格式,如PNG、JPG、GIF。代码以下:github

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 } }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 里提供了用于解码的函数,代码以下:swift

// 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动图。

3、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 = (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 = (