0%

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

1. 基本使用

1.1 通过 Resource 设置图片

Kingfisher 中内置的 ImageResource 和 URL 实现了 Resource 协议,ImageResource 和 URL 的区别是 ImageResource 可自定义 cacheKey。

  • URL 设置图片
1
let url = URL(string: "https://test/image.jpg")!
2
imageView.kf.setImage(with: url)
  • ImageResource
1
let imageResource = ImageResource(downloadURL: url, cacheKey: "custom_cache_key")
2
imageView.kf.setImage(with: imageResource)
1.2 通过 ImageDataProvider 设置图片

Kingfisher 内置了 LocalFileImageDataProvider,Base64ImageDataProvider,RawImageDataProvider 三种 ImageDataProvider。

  • LocalFileImageDataProvider
1
let fileUrl = Bundle.main.url(forResource: "image", withExtension: "jpg")!
2
let imageDataProvider =  LocalFileImageDataProvider(fileURL: fileUrl)
3
imageView.kf.setImage(with: imageDataProvider)
  • Base64ImageDataProvider
1
let base64String = "...."
2
let base64ImageDataProvider = Base64ImageDataProvider(base64String: base64String, cacheKey: "base64_cache_key")
3
imageView.kf.setImage(with: base64ImageDataProvider)
  • RawImageDataProvider
1
let data = Data()
2
let dataImageDataProvider = RawImageDataProvider(data: data, cacheKey: "data_cache_key")
3
imageView.kf.setImage(with: dataImageDataProvider)
  • 自定义 ImageDataProvider
1
//定义
2
public struct FileNameImageDataProvider : ImageDataProvider {
3
    public let cacheKey: String
4
    public let fileName: String
5
    public init(fileName: String, cacheKey: String? = nil) {
6
        self.fileName = fileName
7
        self.cacheKey = cacheKey ?? fileName
8
    }
9
10
    public func data(handler: @escaping (Result<Data, Error>) -> Void) {
11
        if let fileURL = Bundle.main.url(forResource: fileName, withExtension: "") {
12
            handler(Result(catching: { try Data(contentsOf: fileURL) }))
13
        }else {
14
            let error = NSError(domain: "文件不存在", code: -1, userInfo: ["fileName":fileName])
15
            handler(.failure(error))
16
        }
17
    }
18
}
19
//使用
20
let fileNameImageDataProvider = FileNameImageDataProvider(fileName: "image.jpg")
21
imageView.kf.setImage(with: fileNameImageDataProvider)
1.3 展示 placeholder
  • 使用 UIImage 设置 placeholder
1
let placeholderImage = UIImage(named: "placeholder.png")
2
imageView.kf.setImage(with: url, placeholder: placeholderImage)
  • 通过自定义 View 设置 placeholder
1
// 定义
2
// 需要使自定义View遵循Placeholder协议
3
// 可以什么都不实现,是因为当Placeholder为UIview的时候有默认实现
4
class PlaceholderView: UIView, Placeholder {
5
}
6
// 使用
7
let placeholderView = PlaceholderView()
8
imageView.kf.setImage(with: url, placeholder: placeholderView)
1.4 加载 GIF 图
  • 通过 UIImageView 加载 GIF 图
1
let url = URL(string: "https://test/image.gif")!
2
imageView.kf.setImage(with: url)
  • 通过 AnimatedImageView 加载 GIF 图
1
let url = URL(string: "https://test/image.gif")!
2
animatedImageView.kf.setImage(with: url)

上面二者的区别请参考Kingfisher 源码解析之加载动图

1.5 设置指示器
  • 不使用指示器
1
imageView.kf.indicatorType = .none
  • 使用 UIActivityIndicatorView 作为指示器
1
imageView.kf.indicatorType = .activity
  • 使用图片作为指示器
1
let path = Bundle.main.path(forResource: "loader", ofType: "gif")!
2
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
3
imageView.kf.indicatorType = .image(imageData: data)
  • 使用自定义 View 作为指示器
1
// 定义
2
struct CustomIndicator: Indicator {
3
    let view: UIView = UIView()
4
    func startAnimatingView() { view.isHidden = false }
5
    func stopAnimatingView() { view.isHidden = true }
6
    init() {
7
        view.backgroundColor = .red
8
    }
9
}
10
// 使用
11
let indicator = CustomIndicator()
12
imageView.kf.indicatorType = .custom(indicator: indicator)
1.6 设置 transition

transition 用于图片加载完成之后的展示动画,有以下类型

1
public enum ImageTransition {
2
    // 无动画
3
    case none
4
    // 相当于UIView.AnimationOptions.transitionCrossDissolve
5
    case fade(TimeInterval)
6
    // 相当于UIView.AnimationOptions.transitionFlipFromLeft
7
    case flipFromLeft(TimeInterval)
8
    // 相当于UIView.AnimationOptions.transitionFlipFromRight
9
    case flipFromRight(TimeInterval)
10
    // 相当于UIView.AnimationOptions.transitionFlipFromTop
11
    case flipFromTop(TimeInterval)
12
    // 相当于UIView.AnimationOptions.transitionFlipFromBottom
13
    case flipFromBottom(TimeInterval)
14
    // 自定义动画
15
    case custom(duration: TimeInterval,
16
                 options: UIView.AnimationOptions,
17
              animations: ((UIImageView, UIImage) -> Void)?,
18
              completion: ((Bool) -> Void)?)
19
}

使用方式

1
imageView.kf.setImage(with: url, options: [.transition(.fade(0.2))])

2. Processor

2.1 DefaultImageProcessor

将下载的数据转换为相应的 UIImage。支持 PNG,JPEG 和 GIF 格式。

2.2 BlendImageProcessor

修改图片的混合模式(这里不知道这么描述对不对),核心实现如下

  1. 首先利用 DefaultImageProcessor 把 Data 转成 image,然后去绘制
  2. 获取上下文
  3. 为上下文填充背景色
  4. 调用 image.draw 函数设置混合模式
  5. 从上下文中获取图片为 processedImage
  6. 释放上下文,并返回 processedImage
1
let image = 处理之前的图片
2
UIGraphicsBeginImageContextWithOptions(size, false, scale)
3
let context = UIGraphicsGetCurrentContext()
4
let rect = CGRect(origin: .zero, size: size)
5
backgroundColor.setFill()
6
UIRectFill(rect)
7
image.draw(in: rect, blendMode: blendMode, alpha: alpha)
8
let cgImage = context.makeImage()
9
let processedImage = UIImage(cgImage: cgImage, scale: scale, orientation: image.orientation)
10
UIGraphicsEndImageContext()
11
return processedImage
2.3 OverlayImageProcessor

在 image 上添加一层覆盖,其本质也是混合模式,逻辑大致同上

2.4 BlurImageProcessor

给图片添加高斯模糊,用 vimage 实现

2.5 RoundCornerImageProcessor

给图片添加圆角,支持四个角进行相互组合,使用方式如下

1
// 设置四个角的圆角
2
imageView.kf.setImage(with: url, options: [.processor(RoundCornerImageProcessor(cornerRadius: 20))])
3
// 给最上角和右下角设置圆角
4
imageView.kf.setImage(with: url, options: [.processor(RoundCornerImageProcessor(cornerRadius: 20
5
                                            ,roundingCorners: [.topLeft, .bottomRight]))])

实现方式:利用贝塞尔曲线设置一下带圆角的圆角矩形,然后对图片进行裁剪

1
let path = UIBezierPath(
2
                roundedRect: rect,
3
                byRoundingCorners: corners.uiRectCorner,//此参数表示是哪个圆角
4
                cornerRadii: CGSize(width: radius, height: radius)
5
            )
