Post

kingfiher加载动画及本地文件

kingfiher加载动画及本地文件

前言

由于项目中使用了多个图片加载框架,有SDWebImageKingfisher
同时由于有性能相关需求,需要对gif的加载做优化,同事还推荐了Gifu这个框架

为了方便,将项目里全都统一为了Kingfisher,看了下Gifu的实现逻辑,跟Kingfisher是一样的

一、如果只是为了简单播放动画或一般网络图片

如果是一般的网络图片,直接使用UIImageView进行加载即可,Kingfisher会自动判断文件类型,会返回animatedImage(with: xxx, duration: xx)初始化的UIImage,这个会自动加载并播放

retrieveImage -> KingfisherManager.provideImage -> … -> KingfisherWrapper<UIImage>.image(data:options:)

-> .GIF -> KingfisherWrapper.animatedImage(data: data, options: options)CGImageSourceCreateWithDataoptions.preloadAll || options.onlyFirstFrame

.animatedImage(with: animatedImage.images, duration: duration)

1
2
3
4
5
6
7
8
9
10
11
12
import UIKit
import Kingfisher

let imageView = UIImageView()
let resource = Kingfisher.ImageResource(downloadURL: URL(string: "https://xx.xx/xx"), cacheKey: "https://xx.xx/xx")
// 之所以能自动播放gif图片,是因为Kingfisher内部确定图片类型为gif类型时,通过.animatedImage(with: animatedImage.images, duration: duration)进行初始化,这个就可以自动播放
imageView.kf.setImage(
            with: resource,
            placeholder: placeholder,
            options: nil,
            progressBlock: nil
        )
二、如果要精确控制播放次数及回调

如果要监听动态每次循环播放完成、或者指定次数播放完成,需要使用Kingfisher里的Kingfisher.AnimatedImageView类,这个类可以设置动画循环的次数,且有单次播放完成和全部完成的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
import Kingfisher

class TestGifView: UIView, AnimatedImageViewDelegate {

    // icon
    lazy var avatarView: AnimatedImageView  = {
        let view = AnimatedImageView(frame: .zero)
        view.repeatCount = .finite(count: 3)
        view.delegate = self
        return view
    }()
    // 如果设置了确定的循环次数,全部循环完会回调
    func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {
        print("finish")
    }
    // 每次循环一遍的最后一帧播放后就会回调
    func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {
        print("count: \(count)")
    }
    func showDemo() {
        let resource = Kingfisher.ImageResource(downloadURL: URL(string: "https://xx.xx/xx"), cacheKey: "https://xx.xx/xx")
        avatarView.kf.setImage(
            with: resource,
            placeholder: placeholder,
            options: nil,
            progressBlock: nil
        )
    }
}

三、加载沙盒里的文件进行播放

一些特殊场景下,比如下载一个资源包进行自己管理也解压,这个时候文件就在本地沙盒里了,针对本地沙盒的文件加载,也可以直接使用Kingfisher进行管理,目的是为了自动加载、内存缓存,但是不从默认的硬盘缓存读取,也不是直接从网络下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import UIKit
import Kingfisher

let imageView = UIImageView()
let localURL =  URL(string: "/Users/xxx.xx/xx")
let resource = Kingfisher.ImageResource(downloadURL: localURL, cacheKey: localURL.absoluteString)
let options: KingfisherOptionsInfo = [
    KingfisherOptionsInfoItem.cacheOriginalImage,        // 保证 memory cache 生效(不存磁盘没关系)
    .cacheMemoryOnly,           // 只用内存缓存      // 不限制一定来自缓存(你希望手动控制加载)
    .diskCacheExpiration(.expired), // 明确禁用磁盘缓存
]
imageView.kf.setImage(
    with: resource,
    placeholder: placeholder,
    options: options,
    progressBlock: nil
)

4、针对下载图片错误处理,比如下载后发现域名被封,替换域名重新下载等场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import UIKit
import Kingfisher

let imageView = UIImageView()
let resource = Kingfisher.ImageResource(downloadURL: URL(string: "https://xx.xx/xx"), cacheKey: "https://xx.xx/xx")
imageView.kf.setImage(
            with: resource,
            placeholder: placeholder,
            options: nil,
            progressBlock: nil
        ) { [weak self] result in
            switch result {
            case .success(let value):
                break
            case .failure(let error):
                // 或者具体分类处理
                switch error {
                case .requestError(let reason):
                    break
                case .responseError(let reason):
                    switch reason {
                        case .invalidURLResponse(let response):
                        break
                        case .invalidHTTPStatusCode(let response):
                        break
                        case .URLSessionError(let responseError):
                        break
                        case .dataModifyingFailed(let task):
                        break
                        case .noURLResponse(let task):
                        break
                        }
                    break
                case .cacheError(let reason):
                    break
                case .processorError(let reason):
                    break
                case .imageSettingError(let reason):
                    break
                }
            }
        }

