标签:
Kingfisher 是由 @onevcat 编写的用于下载和缓存网络图片的轻量级Swift工具库,其中涉及到了包括GCD、Swift高级语法、缓存、硬盘读写、网络编程、图像编码、图形绘制、Gif数据生成和处理、MD5、Associated Objects的使用等大量iOS开发知识。
本文将详尽的对所涉及到的知识点进行讲解,但由于笔者水平有限,失误和遗漏之处在所难免,恳请前辈们批评指正。
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 的外部接口。
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 绘制,返回正立后的图片。
我们知道,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。
我们并不能像其他格式的图片一样直接传入 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动图。
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