6
context.addPath(path.cgPath)
7
context.clip()
8
image.draw(in: rect)
2.6 TintImageProcessor

用颜色给图像主色,实现方式是利用 CoreImage 中的 CIFilter,使用了这 2 个CIFilter(name: "CIConstantColorGenerator")CIFilter(name: "CISourceOverCompositing")

2.7 ColorControlsProcessor

修改图片的对比度,曝光度,亮度,饱和度,实现方式是利用 CoreImage 中的 CIFilter,使用了这 2 个CIColorControlsCIExposureAdjust

2.8 BlackWhiteProcessor

使图像灰度化,是 ColorControlsProcessor 的特例

2.9 CroppingImageProcessor

对图片进行裁剪

2.10 DownsamplingImageProcessor

对图片下采样,一般在较小的 imageView 展示较大的高清图
核心实现:

1
public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? {
2
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
3
    guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
4
        return nil
5
    }
6
7
    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
8
    let downsampleOptions = [
9
        kCGImageSourceCreateThumbnailFromImageAlways: true,
10
        kCGImageSourceShouldCacheImmediately: true,
11
        kCGImageSourceCreateThumbnailWithTransform: true,
12
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
13
    guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
14
        return nil
15
    }
16
    return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil)
17
}
2.11 GeneralProcessor

用于组合多个已有的 Processor,使用方式如下,最终都会转成 GeneralProcessor

1
// 使用方式1
2
let processor1 = BlurImageProcessor(blurRadius: 5)
3
let processor2 = RoundCornerImageProcessor(cornerRadius: 20)
4
let generalProcessor = GeneralProcessor(identifier: "123") { (item, options) -> KFCrossPlatformImage? in
5
   if let image = processor1.process(item: item, options: options) {
6
       return processor2.process(item: .image(image), options: options)
7
   }
8
   return nil
9
}
10
// 使用方式2,此方法是Processor的扩展
11
let generalProcessor = BlurImageProcessor(blurRadius: 5).append(RoundCornerImageProcessor(cornerRadius: 20)_
12
// 使用方式3,自定义的操作符,调用了append方法
13
let generalProcessor = BlurImageProcessor(blurRadius: 5) |> RoundCornerImageProcessor(cornerRadius: 20)
2.12 自定义 Processor

参考Kingfisher 源码解析之 Processor 和 CacheSerializer

3 缓存

3.1 使用自定义的 cacheKey

通常情况下,会直接通过 URL 去加载图片,这个时候 cacheKey 是 URL.absoluteString,也可使用 ImageResource 自定义 cacheKey

3.2 通过 cacheKey 判断是否缓存,以及缓存的类型

cacheType 是一个枚举,有三个 case:.none 未缓存,.memory 存在内存缓存,.disk 存在磁盘缓存。
需要说明的是 cacheKey+processor.identifier 才是缓存的唯一标识符,只是 DefaultImageProcessor 的 identifier 为空字符串,若是在加载的时候指定了非 DefaultImageProcessor 的 Processor,则在查找的时候需要指定 processorIdentifier

1
let cache = ImageCache.default
2
let isCached = cache.isCached(forKey: cacheKey)
3
let cacheType = cache.imageCachedType(forKey: cacheKey)
4
// 若是指定了Processor,可使用此方法查找缓存
5
cache.isCached(forKey: cacheKey, processorIdentifier: processor.identifier)
3.3 通过 cacheKey,从缓存中获取图片
1
cache.retrieveImage(forKey: "cacheKey") { result in
2
    switch result {
3
    case .success(let value):
4
        print(value.cacheType)
5
        print(value.image)
6
    case .failure(let error):
7
        print(error)
8
    }
9
}
3.4 设置缓存的配置
3.4.1 设置内存缓存的容量限制(默认值设置物理内存的四分之一)
1
cache.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024
3.4.2 设置内存缓存的个数限制
1
cache.memoryStorage.config.countLimit = 150
3.4.3 设置内存缓存的过期时间(默认值是 300 秒)
1
cache.memoryStorage.config.expiration = .seconds(300)

也可指定某一个图片的内存缓存

1
imageView.kf.setImage(with: url, options:[.memoryCacheExpiration(.never)])
3.4.4 设置内存缓存的过期时间更新策略

更新策略是一个枚举,有三个 case,.none 过期时间不更新,.cacheTime 在当前时间上加上过期时间,.expirationTime(_ expiration: StorageExpiration)过期时间更新为指定多久之后过期。默认值是.cacheTime,使用方式如下

1
imageView.kf.setImage(with: url, options:[.memoryCacheAccessExtendingExpiration(.cacheTime)])
3.4.5 设置内存缓存清除过期内存的时间间隔(此值是不可变的,只可在初始化时赋值)
1
cache.memoryStorage.config.cleanInterval = 120
3.4.6 设置磁盘缓存的容量
1
cache.diskStorage.config.sizeLimit =  = 500 * 1024 * 1024
3.4.7 设置磁盘缓存的过期时间和过期时间更新策略

同内存缓存

3.5 手动的缓存图片
1
// 普通缓存
2
let image: UIImage = //...
3
cache.store(image, forKey: cacheKey)
4
5
// 缓存原始数据
6
let data: Data = //...
7
let image: UIImage = //...
8
cache.store(image, original: data, forKey: cacheKey)
3.6 清除缓存
3.6.1 删除指定的缓存
1
forKey: cacheKey,
2
processorIdentifier: processor.identifier,
3
fromMemory: false,//是否才能够内存缓存中删除
4
fromDisk: true //是否从磁盘缓存中删除){}
3.6.2 清空内存缓存,清空过期的内存缓存
1
// 清空内存缓存
2
cache.clearMemoryCache()
3
// 清空过期的内存缓存
4
cache.cleanExpiredMemoryCache()
3.6.3 清空磁盘缓存,清空过期的磁盘缓存和超过磁盘容量限制的缓存
1
// 清空磁盘缓存
2
cache.clearDiskCache()
3
// 清空过期的磁盘缓存和超过磁盘容量限制的缓存
4
cache.cleanExpiredDiskCache()
3.7 获取磁盘缓存大小
1
cache.calculateDiskStorageSize()

4. 下载

4.1 手动下载图片
1
let downloader = ImageDownloader.default
2
downloader.downloadImage(with: url) { result in
3
    switch result {
4
    case .success(let value):
5
        print(value.image)
6
    case .failure(let error):
7
        print(error)
8
    }
9
}
4.2 在发送请求之前,修改 Request
1
// 定义一个requestModifier
2
let modifier = AnyModifier { request in
3
    var r = request
4
    r.setValue("abc", forHTTPHeaderField: "Access-Token")
5
    return r
6
}
7
// 在手动下载时设置
8
downloader.downloadImage(with: url, options: [.requestModifier(modifier)]) {
9
}
10
// 在imageView的setImage的options里设置
11
imageView.kf.setImage(with: url, options: [.requestModifier(modifier)])
4.3 设置超时时间
1
downloader.downloadTimeout = 60
4.4 处理重定向
1
// 定义一个重定向的处理逻辑
2
let anyRedirectHandler = AnyRedirectHandler { (task, resp, req, completionHandler) in
3
        completionHandler(req)
4
}
5
// 在手动下载时设置
6
downloader.downloadImage(with: url, options: [.redirectHandler(anyRedirectHandler)])
7
// 在imageView的setImage的options里设置
8
imageView.kf.setImage(with: url,  options: [.redirectHandler(anyRedirectHandler)])
4.5 取消下载
1
// 取消手动下载
2
let task = downloader.downloadImage(with: url) { result in
3
}
4
task?.cancel()
5
6
// 取消imageView的下载
7
let task = imageView.kf.set(with: url)
8
task?.cancel()

5. 预加载

使用方式如下,具体可参考Kingfisher 源码解析之 ImagePrefetcher

1
let urls = ["https://example.com/image1.jpg", "https://example.com/image2.jpg"]
2
           .map { URL(string: $0)! }
3
let prefetcher = ImagePrefetcher(urls: urls)
4
prefetcher.start()

7. 一些有用的 options

  • loadDiskFileSynchronously 从磁盘中加载时,是否同步的去加载
  • onlyFromCache 是否只从缓存中加载
  • cacheMemoryOnly 是否只使用内存缓存
  • forceRefresh 是否强制刷新,若值为 true,则每次都会重新下载
  • backgroundDecode 是否在子线程去解码
  • …其他配置请参考Kingfisher 源码解析之 Options 解释

1. 命名空间的使用

Kingfisher 命名空间有 2 种方式,一种是使用协议加上包装器,另一种是把类型定义在枚举里。参考协议KingfisherCompatible和类型public enum MemoryStorage {},一个是空协议,扩展了一个 kf 计算属性,但 kf 里又有个 base 指向自己,另一个是空枚举,定义了带有自己命名空间的类型,Alamofire5.0中使用就是第二种

2. 对枚举的了解

Kingfisher 使用了大量的枚举,我以前以为枚举就是为了区分状态,以提高代码的可读性,现在的理解是枚举定义了含义相同,但行为策略可能不同的一组值,比如KingfisherOptionsInfoItem定义了 Kingfisher 的各种配置,但每种配置的意义可能完全不相同,但都是配置,再ExpirationExtending定义了如何更新过期时间,

3. 协议对提高扩展性的重要作用

协议是定义了某种能力,由协议遵循者去实现这些能力,但是由于 Swift 中协议扩展的存在,就可以让协议自己就提供某些能力,只要让协议遵循者去遵循协议,就能自动获取这些能力,减少了遵循协议的复杂性。并且协议仅仅定义了某种能力,不涉及具体类型,更方面的去扩展。我比较喜欢的协议又ResourcePlaceholder,Processor

4. OptionSet 的使用

OptionSet 类似于 OC 的按位枚举,OptionSet 遵循了 RawRepresentable,需要提供了 rawValue 值,OptionSet 还遵循了 SetAlgebra 可以很方便的数组字面量进行赋值,我比较喜欢这个特性,第一次看到这么写public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight],还挺迷糊的,怎么能把一个数组赋值结构体呢