5、Kingfisher里的读取图片类型的代码,是读取前8个字节进行文件类型判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import Foundation

/// Represents image format.
///
/// - unknown: The format cannot be recognized or not supported yet.
/// - PNG: PNG image format.
/// - JPEG: JPEG image format.
/// - GIF: GIF image format.
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]
    }
    
    /// https://en.wikipedia.org/wiki/JPEG
    public enum JPEGMarker {
        case SOF0           //baseline
        case SOF2           //progressive
        case DHT            //Huffman Table
        case DQT            //Quantization Table
        case DRI            //Restart Interval
        case SOS            //Start Of Scan
        case RSTn(UInt8)    //Restart
        case APPn           //Application-specific
        case COM            //Comment
        case EOI            //End Of Image
        
        var bytes: [UInt8] {
            switch self {
            case .SOF0:         return [0xFF, 0xC0]
            case .SOF2:         return [0xFF, 0xC2]
            case .DHT:          return [0xFF, 0xC4]
            case .DQT:          return [0xFF, 0xDB]
            case .DRI:          return [0xFF, 0xDD]
            case .SOS:          return [0xFF, 0xDA]
            case .RSTn(let n):  return [0xFF, 0xD0 + n]
            case .APPn:         return [0xFF, 0xE0]
            case .COM:          return [0xFF, 0xFE]
            case .EOI:          return [0xFF, 0xD9]
            }
        }
    }
}


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
    }
    
    public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool {
        guard imageFormat == .JPEG else {
            return false
        }
        
        var buffer = [UInt8](repeating: 0, count: base.count)
        base.copyBytes(to: &buffer, count: base.count)
        for (index, item) in buffer.enumerated() {
            guard
                item == marker.bytes.first,
                buffer.count > index + 1,
                buffer[index + 1] == marker.bytes[1] else {
                continue
            }
            return true
        }
        return false
    }
}


6、Kingfisher支持webp

Podfile

1
2
3
4
5
6
7
8
9
10
11
pod 'Kingfisher'
pod 'KingfisherWebP'

# 如果M系列芯片要按x86编译
post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
    end
  end
end

在启动的地方配置

1
2
3
4
KingfisherManager.shared.defaultOptions += [
  .processor(WebPProcessor.default),
  .cacheSerializer(WebPSerializer.default)
]

在使用的地方正常使用即可


7、Kingfisher主动缓存到硬盘

有些场景下,比如上传了图片后,希望直接缓存到硬盘,省的多一次下载

1
2
3
4
5
let filePath = "/Users/xxx/xx/aa.png"
let uploadUrl = "https://xx/xx/aa.png"
let fileURL = URL(fileURLWithPath: filePath)
let data = try Data(contentsOf: fileURL)
ImageCache.default.storeToDisk(data, forKey: uploadUrl)
8、Kingfisher预加载

某些场景下,需要对一些网络图片进行预加载,方便使用时快速展示没有加载过程

1
2
3
4
5
6
7
8
9
10
let prefetcher = ImagePrefetcher(
    urls: urls,
    options: [
        .doNotCacheMemory                         // 不保留在内存缓存(可选)
    ]
) { skipped, failed, completed in
    print("跳过 \(skipped.count),成功 \(completed.count),失败 \(failed.count)")
}

prefetcher.start()
9、下载图片后自定义解密

在某些场景下,图片由于加密了,下载后的图片需要进行解密后才能显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Kingfisher

struct SmartDecryptionModifier: ImageDataModifier {
    func modify(_ data: Data) -> Data? {
        if isEncrypted(data) {
            return decrypt(data)
        } else {
            return data // 原样返回
        }
    }
    
    private func isEncrypted(_ data: Data) -> Bool {
        // 示例:检查前缀、自定义 magic number、Header bytes 等
        return data.starts(with: [0xAB, 0xCD]) // 举例:加密数据头部是 0xABCD
    }
    
    private func decrypt(_ data: Data) -> Data? {
        // 替换为你自己的解密实现,比如 AES、ChaCha 等
        return MyCrypto.decrypt(data: data)
    }
}
KingfisherManager.shared.defaultOptions += [
    .dataModifier(SmartDecryptionModifier()),
    .processor(WebPProcessor.default),
    .cacheSerializer(WebPSerializer.default)
]

由于Kingfisher在进入后台后会对一些过期资源进行内存清理,导致从后台进入前台后会出现闪烁的情况,解决方案是可以将进入后台的监听手动关闭

1
2
3
        // 防止Kingfisher进入后台后清理资源导致闪烁
        NotificationCenter.default.removeObserver(KingfisherManager.shared.cache, name: UIApplication.didEnterBackgroundNotification, object: nil)

This post is licensed under CC BY 4.0 by the author.