前序
Kingfisher是由onevcat编写的用于下载和缓存网络图片的轻量级Swift工具库,目前在github收获的star已经达到了18k了。其内容包括了GCD、Swift的高级语法、缓存、硬盘读写、网络编程、图形绘制等大量iOS开发知识。
本篇博客不再简述Kingfisher源码的解析了,而是上了一个层次,当你看完或者了解过Kingfisher的时候,会发现别人写的代码真好,或者怎一个好字了得。本人也是看了Kingfisher的很多遍源码,每次看一遍都会得到不同的感受和体验。抽出这个时间,本人将Kingfisher一些好的代码思想和风格运用到自己目前的项目中或者新建的Demo中,供大家查看和思考。
踏踏实实提高技术才是硬道理【具备】。
目录
阅读Kingfisher源码的一些收获
下面我们一一来剖析Kingfisher以及本人是如何在项目中使用的。
kf前缀命名空间所得
当大家使用Kingfisher的时候,肯定会对一个现象感到好奇,为什么要用.kf.的方式,那么我们今天就来探究一下以及如果在自己项目中使用这种?
imageView.kf.setImage(with: url, placeholder: image)
对于这种带有前缀的写法:可以很好避免与系统方法冲突,也可以宣传属于自己的style,废话少说,今天都是干货,主要讲述代码思想和项目如何使用。
1.1 Kingfisher实现
打开Kingfisher源码的Kingfisher.swift文件
import UIKit
public typealias KFCrossPlatformImage = UIImage
public typealias KFCrossPlatformColor = UIColor
#if !os(watchOS)
public typealias KFCrossPlatformImageView = UIImageView
public typealias KFCrossPlatformView = UIView
public typealias KFCrossPlatformButton = UIButton
#endif
public struct KingfisherWrapper<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
public protocol KingfisherCompatible: AnyObject { }
public protocol KingfisherCompatibleValue {}
extension KingfisherCompatible {
public var kf: KingfisherWrapper<Self> {
get { return KingfisherWrapper(self) }
set { }
}
}
extension KingfisherCompatibleValue {
public var kf: KingfisherWrapper<Self> {
get { return KingfisherWrapper(self) }
set { }
}
}
extension KFCrossPlatformImage: KingfisherCompatible { }
#if !os(watchOS)
extension KFCrossPlatformImageView: KingfisherCompatible { }
extension KFCrossPlatformButton: KingfisherCompatible { }
extension NSTextAttachment: KingfisherCompatible { }
#endif
上面代码的逻辑:
- 首先定义了一个结构体,有一个泛型属性base;
- 定义一个协议;
- 定义了一个只读的kf关联属性,指定关联类型为,这里的Self理解为协议约束,需要遵守协议的类型;
- ImageView遵守协议,所以imageView可以用.kf.
1.2 项目中使用
1.2.1 常规做法
需求:利用协议实现前缀【统计字符串有几个数字出现,例如1234dafdaf1234,应该返回数字8?】
如果说仅仅是这个需求,用一个方法就可以实现的,如下【大多数人第一反应就是下面方面】
extension String {
///计算属性===方法,下面两种完全等价
//方法
func numberCount() -> Int {
var count = 0
for c in self where("0"..."9").contains(c) {
count += 1
}
return count
}
//计算属性
var numberCount1: Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print("1234dafdaf1234".numberCount())
1.2.2 进阶版本1
但是如果想进一步凸显封装和代码的可读性,可以这样做:
struct ZXY {
var string: String
init(_ str: String) {
self.string = str
}
var numberCount: Int {
var count = 0
for c in string where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
extension String {
var zxy: ZXY {return ZXY(self)}//传值self字符串
}
print("1234dafdaf1234".zxy.numberCount)
上面已经完成对字符串的拓展功能的系列,也已经很好的很优雅的解决了问题,但是如果相对字符串拓展一个功能的话,这就OK啦! 但是如果想对数组进行拓展一个类似的方法,还要在ZXY里面增加array属性和初始化以及拓展Array功能,就会发现冗余代码太多,且不够封装,不够通用。
1.2.3 进阶版本2
这时候泛型的作用就来啦,如下:
struct ZXY<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
extension String {
var zxy: ZXY<String> {ZXY(self)}
}
class Person{}
extension Person {
var zxy: ZXY<Person> {ZXY(self)}
}
extension ZXY where Base == String {
var numberCount: Int {
var count = 0
for c in base where("0"..."9").contains(c){
count += 1
}
return count
}
}
extension ZXY where Base == Person {
func run() {
print("run")
}
}
"1234dafdaf1234".zxy.numberCount
Person().zxy.run()
1.2.4 最终版本
上面实现了通过类的对象调用,可不可以实现通过类本身来调用呢,因为我在使用类.zxy的时候,并不想出现类对象的属性,只想出现类型本身的方法和属性,这就需要用到来修饰。
这些代码,增加其他,会导致代码还是会有点冗余,这样就发现了,将公共的地方抽出来(协议只能声明一些东西,想扩充一些东西,就是在extension加入)
///前缀类型
struct ZXY<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
///利用协议扩展前缀属性
protocol ZXYCompatible {}
extension ZXYCompatible {
var zxy: ZXY<Self> {ZXY(self)}
static var zxy: ZXY<Self>.Type {ZXY<Self>.self}
}
///给字符串扩展功能
//让String拥有前缀属性
extension String: ZXYCompatible {}
//给string.zxy以及String().zxy前缀扩展功能
extension ZXY where Base == String {
var numberCount: Int {
var count = 0
for c in base where("0"..."9").contains(c){
count += 1
}
return count
}
static func test() {
print("test")
}
}
class Person{}
extension Person: ZXYCompatible{}
class Dog{}
extension Dog: ZXYCompatible{}
extension ZXY where Base == Person {
func run() {
print("run")
}
}
枚举的使用
Kingfisher使用了大量的枚举,以前认为枚举就是为了区分状态,以提高代码的可读性,现在的理解是枚举定义了含义相同,但行为策略可能不同的一组值。
2.1 Kingfisher使用案例
本模块讲述Kingfisher的【定义错误枚举】和【定义更新时间】。
2.1.1 KingfisherError案例
首先看下源码如何实现
public enum KingfisherError: Error {
// MARK: Member Cases
case requestError(reason: RequestErrorReason)
case responseError(reason: ResponseErrorReason)
// MARK: Helper Properties & Methods
public var isTaskCancelled: Bool {
if case .requestError(reason: .taskCancelled) = self {
return true
}
return false
}
///请求失败原因
public enum RequestErrorReason {
case emptyRequest
case invalidURL(request: URLRequest)
case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken)
}
///响应失败原因
public enum ResponseErrorReason {
case invalidURLResponse(response: URLResponse)
case invalidHTTPStatusCode(response: HTTPURLResponse)
case URLSessionError(error: Error)
case dataModifyingFailed(task: SessionDataTask)
case noURLResponse(task: SessionDataTask)
}
}
// MARK: - LocalizedError Conforming
extension KingfisherError: LocalizedError {
/// A localized message describing what error occurred.
public var errorDescription: String? {
switch self {
case .requestError(let reason):
return reason.errorDescription
case .responseError(let reason):
return reason.errorDescription
}
}
}
extension KingfisherError.RequestErrorReason {
var errorDescription: String? {
switch self {
case .emptyRequest:
return "The request is empty or `nil`."
case .invalidURL(let request):
return "The request contains an invalid or empty URL. Request: \(request)."
case .taskCancelled(let task, let token):
return "The session task was cancelled. Task: \(task), cancel token: \(token)."
}
}
var errorCode: Int {
switch self {
case .emptyRequest: return 1001
case .invalidURL: return 1002
case .taskCancelled: return 1003
}
}
}
extension KingfisherError.ResponseErrorReason {
var errorDescription: String? {
switch self {
case .invalidURLResponse(let response):
return "The URL response is invalid: \(response)"
case .invalidHTTPStatusCode(let response):
return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)."
case .URLSessionError(let error):
return "A URL session error happened. The underlying error: \(error)"
case .dataModifyingFailed(let task):
return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)."
case .noURLResponse(let task):
return "No URL response received. Task: \(task),"
}
}
var errorCode: Int {
switch self {
case .invalidURLResponse: return 2001
case .invalidHTTPStatusCode: return 2002
case .URLSessionError: return 2003
case .dataModifyingFailed: return 2004
case .noURLResponse: return 2005
}
}
}
通过上面的KingfisherError定义了Kingfisher的所有错误类型【上面仅仅是请求错误和响应错误】在代码中如何使用呢?
guard let httpResponse = response as? HTTPURLResponse else {
let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))
onCompleted(task: dataTask, result: .failure(error))
completionHandler(.cancel)
return
}
2.1.2 StorageExpiration案例
Storage.swift定义了缓存日期的规范。通过TimeConstants定义常量,然后通过StorageExpiration定义缓存过期枚举。
/// Constants for some time intervals
struct TimeConstants {
static let secondsInOneMinute = 60
static let minutesInOneHour = 60
static let hoursInOneDay = 24
static let secondsInOneDay = 86_400
}
public enum StorageExpiration {
/// The item never expires.
case never
/// The item expires after a time duration of given seconds from now.
case seconds(TimeInterval)
/// The item expires after a time duration of given days from now.
case days(Int)
/// The item expires after a given date.
case date(Date)
/// Indicates the item is already expired. Use this to skip cache.
case expired
func estimatedExpirationSince(_ date: Date) -> Date {
switch self {
case .never: return .distantFuture
case .seconds(let seconds):
return date.addingTimeInterval(seconds)
case .days(let days):
let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days)
return date.addingTimeInterval(duration)
case .date(let ref):
return ref
case .expired:
return .distantPast
}
}
var estimatedExpirationSinceNow: Date {
return estimatedExpirationSince(Date())
}
var isExpired: Bool {
return timeInterval <= 0
}
var timeInterval: TimeInterval {
switch self {
case .never: return .infinity
case .seconds(let seconds): return seconds
case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days)
case .date(let ref): return ref.timeIntervalSinceNow
case .expired: return -(.infinity)
}
}
}
2.2 项目使用案例
项目中使用到的HttpRequestError,定义了无网络、网络异常;网络超时;服务器异常;请求已取消等枚举,并定义属性错误提示文案。
public enum HttpRequestError {
/// 无网络、网络异常
case netless(String)
/// 网络超时
case timeout(String)
/// 服务器异常
case serviceException(String)
/// 请求已取消
case cancelled(String)
/// 错误提示文案
public var errorMessage: String {
switch self {
case .netless(let msg):
return msg
case .timeout(let msg):
return msg
case .serviceException(let msg):
return msg
case .cancelled(let msg):
return msg
}
}
}
在项目中使用网络封装:
func put(request: ZXYRequestEntity, success: @escaping ((ZXYResponseEntity) -> Void), failure: @escaping ((HttpRequestError) -> Void)) -> ZXYRequestCancleable? {
return self.request(method: .put, request: request, success: success, failure: failure)
}
封装使用:
ZXYHttpManager.shared.put(request: request) { [weak self] (response) in
if isAESApi {
response.bodyMessage = response.bodyMessage?.decrypt()
}
self?.handleResponse(response, success: success, failure: failure)
} failure: {(_ message) in
failure(message.errorMessage)
}
协议增加拓展性
协议是定义了某种能力,由协议遵循者去实现这些能力,但是由于Swift中协议扩展的存在,就可以让协议自己就提供某些能力,只要让协议遵循者去遵循协议,就能自动获取这些能力,减少了遵循协议的复杂性。并且协议仅仅定义了某种能力,不涉及具体类型,更方面的去扩展。
我们先从一个小的技能点来说:在Kingfisher中大量使用了协议的功能,
面试题:swift中将协议部分方法设为可选,该怎么实现?
import UIkit
protocol OptionalProtocol {
func optionalMethod()
func mustMethods()
func anotherOptionalMethod()
}
extension OptionalProtocol {
func optionalMethod() {
print("一个可选方法")
}
func anotherOptionalMethod() {
print("另一个可选方法")
}
}
class MyClass: OptionalProtocol {
func mustMethods() {
print("必须要实现的方法")
}
}
3.1 Resource案例
Resource.swift定义了协议Resource,里面有两个计算属性cacheKey和downloadURL,并且在Resource的拓展中定义方法【遵守协议的就不需要实现该方法】,然后ImageResource遵守协议,然后有自身有初始化方法,当然还有URL遵守协议
public protocol Resource {
/// The key used in cache.
var cacheKey: String { get }
/// The target image URL.
var downloadURL: URL { get }
}
extension Resource {
public func convertToSource(overrideCacheKey: String? = nil) -> Source {
return downloadURL.isFileURL ?
.provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: overrideCacheKey ?? cacheKey)) :
.network(ImageResource(downloadURL: downloadURL, cacheKey: overrideCacheKey ?? cacheKey))
}
}
public struct ImageResource: Resource {
public init(downloadURL: URL, cacheKey: String? = nil) {
self.downloadURL = downloadURL
self.cacheKey = cacheKey ?? downloadURL.absoluteString
}
// MARK: Protocol Conforming
/// The key used in cache.
public let cacheKey: String
/// The target image URL.
public let downloadURL: URL
}
extension URL: Resource {
public var cacheKey: String { return absoluteString }
public var downloadURL: URL { return self }
}
3.2 项目框架使用
项目中最原始使用CTMediator的target-action方式来实现,后期团队讨论如何改善CTMediator的硬编码,所以讨论使用协议来拓展,解决了硬编码问题
如下:
1.定义公共协议,为每个模块创建基本的协议
public protocol Routable {
// 公共协议
}
2.创建模块协议遵守基本协议,定义模块方法
public protocol Broker_Routable: Routable {
/// 详情页
/// - Parameters:
/// - brokerId: id
/// - collectionHandle: 收藏操作(取消或者添加)
func brokerDetailVC(brokerId: Int, collectionHandle: @escaping ((_ isCandel: Bool) -> Void)) -> UIViewController
}
3. 因为要Router.broker.方法,使用share为了遵守各个模块的协议的单例
public class Router {
static let shared: Router = Router()
private init() {}
/// 交易商模块
public static var broker: Broker_Routable { shared as! Broker_Routable }
}
4. 实现协议的方法
extension Router: Broker_Routable {
public func brokerDetailVC(brokerId: Int, collectionHandle: @escaping ((Bool) -> Void)) -> UIViewController {
let (vc, input) = BrokerDetailModuleBuilder.setupModule()
input.configBrokerDetailScene(brokerId: brokerId)
input.configCancelCollectionCallback(collectionHandle)
return vc
}
}
调用如下
let vc = Router.broker.brokerDetailVC(brokerId: id) { (_) in}
self.navigationController?.pushViewController(vc, animated: true)
上面仅仅是一个交易商模块,如果对于整个项目而言,broker通过Router.broker,Grade通过 Router.grade方式,complainCenter通过Router.complainCenter方式……使用share的单例遵守各个子模块的协议方法,就可以达到share为grade、complainCenter等
public class Router {
static let shared: Router = Router()
private init() {}
public static var home: Home_Routable { shared as! Home_Routable}
public static var broker: Broker_Routable { shared as! Broker_Routable }
public static var grade: Grade_Routable { shared as! Grade_Routable }
public static var complainCenter: ComplainCenter_Routable { shared as! ComplainCenter_Routable }
public static var mine: Mine_Routable { shared as! Mine_Routable }
public static var account: Account_Routable { shared as! Account_Routable }
}
使用协议解决了硬编码的CTMediator的方式,也很好的使用了swift的编程思想。
defer对代码简洁的好处
defer block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。
4.1 SessionDelegate的defer
private func cancelTask(_ dataTask: URLSessionDataTask) {
lock.lock()
defer { lock.unlock() }
dataTask.cancel()
}
swift的defer简单的使用场景
跟swift文档举的例子类似,defer一个很适合的使用场景就是用来做清理工作。文件操作就是一个很好的例子: 关闭文件
func foo() {
let fileDescriptor = open(url.path, O_EVTONLY)
defer {
close(fileDescriptor)
}
// use fileDescriptor...
}
这样就不怕哪个分支忘了写,或者中间 throw 个 error,导致 fileDescriptor 没法正常关闭。
项目中画圆圈【消息红点】
class MessageCenterCircleDot: UIView {
fileprivate var fillColor: UIColor = UIColor.rgbColor(254, 98, 98)
convenience init(frame: CGRect, fillColor: UIColor) {
self.init()
self.fillColor = fillColor
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let size = rect.size.width
let context = UIGraphicsGetCurrentContext()!
context.setFillColor((self.backgroundColor?.cgColor ?? UIColor.white.cgColor)!)
context.fill(rect)
context.saveGState()
defer { context.restoreGState() }
let path = UIBezierPath.init(roundedRect: rect, cornerRadius: size * 0.5)
context.addPath(path.cgPath)
context.closePath()
context.setFillColor(self.fillColor.cgColor)
context.fillPath()
}
}
关联Associate封装
想到要如何为所有的对象增加实例变量吗?使用Category可以很方便地为现有的类增加方法,但却无法直接增加实例变量。后来,系统提供了Associative References,这个问题就很容易解决了。这种方法也就是所谓的关联【association】,可以在runtime期间动态地添加任意多的属性,并且随时读取。
5.1 Kingfisher关于关联的封装
使用泛型代表为不同对象增加属性
func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {
return objc_getAssociatedObject(object, key) as? T
}
func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
Kingfisher使用封装的关联对KFCrossPlatformImage=UIImage的拓展动画数据
private var animatedImageDataKey: Void?
private(set) var animatedImageData: Data? {
get { return getAssociatedObject(base, &animatedImageDataKey) }
set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) }
}
5.2 项目使用封装
private var recordedUserInfo: UserInfoEntity? {
get { return getAssociatedObject(self, &recordedUserInfoKey) }
set { setRetainedAssociatedObject(self, &recordedUserInfoKey, newValue) }
}
通知的另类写法
通知中心【NSNotificationCenter】实际是在程序内部提供了一种广播机制。把接收到的消息,根据内部的消息转发表,将消息转发给需要的对象。这句话其实已经很明显的告诉我们要如何使用通知了。第一步:在需要的地方注册要观察的通知,第二步:在某地方发送通知。第三步:在合适时机移除通知
6.1 Kingfisher通知
let notifications: [(Notification.Name, Selector)]
#if !os(macOS) && !os(watchOS)
#if swift(>=4.2)
notifications = [
(UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)),
(UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)),
(UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache))
]
#else
notifications = [
(NSNotification.Name.UIApplicationDidReceiveMemoryWarning, #selector(clearMemoryCache)),
(NSNotification.Name.UIApplicationWillTerminate, #selector(cleanExpiredDiskCache)),
(NSNotification.Name.UIApplicationDidEnterBackground, #selector(backgroundCleanExpiredDiskCache))
]
#endif
#elseif os(macOS)
notifications = [
(NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)),
]
#else
notifications = []
#endif
notifications.forEach {
NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)
}
Kingfisher通过数组和元祖的配合,数组notifications包含元祖的集合,元祖中的元素是通知名称和方法【上面的太多是加入了平台的原因】 上面是针对多个通知的使用
6.2 本项目使用
func addObserverForNoti() {
let notifications: [(Notification.Name, Selector)]
notifications = [
(NewsTableView.newsScrollTopNotification, #selector(onRecvNewsScrollTopNotification(_:))),
(Notification.Name.init(Notification.nextUpdateAppNotification), #selector(onRecvClickNextUpdateNotification(_:))),
(Notification.Name.init(Notification.userAgreenmentAppNotification), #selector(onRecvClickUserAgreenmentNotification(_:)))
]
notifications.forEach {
NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)
}
}
判断图片格式的原理
一般图片格式的都在data的前几个字节里,只要按对应的规则去取,然后进行判断就行了
Kingfisher判断图片格式
public enum ImageFormat {
/// The format cannot be recognized or not supported yet.
case unknown
/// PNG image format.
case PNG
/// JPEG image format.
case JPEG
/// GIF image format.
case GIF
struct HeaderData {
static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
static var JPEG_SOI: [UInt8] = [0xFF, 0xD8]
static var JPEG_IF: [UInt8] = [0xFF]
static var GIF: [UInt8] = [0x47, 0x49, 0x46]
}
}
extension Data: KingfisherCompatibleValue {}
// MARK: - Misc Helpers
extension KingfisherWrapper where Base == Data {
/// Gets the image format corresponding to the data.
public var imageFormat: ImageFormat {
guard base.count > 8 else { return .unknown }
var buffer = [UInt8](repeating: 0, count: 8)
base.copyBytes(to: &buffer, count: 8)
if buffer == ImageFormat.HeaderData.PNG {
return .PNG
} else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0],
buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1],
buffer[2] == ImageFormat.HeaderData.JPEG_IF[0]{
return .JPEG
} else if buffer[0] == ImageFormat.HeaderData.GIF[0],
buffer[1] == ImageFormat.HeaderData.GIF[1],
buffer[2] == ImageFormat.HeaderData.GIF[2] {
return .GIF
}
return .unknown
}
}
总结
本篇文章主要讲述Kingfisher的部分优秀思想,以及在合适的地方,用之,并慢慢优化本人项目中的代码。在此,希望大家也能尝试用之,慢慢提高自己写代码的可移植性和可拓展封装等性。【毕竟工作了一些年份,已经过了业务书写的能力,而更具备更高层次的代码能力】
有一句心灵鸡汤送给此时想提高自己的你:“总有人要赢,那么反问一下自己,为什么不可能是我呢?”赢有点大了,换句话可能更好点:很多人都能进心仪的公司,那反问一下自己,为什么不可以是我呢?
踏踏实实提高技术才是硬道理【 】!!!
感谢大家❤️
- 如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容,本人会不断更新优质博客内容。
- 欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。
- 觉得不错的话,也可以关注本人其他的有关iOS底层、Flutter及小程序方面的文章(感谢掘友的鼓励与支持🌹🌹🌹)
机会❤️❤️❤️🌹🌹🌹
如果想和我一起共建抖音,成为一名bytedancer,Come on。期待你的加入!!!