5. 加载高清大图的加载

高清大图如果直接加载,会占用较高的内存,为了减少内存的加载,可以使用下采样进行加载高清大图,这也是 Kingfisher 的 demo 中展示高清图的方式,这种方式有个弊端,就是如果我们需要对高清图进行方法展示的,可能会变的模糊,这时候可使用CATiledLayer展示高清图,它可分段绘制

6. 加载 GIF 的 2 中方式

  1. 从 GIF 图中获取所有的图片,利用 UIImage.animateImage()生成一个动图,赋值给 UIImageView
  2. 自定义 ImageView 继承自 UIImageView,实现一个定时器根据相应的时间,展示 GIF 图对应的那一帧图片

7. 判断图片格式的原理

一般图片格式的都在 data 的前几个字节里,只要按对应的规则去取,然后进行判断就行了

8. 在子线程对图片进行解码

在子线程对图片进行解码就是在子线程里把 UIImage 里画到一个画布上,从画布上取出画好的图片

9. defer 对提升代码简洁的帮助

从下图中可看到 context 创建了 2 此,需要释放 2 次,释放只能在绘画之后才能释放,如果不用 defer,你会怎么写

10. Kingfisher 使用方式如此简单,但也很方面的进行很多设置

这里是因为对于每个配置项都有一个默认值或者对配置项为 nil 做了默认处理。这里还想说一下,我们在配置 options 的时候,options 的类型是public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem],Kingfisher 中真正用的时候是结构体KingfisherParsedOptionsInfo ,由于配置项属性太多,直接使用结构体,初始化结构体的时候不方便,而使用数组,再通过遍历生成KingfisherParsedOptionsInfo,会方便很多,为喵神的细节处理点赞

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

ImagePrefetcher 提供了哪些功能

ImagePrefetcher 是 Kingfisher 提供预加载功能的一个类,提供了一下功能

  • start():开启预加载
  • stop():停止预加载
  • maxConcurrentDownloads:设置最大缓存并发量
  • progressBlock 和 progressSourceBlock:缓存进度的回调
  • completionHandler 和 completionSourceHandler:缓存结束的回调

ImagePrefetcher 预加载的流程图

ImagePrefetcher预加载的流程图

ImagePrefetcher 两个问题

当调用 stop()函数之后的逻辑

先来看下 stop 函数的实现,实现比较简单,在预加载的队列里异步的执行把标志位 stopped 设置为 true,并且取消当前所有未完成的下载任务,看起来很简单。

1
public func stop() {
2
    pretchQueue.async {
3
        if self.finished { return }
4
        self.stopped = true
5
        self.tasks.values.forEach { $0.cancel() }
6
    }
7
}

但是 stopped 这个标志位只在网络请求结束的回调里去判断了,这就会发生一些歧义,交给读者去判断 Kingfisher 这么做是否是合理的?当调用 stop 函数时,会出现以下几种情况以及对应的结果

  1. 调用 stop 时,已经预加载结束了,由于已经结束,会直接返回
  2. 调用 stop 时,现在已经有正在下载图片的任务了,会取消所所有请求,然后请求就会走结束的回调,在结束的回调里把剩下的未加载的数据放入到失败的数据源的数组中,调用结束回调
  3. 调用 stop 时,还没有正在下载的任务,会继续预加载数据,直到结束,或者有一个请求结束

对于情况 1 和情况 2 都是合理的,并且是绝大部分都会是情况 1 和情况 2,对于情况 3,调用 stop 时并没有真正的去停止,但是这种情况也是较少出现的。

对于 stop 方法,喵神的注释是这样的

/// Stops current downloading progress, and cancel any future prefetching activity that might be occuring.

缓存进度和缓存结束的回调为什么要各有 2 个

我第一次看代码,就想为什么要有 2 个呢?为什么这么设计呢?这里以缓存进度的回调举例,它们两个的原因是一样的。先来看下定义,

1
public typealias PrefetcherProgressBlock =
2
    ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
3
4
public typealias PrefetcherSourceProgressBlock =
5
    ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)

我们发现基本是一样的,只是回调里的参数类型不一样,一个 Resource,另一个 Source。如果你对这 2 个类型比较了解,想必你应该能猜到这么设计的原因了。

Source 是一个枚举,Kingfisher 中为 UIImage 提供数据源用的,定义如下,有 2 个 case,一个是关联了 Resource,另一个关联了 ImageDataProvider

1
public enum Source {
2
    case network(Resource)
3
    case provider(ImageDataProvider)
4
}

Resource 是一个协议,定义如下,提供数据源的真正类型之一,一般用于加载网络图片

1
public protocol Resource {
2
    var cacheKey: String { get }
3
    var downloadURL: URL { get }
4
}

ImageDataProvider 也是一个协议,定义如下,提供数据源的另一个真正类型,一般用于本地图片

1
public protocol ImageDataProvider {
2
    var cacheKey: String { get }
3
    func data(handler: @escaping (Result<Data, Error>) -> Void)
4
}

回答上面的问题,由于我们一般情况下预加载的都是网络图片,因此提供一个方便我们使用的回调,但为了覆盖到所有情况,就提供了 2 个情况的回调,这个在 ImagePrefetcher 的便利初始化方法里我们就能看出来,当使用[URL](注:在 URL 的扩展里实现了 Resource 协议)或者[Resource]初始化的时候,就使用 PrefetcherProgressBlock,当使用[Source]初始化时,就使用的 PrefetcherSourceProgressBlock

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

