Kingfisher 源码阅读笔记(2)

935 阅读2分钟

这是我参与更文挑战的第12天,活动详情查看: 更文挑战

本文为在阅读 Kingfisher 源码时的收货。

Kingfisher 源码阅读笔记(1) 的续篇。

多线程处理缓存数据

Kingfisher 在判断本地是否已经存在图片的缓存文件时,使用了 maybeCached 保存了当前已经存在的文件路径。在修改 maybeCached 时使用了专门的队列 maybeCachedCheckingQueue

var maybeCached : Set<String>?
let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue")

// 添加缓存内容时使用异步操作
maybeCachedCheckingQueue.async {
    self.maybeCached?.insert(fileURL.lastPathComponent)
}

// 获取缓存内容时使用同步操作
let fileMaybeCached = maybeCachedCheckingQueue.sync {
    return maybeCached?.contains(fileURL.lastPathComponent) ?? true
}

在处理不牵扯到 UI 更改的缓存数据时,可以考虑使用专门的自定义串行队列来获取、修改缓存数据,提供性能。

另外,每次读取缓存文件信息之后,都要更新文件的最后访问时间,这个操作也放到了单独的队列中。

let metaChangingQueue: DispatchQueue
metaChangingQueue = DispatchQueue(label: creation.cacheName)
metaChangingQueue.async {
    meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration)
}

异常的处理

Kingfisher 将所有的可能抛出的异常全部定义在了 KingfisherError 中,每个成员(case)表示一类错误,具体的错误信息放在 case 关联的 reason 中。

public enum KingfisherError: Error {

    public enum RequestErrorReason {
        // 具体错误
    }
    
    public enum ResponseErrorReason {
        // 具体错误
    }
    // 其他具体错误
    
    // MARK: Member Cases
    
    case requestError(reason: RequestErrorReason)
    case responseError(reason: ResponseErrorReason)
    // 其他错误类型...
    
    // 其他方法
}

在发生异常时,都会转化成 KingfisherError 对应的异常,具体的底层异常也会携带到错误信息当中。比如,在将图片数据存储到文件时,发生的异常时,会发出 cacheError 类型的异常,详细的信息为 cannotCreateCacheFile,更底层的错误放在了关联的 error 中。

do {
    try data.write(to: fileURL)
} catch {
    throw KingfisherError.cacheError(
        reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
    )
}

在制作 framework 时,可以参考这种方式:

  1. 将所有可能抛出的异常进行归类,集中在了一处,使用者对可能抛出的异常一目了然;
  2. 将底层的异常封装成更抽象的异常,同时保留底层的异常,而不是直接向上抛异常。

另外,Kingfisher 也考虑到了 Objectiv-C 中的 NSError。Swift 中的 Error 想要完整的转化为 Objectiv-C 中的 NSError 就需要实现 CustomNSError 协议。相关信息可以查看「没故事的卓同学」的文章Swift 3必看:Error与NSError的关系

打破一切外部可能造成的循环引用

当需要使用外部传入闭包时,要想尽一切办法消除存在循环引用。比如下面代码:Backend 要强引用 config,当使用完 configcachePathBlock 闭包后,要及时的清除。

 public class Backend<T: DataTransformable> {

    public var config: Config

    init(noThrowConfig config: Config, creatingDirectory: Bool) {
        var config = config

        // `config` 的 `cachePathBlock` 闭包生成 directoryURL
        self.directoryURL = creation.cachePathBlock("", "")

        // Break any possible retain cycle set by outside.
        config.cachePathBlock = nil
        self.config = config
    }
}

configcachePathBlock 属性专门设计为隐式解析可选类型

public struct Config {
    var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = {
            (directory, cacheName) in
        return directory.appendingPathComponent(cacheName, isDirectory: true)
    }
}