本篇文章主要介绍 Processor 和 CacheSerializer 的基本定义和调用时机,以及利用二者扩展 Kingfisher 以支持 webp 格式的图片

Processor

Processor 介绍

Kingfisher 中 Processor 是一个协议,定义了对原始数据进行加工处理转换成 UIImage 的能力(Kingfisher 缓存的是处理成功之后的 UIImage,根据 options 的值来决定是否缓存原始数据)。
这里的原始数据是指 ImageProcessItem,它是一个枚举类型。Processor 和 ImageProcessItem 定义如下,都是特别简单

1
public enum ImageProcessItem {
2
    case image(KFCrossPlatformImage)
3
    case data(Data)
4
}
5
public protocol ImageProcessor {
6
    //标识符,在缓存的时候用到,用于区分原始数据和处理加工之后的数据的
7
    var identifier: String { get }
8
    //交给具体的实现类去实现,ImageProcessItem,最终返回一个UIImage
9
    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
10
}
关于 Processor 的两个问题

如果你了解过 Kingfisher,请尝试回答下这 2 个问题

  1. ImageProcessor.process 都在什么时候调用呢?
  2. ImageProcessItem 关联了 2 种类型,一种是 Data,另一种是 UIImage,那么这 2 种类型分别什么时候会用到呢?

ImageProcessor.process 在什么时候调用,在调用的时候会传递什么类型的数据?

  1. 当从网络上下载图片成功之后,会调用 process 把下载成功的 data 加工处理成我们需要的 UIImage。很明显这种情况下传递的是 Data 类型。
  2. 当 source 是 ImageDataProvider 时,从 source 中获取到 Data 之后,会调用 process 把 data 加工处理成我们需要的 UIImage。很明显这种情况下传递的也是 Data 类型。
  3. 当读取缓存失败,但读取原始数据缓存成功之后,会调用 process 把原始数据加工处理成我们需要的 UIImage。这种情况会先把读取到的 data 使用 cacheSerializer 反序列化为 UIImage,然后传递 UIImage 类型

CacheSerializer

CacheSerializer 介绍

Kingfisher 中 CacheSerializer 定义了图片序列化和反序列化的能力,也是一个协议

1
public protocol CacheSerializer {
2
    func data(with image: KFCrossPlatformImage, original: Data?) -> Data?
3
    func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
4
}
CacheSerializer 的调用时机
  1. 当需要磁盘缓存时,会调用func data(with image: KFCrossPlatformImage, original: Data?) -> Data?把 image 序列化成 data,以便写入文件
  2. 当从磁盘读取数据时,会调用func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?把 data 反序列化为 UIImage

使用 Processor 和 CacheSerializer 扩展 Kingfisher,使 Kingfisher 支持 webP 格式的图片

Kingfisher 本身是不支持 webp 格式的图片,但是可以利用 Processor 和 CacheSerializer 对 Kingfisher 进行扩展,让 Kingfisher 支持 webP 格式的图片

WebP 标准是 Google 定制的,迄今为止也只有 Google 发布的 libwebp 实现了该的编解码 。 所以这个库也是该格式的事实标准。

因此要想支持 webp 格式的图片,需要依赖 libwebp 库,用来实现图片的编码和解码,对于这块的代码我是从SDWebImageWebPCoder复制过来的,并且去掉了对动图的支持和一些 SD 配置的代码,如果你对这块感兴趣,请参考源码,由于 SD 是 OC 写的,所以这部分我用的也是 OC,最终给 UIImage 添加了一个分类,提供了下面 2 个方法

1
@interface UIImage (WebP)
2
//序列化为Data
3
@property(nonatomic,strong,readonly,nullable) NSData *webPData;
4
//通过data反序列化为UIImage
5
+ (nullable instancetype)imageWithWebPData:(NSData *)webPdata;
6
+
7
@end
实现 Processor

在 process 判断 item 的类型,若是 image 则直接返回,若是 data 则反序列化为 UIImage

1
public struct WebPProcessor: ImageProcessor {
2
    public static let `default` = WebPProcessor()
3
    public let identifier = "WebPProcessor"
4
    public init() {}
5
6
    public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
7
        switch item {
8
        case .image(let image):
9
            return image
10
        case .data(let data):
11
            return UIImage(webPData: data)
12
        }
13
    }
14
}
CacheSerializer
1
public struct WebPCacheSerializer: CacheSerializer {
2
    public static let `default` = WebPCacheSerializer()
3
    private init() {}
4
5
    public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
6
        return image.webPData;
7
    }
8
9
    public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
10
        return UIImage(webPData: data);
11
    }
12
}
使用
1
if let url = URL(string:"http://q21556z4z.bkt.clouddn.com/123.webp?e=1575537931&token=7n8bncOpnUSrN4mijeEAJRdVXnC-jm-mk5qTjKjR:L1_MWy3xugv9ct6PD294CHzwiSE=&attname=") {
2
    imageView.kf.setImage(
3
        with: url,
4
        options: [.processor(WebPProcessor.default), .cacheSerializer(WebPCacheSerializer.default)]
5
    )
6
}

补充

虽说上面的代码都比较简单,但是我感觉 Kingfisher 的这个设计真的挺好的,可扩展支持任意类型的图片,并且 Processor 是用来加工处理图片的,能做的还有其他方面,比如 Kingfisher 中提供了多种实现类,比如圆角的 RoundCornerImageProcessor,显示高清图的 DownsamplingImageProcessor,组装多种 Processor 的 GeneralProcessor。
demo 地址

参考

SDWebImageWebPCoder
移动端图片格式调研
libwebp 地址

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

Kingfisher 中 ImageCache 里提供内存缓存和磁盘缓存,分别是MemoryStorage.Backend<KFCrossPlatformImage>DiskStorage.Backend<Data>来实现的,注:内存缓存和磁盘缓存都是通过class Backend,不过这 2 个类,是完全不同的类,使用枚举来充当命名空间来区分的,分别定义在MemoryStorage.swiftDiskStorage.swift

内存缓存

内存缓存一共有三个类构成,Backend提供缓存的功能,Config提供缓存的配置项,StorageObject<T>缓存的封装类型

Config的主要内容
1
public struct Config {
2
    //内存缓存的最大容量,ImageCache.default中提供的默认值是设备物理内存的四分之一
3
    public var totalCostLimit: Int
4
    //内存缓存的最大长度
5
    public var countLimit: Int = .max
6
    //内存缓存的的过期时长
7
    public var expiration: StorageExpiration = .seconds(300)
8
    //清除过期缓存的时间间隔
9
    public let cleanInterval: TimeInterval
10
}
StorageObject<T>的主要内容
1
class StorageObject<T> {
2
      //缓存的真正的值
3
      let value: T
4
      //存活时间,也就是多久之后过期
5
      let expiration: StorageExpiration
6
      //缓存e的key
7
      let key: String
8
      //过期时间,默认值是当前时间加上expiration
9
      private(set) var estimatedExpiration: Date
10
      // 更新过期时间
11
      func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
12
          switch extendingExpiration {
13
          case .none://不更新过期时间
14
              return
15
          case .cacheTime://把过期时间设置为当前时间加上存活时间
16
              self.estimatedExpiration = expiration.estimatedExpirationSinceNow
17
          case .expirationTime(let expirationTime)://把过期时间设置为指定时间
18
              self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
19
          }
20
      }
21
      // 是否已经过期
22
      var expired: Bool {
23
          //estimatedExpiration.isPast 是对Date的一个扩展方法,判断estimatedExpiration是否小于当前时间
24
          return estimatedExpiration.isPast
25
      }
26
}
Backend的主要内容
1
public class Backend<T: CacheCostCalculable> {
2
    //使用NSCache进行缓存
3
    let storage = NSCache<NSString, StorageObject<T>>()
4
    //存放所有缓存的key,在删除过期缓存是有用
5
    var keys = Set<String>()
6
    //定时器,用于定时清除过期数据
7
    private var cleanTimer: Timer? = nil
8
    //配置项
9
    public var config: Config
10
    ...下面还有一些缓存数据,读取数据,删除缓存,是否已缓存,删除过期数据等方法
11
}

由上面我们可以看出,Kingfisher 中内存缓存是用 NSCache 实现的,NSCache 是一个类似于 Dictionary 的类,拥有相似的 API,不过区别于 Dictionary 的是,NSCache 是线程安全的,并且提供了设置最大缓存个数和最大缓存大小的配置,Backend 就是通过设置 NSCache 的countLimittotalCostLimit来实现最大缓存个数和最大缓存大小。

通过下面的代码,看下 Backend 是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过期数据的?代码中有详细的注释,注:下面的代码删除了一些非核心代码,比如异常,加锁保证线程安全等

缓存数据
1
func store(value: T,forKey key: String,expiration: StorageExpiration? = nil) {
2
    //获取存活时间,若缓存时没设置,则从配置中获取
3
    let expiration = expiration ?? config.expiration
4
    //判断是否过期,若已经过期直接返回
5
    guard !expiration.isExpired else { return }
6
    //把要缓存的值封装成StorageObject类型
7
    let object = StorageObject(value, key: key, expiration: expiration)
8
    //把结果缓存起来
9
    storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
10
    //把key保存起来
11
    keys.insert(key)
12
}
读取数据,判断数据是否已缓存
1
// 读取数据
2
func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
3
    //从NSCache中获取数据,如获取不到直接返回nil
4
    guard let object = storage.object(forKey: key as NSString) else { return nil }
5
    //判断是否过期,若过期直接返回nil
6
    if object.expired { return nil }
7
    //去更新过期时间
8
    object.extendExpiration(extendingExpiration)
9
    return object.value
10
}
11
// 判断是否缓存,其本质就是去读取数据,只是不更新缓存时间,若取到,则已缓存,否则未缓存
12
func isCached(forKey key: String) -> Bool {
13
    guard let _ = value(forKey: key, extendingExpiration: .none) else {
14
        return false
15
    }
16
    return true
17
}
删除缓存
1
 func remove(forKey key: String) throws {
2
    storage.removeObject(forKey: key as NSString)
3
    keys.remove(key)
4
}
删除过期数据,这里使用 Set 存储 key 的原因是 NSCache,并没有像 Dictionary 一样提供获取 allKeys 或 allValues 的方法
1
func removeExpired() {
2
    for key in keys {
3
        let nsKey = key as NSString
4
        //通过key获取数据,若获取失败,则删除从keys中删除key
5
        guard let object = storage.object(forKey: nsKey) else {
6
            keys.remove(key)
7
            continue
8
        }
9
        //判断object是否过期,若过期,则从cache中删除数据,从keys中删除key
10
        if object.estimatedExpiration.isPast {
11
            storage.removeObject(forKey: nsKey)
12
            keys.remove(key)
13
        }
14
    }
15
}

磁盘缓存

Kingfisher 中磁盘缓存是通过文件系统来实现的,也就是说每个缓存的数据都对应一个文件,其中 Kingfisher 把文件的创建时间修改为最后一次读取的时间,把文件的修改时间修改为过期时间。

磁盘缓存一共有三个类构成,Backend提供缓存的功能,Config提供缓存的配置项,FileMeta存储着文件信息。

Config的主要内容
1
public struct Config {
2
    //磁盘缓存占用磁盘的最大值,为0z时,表示不限制
3
    public var sizeLimit: UInt
4
    //存活时间
5
    public var expiration: StorageExpiration = .days(7)
6
    //文件的扩展名
7
    public var pathExtension: String? = nil
8
    //是否需要把文件名哈希
9
    public var usesHashedFileName = true
10
    //操作文件的FileManager
11
    let fileManager: FileManager
12
    //文件缓存所在的文件夹,默认在cache文件夹里
13
    let directory: URL?
14
}
FileMeta的主要内容
1
struct FileMeta {
2
    //文件路径
3
    let url: URL
4
    //文件最后一次读取时间
5
    //这个在超过sizeLimit大小时,需要删除文件时,用此属性进行排序,把时间较早的删除掉
6
    let lastAccessDate: Date?
7
    //过期时间
8
    let estimatedExpirationDate: Date?
9
    //是否是个文件夹
10
    let isDirectory: Bool
11
    //文件大小
12
    let fileSize: Int
13
}
Backend的主要内容
1
public class Backend<T: DataTransformable> {
2
    //配置信息
3
    public var config: Config
4
    //写入文件所在的文件夹,默认在cache文件夹里
5
    public let directoryURL: URL
6
    //修改文件原信息时,所在的队列
7
    let metaChangingQueue: DispatchQueue
8
     //该方法会在init着调用,保证directoryURLs文件夹,已经被创建过了
9
    func prepareDirectory() throws {
10
        let fileManager = config.fileManager
11
        let path = directoryURL.path
12
        guard !fileManager.fileExists(atPath: path) else { return }
13
        try fileManager.createDirectory(atPath: path,withIntermediateDirectories: true,attributes: nil)
14
    }
15
    ...下面还有缓存数据,读取数据,判断是否已缓存,删除缓存,删除过期缓存,删除超过sizeLimit的缓存,统计缓存大小等
16
}

通过下面的代码看Backend是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过期缓存,删除超过 sizeLimit 的缓存,统计缓存大小以及如何通过 key 生成文件名的?代码中有详细的注释。注:下面的代码删除了一些非核心代码,比如异常,加锁保证线程安全等

通过 key 生成文件名

下面那段代码和源码中不太一样,但逻辑是一样的,我改成这样是因为方面我描述

1
//首先判断是否使用key的MD5值当做文件名,若是,则把filename设置成key.MD5
2
//然后再判断是否设置了扩展名,若设置了,则把扩展名拼接到filename上
3
func cacheFileName(forKey key: String) -> String {
4
    var filename = key
5
    if config.usesHashedFileName {
6
        filename = key.kf.md5
7
    }
8
    if let ext = config.pathExtension {
9
        filename =  "\(filename).\(ext)"
10
    }
11
    return filename
12
}
缓存数据
1
func store(
2
    value: T,
3
    forKey key: String,
4
    expiration: StorageExpiration? = nil) throws
5
{
6
    //获取存活时间,若缓存时没设置,则从配置中获取
7
    let expiration = expiration ?? config.expiration
8
     //判断是否过期,若已经过期直接返回
9
    guard !expiration.isExpired else { return }
10
    // 把value转成data,这里value类型是DataTransformable,需要实现toData等其他方法
11
    let data: try value.toData()
12
    //通过cacheKeyc生成一个完整的路径
13
    //完整的路径等于directoryURL+filename
14
    let fileURL = cacheFileURL(forKey: key)
15
    let now = Date()
16
    //把当前时间设置为文件的创建时间,把过期时间设置为文件的修改时间
17
    let attributes: [FileAttributeKey : Any] = [
18
        .creationDate: now.fileAttributeDate,
19
        .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
20
    ]
21
    //通过fileManager把data写入文件
22
    config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
23
}

上面代码中给文件设置创建时间和修改时间用的是给 Date 扩展的计算属性 fileAttributeDate,fileAttributeDate 返回的是 Date(timeIntervalSince1970: ceil(timeIntervalSince1970)),也就是说把 date 的秒值向上取整后再转成 date,为什么要这么做呢?作者解释说,date 在内容中实际是一个 double 类型的值,而在 file 的属性中,只接受 Int 类型的值,会默认舍去小数部分,会导致对测试不友好,所以就改成这样了,我不是很理解为什么对测试不友好,难道是会导致提前一会结束过期吗?

加载缓存
1
func value(
2
    forKey key: String,/
3
    referenceDate: Date,
4
    actuallyLoad: Bool,
5
    extendingExpiration: ExpirationExtending) throws -> T?
6
{
7
    let fileManager = config.fileManager
8
    //通过cacheKeyc生成一个完整的路径
9
    let fileURL = cacheFileURL(forKey: key)
10
    let filePath = fileURL.path
11
    //判断是否存在该文件是否存在
12
    guard fileManager.fileExists(atPath: filePath) else {
13
        return nil
14
    }
15
    //通过fileURL生成一个FileMeta文件描述信息的类
16
    let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
17
    let meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
18
    //判断文件的过期时间是否大于referenceDate
19
    if meta.expired(referenceDate: referenceDate) {
20
        return nil
21
    }
22
    //判断是否是真的需要去加载数据,比如判断是否已缓存的时候,就不需要真的去加载,只要知道有就好了
23
    if !actuallyLoad { return T.empty }
24
    //读取文件
25
    let data = try Data(contentsOf: fileURL)
26
    let obj = try T.fromData(data)
27
    //更新文件的描述信息,本质也是为了h更新最后一次的读取时间和过期时间
28
    metaChangingQueue.async { meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration) }
29
}
判断是否已缓存

通过调用 value 方法,判断 value 的返回值是否为 nil,调用时会把 actuallyLoad 参数传为 false,这样就不会去读取文件

通过 key 删除缓存,以及删除所有缓存
1
//通过key生成URL,然后把该文件删除
2
func remove(forKey key: String) throws {
3
    let fileURL = cacheFileURL(forKey: key)
4
    config.fileManager.removeItem(at: url)
5
}
6
//直接把文件夹删除
7
func removeAll(skipCreatingDirectory: Bool) throws {
8
    try config.fileManager.removeItem(at: directoryURL)
9
    if !skipCreatingDirectory {
10
        try prepareDirectory()
11
    }
12
}
获取缓存大小

获取文件夹下的所有文件,并把每个文件的大小加起来

删除过期的缓存
1
//删除在指定时间过期的缓存,若传入当前时间,则是删除现在已经过期的文件
2
//返回值:删除的文件路径
3
func removeExpiredValues(referenceDate: Date = Date()) throws -> [URL] {
4
    let propertyKeys: [URLResourceKey] = [
5
        .isDirectoryKey,
6
        .contentModificationDateKey
7
    ]
8
    //获取所有的文件URL
9
    let urls = try allFileURLs(for: propertyKeys)
10
    let keys = Set(propertyKeys)
11
    //过滤出过期的文件URL
12
    let expiredFiles = urls.filter { fileURL in
13
        let meta = FileMeta(fileURL: fileURL, resourceKeys: keys)
14
        if meta.isDirectory {
15
            return false
16
        }
17
        return meta.expired(referenceDate: referenceDate)
18
    }
19
    //遍历所有的过期的文件UR,依次删除它们
20
    try expiredFiles.forEach { url in
21
        try removeFile(at: url)
22
    }
23
    return expiredFiles
24
}
缓存大小超过 sizeLimit 时删除缓存
1
func removeSizeExceededValues() throws -> [URL] {
2
      //如果sizeLimit == 0代表不限制大小,直接返回
3
      if config.sizeLimit == 0 { return [] }
4
      var size = try totalSize()
5
      //如果当前的缓存大小小于sizeLimit直接返回
6
      if size < config.sizeLimit { return [] }
7
      let urls = 获取所有的URLs
8
      //通过urls生成所有的文件信息,这里包含的信息有是否是文件夹,创建时间,和文件大小
9
      var pendings: [FileMeta] = urls.compactMap { fileURL in
10
          guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {
11
              return nil
12
          }
13
          return meta
14
      }
15
      //通过创建时间排序,也就是通过最后一次的读取时间
16
      pendings.sort(by: FileMeta.lastAccessDate)
17
      var removed: [URL] = []
18
      let target = config.sizeLimit / 2
19
      //直到当前缓存大小小于sizeLimit的2分之一,否则按照最后的读取时间一次删除
20
      while size > target, let meta = pendings.popLast() {
21
          size -= UInt(meta.fileSize)
22
          try removeFile(at: meta.url)
23
          removed.append(meta.url)
24
      }
25
      return removed
26
  }

补充

在 ImageCache 里监听了三个通知,分别是收到内存警告,应用即将被杀死,应用已经进入到后台,在这三个通知里分别做了,清空内存缓存,异步的清除磁盘过期缓存和磁盘大小超过 simeLimit 清除缓存,在后台清除磁盘过期缓存和磁盘大小超过 simeLimit 清除缓存

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

1. 当我们调用 imageView.kf.setImage()之后发生了什么?

当我们调用imageView.kf.setImage()之后发生了什么?

2. Kingfisher 中图片加载的流程是什么?

 Kingfisher中图片加载的流程是什么?

3. Kingfisher 是如何缓存图片的?

Kingfisher是如何缓存图片的?

4. Kingfisher 是如何查找缓存的?

 Kingfisher是如何查找缓存的?
注:图中有较多的查找失败,加工失败,并且也返回 true,并且返回 true,也不会再重新从网络上加载,但是 Kingfisher 里就是这么处理的,我认为是合理的,首先这种情况发生的情况是极低的,首先在获取之前先去排查了一下,文件是否存在,只有在存在的时候才会去加载,因此查找不到的可能性极低,而加工失败的话,很大可能性是 processor 或者 cacheSerializer 的问题,即使重新下载一遍,很很有可能有问题,除非我们写入文件的数据,在其他地方被动过,但这种可能性也不大

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

Kingfisher 加载 GIF 的两种使用方式

  1. 使用 UIImageView

    1
    let imageView = UIImageView()
    2
    imageView.kf.setImage(with: URL(string: "gif_url")!)
  2. 使用 AnimatedImageView,AnimatedImageView 继承自 UIImageView

    1
    let imageView = AnimatedImageView()
    2
    imageView.kf.setImage(with: URL(string: "gif_url")!)

Kingfisher 内部是如何处理的

看了上面 2 个显示 GIF 的方法,我们可能下面 2 个疑问,如果你对下面 2 个问题很清楚,本篇文章你可以跳过了

  • 加载 GIF 图和加载普通图片的使用方式是一样的,它是怎么做到如果是 GIF 图就显示 GIF 图,是普通图片就是现实普通图片的
  • 使用 UIImageView 和 AnimatedImageView 的调用方式也是一样的,这 2 中加载方式是否不同
    我们先来看第一个问题,Kingfisher 是如何区分 GIF 图和普通图片的,这个问题分 3 种情况
  1. 图片通过 Resource(通过网络下载的)或者 ImageDataProvider 提供的
  2. 图片是从缓存中内存缓存中加载的
  3. 图片是从磁盘缓存中加载的

首先来看第一种情况,在这之前,先来看下Kingfisher中配置项的这个配置public var processor: ImageProcessor = DefaultImageProcessor.default,这个配置是提供网络下载完成或者加载完成本地 Data 之后,会调用processorfunc process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把 Data 转换成 UIImage,而 processor 的默认值是DefaultImageProcessor,在DefaultImageProcessor该方法的实现会调用下面这个方法

1
public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
2
     var image: KFCrossPlatformImage?
3
     switch data.kf.imageFormat {
4
     case .JPEG:
5
         image = KFCrossPlatformImage(data: data, scale: options.scale)
6
     case .PNG:
7
         image = KFCrossPlatformImage(data: data, scale: options.scale)
8
     case .GIF:
9
         image = KingfisherWrapper.animatedImage(data: data, options: options)
10
     case .unknown:
11
         image = KFCrossPlatformImage(data: data, scale: options.scale)
12
     }
13
     return image
14
 }

在这个方法里会先判断图片的类型,判断的方式是取 data 的前 8 个字节,感兴趣的话,可以去源码里看下,这里就不贴了,如果是 GIF 图的话KingfisherWrapper.animatedImage这个方法

1
public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
2
    let info: [String: Any] = [
3
        kCGImageSourceShouldCache as String: true,
4
        kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
5
    ]
6
    guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
7
        return nil
8
    }
9
    //这里去掉了Macos下的处理
10
    var image: KFCrossPlatformImage?
11
    if options.preloadAll || options.onlyFirstFrame {
12
        guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
13
            return nil
14
        }
15
        if options.onlyFirstFrame {
16
            image = animatedImage.images.first
17
        } else {
18
            let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
19
            image = .animatedImage(with: animatedImage.images, duration: duration)
20
        }
21
        image?.kf.animatedImageData = data
22
    } else {
23
        image = KFCrossPlatformImage(data: data, scale: options.scale)
24
        var kf = image?.kf
25
        kf?.imageSource = imageSource
26
        kf?.animatedImageData = data
27
    }
28
    return image
29
}

这个方法时展示 GIF 的核心逻辑,下面详细介绍下这个方法
首先把 data 转成 CGImageSource,然后判断options.preloadAll || options.onlyFirstFrame 的值,其中 onlyFirstFrame 默认值为 false,若为 false 则只加载第一帧,preloadAll 这个值,在我们使用imageView.kf.setImage时,则取决于 imageView 的func shouldPreloadAllAnimation()函数的返回值,此函数是 Kingfisher 给 UIImageView 扩展的方法,在 UIImageVIew 中一直返回 true

1
@objc extension KFCrossPlatformImageView {
2
    func shouldPreloadAllAnimation() -> Bool { return true }
3
}

也就是说在默认情况下,在上面的方法里会把imageSource转换成GIFAnimatedImage类的实例,而在这个类的实例里,做了获取 GIF 图的每一帧,并获取每一帧的时间然后加起来,最后通过UIImage.animatedImage(with: [images], duration: duration)生成一个动图的 image 实例,然后把 image 赋值给imageView.image

下面把 imageSource 转成 animatedImage 的代码,忽略了较多的异常情况

1
let options: [String: Any] = [
2
    kCGImageSourceShouldCache as String: true,
3
    kCGImageSourceTypeIdentifierHint as String:kUTTypeGIF
4
]
5
//把data转换成imageSource
6
let imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)!
7
//获取GIF的总帧数
8
let frameCount = CGImageSourceGetCount(imageSource)
9
var images = [UIImage]()
10
var gifDuration = 0.0
11
for i in 0..<frameCount {
12
    //获取第i帧的图片,并把图片添加到数组里去
13
    let cgImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary)!
14
    images.append( UIImage(cgImage: cgImage, scale: 1, orientation: .up))
15
    //若只有一帧,把动画时间设置成无限大,否则的话获取每一帧的时间
16
    if frameCount == 1 {
17
        gifDuration = Double.infinity
18
    }else {
19
        //获取每一帧的属性,
20
        let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as! [String: Any]
21
        //获取属性中的GIF信息,以及获取信息中的时间
22
        let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as! [String: Any]
23
        let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
24
        let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
25
        let duration = unclampedDelayTime ?? delayTime
26
        gifDuration += duration?.doubleValue ?? 0.1
27
    }
28
}
29
imageView.image = UIImage.animatedImage(with: images, duration: gifDuration)

接着看第二种情况,若是从内存缓存中加载的,缓存的就是动图,所以是直接加载的

最后看第三种情况,若是从磁盘中缓存的,Kingfisher 又是如何处理的,在这之前,先来看下Kingfisher中配置项的这个配置public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default,这个配置是提供当从磁盘中读取完数据之后,把数据反序列化为 UIImage,会调用cacheSerializerpublic func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把 Data 反序列化为 UIImage,而 cacheSerializer 的默认值是DefaultCacheSerializer,在DefaultCacheSerializer该方法的实现也会调用public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage?这个方法,下面就是跟第一种情况的逻辑一样了

下面来看 AnimatedImageView 是如何加载 GIF 图的,上面说 imageView 的shouldPreloadAllAnimation一直返回 true,而 AnimatedImageView 重写了此函数,并返回 false,因此option.preloadAll等于 false,所以会走 else 里的逻辑,把 data 转成 image,利用关联属性,给 image 添加了两个属性imageSource:CGImageSourceanimatedImageData:Data,并对其进行赋值

到现在为止,我们还是没有看到 AnimatedImageView 是如何展示 GIF 图的。接着往下看
AnimatedImageView 重写了 image 的 didSet,而上面的方法返回后,会对 imageView.image 进行赋值,正好触发了 image 的 didSet,在这里开启了一个 CADisplayLink 和 Animator。

Animator 为 imageView 提供动图的数据,每一帧的图片以及时间,需要注意的是,它并不会一次加载好所有帧的图片,默认情况下,只是先加载前 10 帧,剩下的等需要的再去加载

CADisplayLink,在每次屏幕刷新的时候,去判断是否需要展示新的一帧图片,若需要,则刷新 imageView

这里刷新是调用self.layer.setNeedsDisplay(),而调用此方法,系统会调用layer.delegate里的open func display(_ layer: CALayer),而 UIView 的 layer.delegate 是自己本身,所以会调用 AnimatedImageView 重写的 display 方法,这是我最开始没有想明白的地方

1
override open func display(_ layer: CALayer) {
2
     if let currentFrame = animator?.currentFrameImage {
3
         layer.contents = currentFrame.cgImage
4
     } else {
5
         layer.contents = image?.cgImage
6
     }
7
 }

UIImageView 和 AnimatedImageView 在展示 GIF 图有什么不同

AnimatedImageView 支持一下 5 点特性,而 UIImageView 都不支持

  1. repeatCount:循环次数
  2. autoPlayAnimatedImage:是否自动开始播放
  3. framePreloadCount:预加载的帧数
  4. backgroundDecode:是否在后台解码
  5. runLoopMode:GIF 播放所在的 runLoopMode

并且 AnimatedImageView 由于不用同时解码所有帧的图形数据,所以更节省内存,但是由于多了一些计算所以会比较浪费 CPU

Kingfisher 源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

  1. targetCache,originalCache

    • 类型 ImageCache,Kingfisher 中的缓存管理器,提供内存和磁盘缓存
    • targetCache 最终展示出来的缓存管理器
    • originalCache 原始数据的缓存管理器
    • 默认值都为 nil,为 nil 时,使用 ImageCache(name: “default”)
  2. downloader

    • 类型 ImageDownloader,图片下载器,提供图片下载的功能
    • 默认值为 nil,为 nil 时,使用 ImageDownloader(name: “default”)
  3. downloadPriority 下载优先级

  4. transition

    • 类型 ImageTransition,默认是为 ImageTransition.none
    • 为 imageView 显示 image 的时候提供过渡动画
  5. forceTransition

    • 类型为 Bool,默认值 false
    • 是否强制使用过渡动画,当值为 true 时,transition 不为.none 时就使用过渡动画,当值为 false 时,只有 transition 不为.none 并且不使用缓存时,才使用缓存动画
  6. forceRefresh 是否强制刷新,若值为 true,则不使用缓存

  7. fromMemoryCacheOrRefresh 当值为 true 时,刷新的时候,若使用缓存,则只使用内存缓存,不去磁盘缓存

  8. cacheMemoryOnly 是否只使用内存缓存

  9. waitForCache 是否等待缓存完成,再调用回调

  10. onlyFromCache 是否只从缓存中加载

  11. backgroundDecode 是否在子线程去解码

  12. preloadAllAnimationData

    • 是否预加载 GIF 图每一帧画面,默认值为 false
    • 若使用 UIImageView.setImage(),去加载图片,则会被强制设置成 true,因为
    1
     @objc extension UIImageView {
    2
        func shouldPreloadAllAnimation() -> Bool { return true }
    3
     }
    4
    5
     public func setImage(
    6
        with source: Source?,
    7
        placeholder: Placeholder? = nil,
    8
        options: KingfisherOptionsInfo? = nil ...
    9
       -> DownloadTask? {
    10
             ...
    11
            //若shouldPreloadAllAnimation()的值为true
    12
            //则把preloadAllAnimationData设为true
    13
            //而ImageView中的shouldPreloadAllAnimation()一直为true
    14
             if base.shouldPreloadAllAnimation() {
    15
                options.preloadAllAnimationData = true
    16
             }
    17
            ...
    18
    }
  13. onlyLoadFirstFrame 若图片是 GIF 图时,是否只显示第一帧

  14. callbackQueue,processingQueue

    • callbackQueue,图片处理结束之后,回调所在的队列,默认值 mainCurrentOrAsync
    • processingQueue,处理图片时所在的队列,比如利用 processor 给图片添加圆角等操作时,所在的队列,默认使用一个串行的子队列
    • 一共有四个值可选择.mainAsync,.mainCurrentOrAsync,.untouch,.dispatch(DispatchQueue)
    • .mainAsync 主线程中异步执行
    • .mainCurrentOrAsync,若当前线程是主线程,则直接执行,否则在主线程异步执行
    • .untouch 不改变当前所在的线程
    • .dispatch(DispatchQueue) 在指定的队列中执行
  15. requestModifier 发送请求时对原始的请求进行修改,返回新的 Request

  16. redirectHandler 当请求发生重定向是,自定义的一些处理

  17. processor 加工者,自定义 data|image 转成 image 的逻辑,

  18. imageModifier image 修饰器,对 image 做一些修改,比如返回 image.withRenderingMode(renderingMode)

  19. cacheSerializer 定义图片序列化和反序列化

  20. keepCurrentImageWhileLoading 当加载一个新图时,是否要保持当前的图片

  21. cacheOriginalImage 是否要缓存原始的数据

  22. onFailureImage 加载失败时,要展示的图片

  23. alsoPrefetchToMemory 预加载时,需要从磁盘加载时,是否也需要同步到内存中

  24. loadDiskFileSynchronously 从磁盘中加载时,是否同步的去加载

  25. memoryCacheExpiration,diskCacheExpiration 设置内存、磁盘缓存过期时间

  26. memoryCacheAccessExtendingExpiration

    • 当从内存、磁盘中取图片时,往后延长过期时间的策略
    • 有以下几个值:.none,.cacheTime,.expirationTime(_ expiration: StorageExpiration)
    • .none 保持原来的过期时间
    • .cacheTime 设置过期时间为当前时间加上原来的过期时间
    • .expirationTime(_ expiration: StorageExpiration) 设置过期时间到指定时间
  27. alternativeSources 当加载失败时,可供替代的数据源

  28. onDataReceived 接收到数据时,需要回调时,可设置此属性,比如 setImage 时设置的 DownloadProgressBlock,就是在里面封装了此属性

在做小程序的时候,要实现下面的搜索历史界面
屏幕快照 2019-10-14 上午10.20.07.png

下面的搜索很明显的想到是用 flex 布局,然后把 justify-content 设置为 justify-content: flex-start;
代码如下:

1
<!--wxml-->
2
<view class="flex">
3
<button class="item">1</button>
4
<button class="item">2</button>
5
<button class="item">3</button>
6
<button class="item">4</button>
7
<button class="item">5</button>
8
<button class="item">6</button>
9
<button class="item">7</button>
10
</view>
11
12
13
<!--wxss-->
14
.flex{
15
  display: flex;
16
  flex-wrap: wrap;
17
  justify-content: flex-start;
18
}
19
.flex .item{
20
  width: 216rpx;
21
  background-color: red;
22
  margin-bottom: 34rpx;
23
}

屏幕快照 2019-10-14 上午10.35.32.png

可效果却不尽人意,发现 justify-content 不起作用,无论怎么设置都是 space-around 的效果。
经过排查,发现原因是小程序 button 中的默认样式中的margin-left: auto;margin-right: auto;所引起的。


flex 格式化上下文中,在通过 justify-content 和 align-self 进行对齐之前,任何正处于空闲的空间都会分配到该方向的自动 margin 中去。参考自探秘 flex 上下文中神奇的自动 margin

原因找到了,具体修改就容易多了,我们可以覆盖 button 的 margin-left 和 margin-right 的默认值,或者在 button 外面包裹一层 view。

在遇到这个问题之前,我也没想到过 flex 和 margin 之间还能这么用,涨姿势了

####题目
输入数字 n,按顺序打印出从 1 到 n 最大的 n 位十进制数。比如输入 3,则打印 1、2、3 一直到最大的 3 位数 999 ####方案
看到题目,首先想到先求出最大的 n 位数(maxN),然后从 1 开始遍历到 maxN.但是这里有个陷进,就是 maxN 有可能超出Int或者long long表示的最大数的范围,这个时候回发生溢出错误。

所以这个时候要选择合适的类型来表示 maxN,我这里选择的是用Array来表示 maxN,最高位放到数组的最前面。以四位数举例,585 可以表示为[0,5,8,5],

用数组表示 maxN 之后,面临着三个问题:

  1. 输出打印
    这个时候只需要遍历数组,把前面的 0 去掉即可

  2. 加 1
    对数组进行倒序遍历,对后位进行+1,若结果大于 9,则把该位置 0,继续对前面的数进行+1,若结果不大于 9,则把结果赋值给当前位并停止遍历

  3. 何时停止
    若数组的首位进行进位时,则表示已经遍历了最大值,需要停止循环 ####代码 Swift

1
func printOneToMaxOfDigits(digitNumber:Int) {
2
    //初始化一个全为0的长度为digitNumber的数组
3
    var array = Array(repeating: 0, count: digitNumber)
4
    //相当于生成一个从digitNumber-1到0的一个序列
5
    let strideTo = stride(from: digitNumber - 1, to: -1, by: -1)
6
7
   //对数组进行加1的函数,若返回false则代表已经超出最大值,可以停止了
8
    func addOne() -> Bool{
9
        for index in strideTo {
10
            if array[index] + 1 > 9 {
11
                array[index] = 0
12
                if index == 0 {
13
                    return false
14
                }
15
            }else {
16
                array[index] = array[index] + 1
17
                break
18
            }
19
        }
20
        return true
21
    }
22
23
    //对数组进行打印
24
    func printArray() {
25
        var result = ""
26
        var isAppend = false
27
        for item in array {
28
            if isAppend || item != 0 {
29
                result += "\(item)"
30
                isAppend = true
31
            }
32
        }
33
        print(result)
34
    }
35
36
37
    //调用加1方法,知道加1方法返回false
38
    while addOne() {
39
        printArray()
40
    }
41
}
42
printOneToMaxOfDigits(digitNumber: 5)