
一、問(wèn)題出現(xiàn)
在公司項(xiàng)目中,需要顯示一些網(wǎng)絡(luò) GIF 圖片,使用的是 Kingfisher 第三方圖片緩存庫(kù)進(jìn)行加載圖片,一般情況下挺好的,但有時(shí)候會(huì)出現(xiàn)內(nèi)存暴增,一開(kāi)始以為是沒(méi)有對(duì)圖片緩存進(jìn)行釋放導(dǎo)致,后來(lái)測(cè)試發(fā)現(xiàn)是因?yàn)槟硞€(gè) GIF 幀數(shù)過(guò)高導(dǎo)致的,一個(gè) 1MB 大小但幀數(shù)有 150 幀的 GIF 圖片,采用 Kingfisher 加載到內(nèi)存中需要占用至少 300 MB 以上的內(nèi)存,多加載幾張這樣的 GIF 內(nèi)存直接爆炸,所以需要進(jìn)行 GIF 圖片加載進(jìn)行優(yōu)化。
二、問(wèn)題思考
為什么會(huì)導(dǎo)致這樣的內(nèi)存暴增呢?
因?yàn)?Kingfisher 在加載 GIF 圖的時(shí)候,會(huì)把 GIF 圖的所有幀圖片數(shù)據(jù)都加載到內(nèi)存進(jìn)行顯示,導(dǎo)致內(nèi)存暴增。
降低內(nèi)存消耗,提高 CPU 消耗
去網(wǎng)上找第三方 GIF 圖加載優(yōu)化庫(kù),發(fā)現(xiàn)了SwiftGif和 YLGIFImage-Swift 這兩個(gè)框架,我看了一下 YLGIFImage-Swift 框架里面的實(shí)現(xiàn),是通過(guò)動(dòng)態(tài)加載動(dòng)畫(huà)幀的形式來(lái)優(yōu)化的。
動(dòng)態(tài)加載幀原理:
- 一開(kāi)始不加載所有圖片幀,只加載少量的幀圖片
- 在動(dòng)畫(huà)執(zhí)行過(guò)程中利用定時(shí)器不斷進(jìn)行加載幀圖片
- 釋放已執(zhí)行完動(dòng)畫(huà)的幀圖片內(nèi)存
- 內(nèi)存消耗降低,這樣的代價(jià)就是會(huì)導(dǎo)致 CPU 的使用提高
因?yàn)轫?xiàng)目代碼使用到的是 Swift3.2,YLGIFImage-Swift 第三方庫(kù)更新比較慢,所以對(duì)該框架手動(dòng)進(jìn)行了一些調(diào)整和優(yōu)化。
三、源代碼解析和優(yōu)化
String+MD5.swift 文件如下:
【需要橋接 OC 頭文件 <CommonCrypto/CommonDigest.h> 】
// String+MD5.swift
import Foundation
extension String {
/// 字符串 MD5 加密
var encodeMD5: String? {
guard let str = cString(using: String.Encoding.utf8) else { return nil }
let strLen = CC_LONG(lengthOfBytes(using: String.Encoding.utf8))
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
let result = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLen)
// MD5 加密
CC_MD5(str, strLen, result)
// 把結(jié)果打印輸出成 16 進(jìn)制字符串
let hash = NSMutableString()
for i in 0..<digestLen {
hash.appendFormat("%02x", result[I])
}
result.deallocate(capacity: digestLen)
return String(format: hash as String)
}
}
GIFImage.swift 文件如下:
// GIFImage.swift
import UIKit
import ImageIO
import MobileCoreServices
class GIFImage {
/// 內(nèi)部讀取圖片幀隊(duì)列
fileprivate lazy var readFrameQueue: DispatchQueue = DispatchQueue(label: "image.gif.readFrameQueue", qos: .background)
/// 圖片資源數(shù)據(jù)
fileprivate var cgImageSource: CGImageSource?
/// 總動(dòng)畫(huà)時(shí)長(zhǎng)
var totalDuration: TimeInterval = 0.0
/// 每一幀對(duì)應(yīng)的動(dòng)畫(huà)時(shí)長(zhǎng)
var frameDurations: [Int: TimeInterval] = [:]
/// 每一幀對(duì)應(yīng)的圖片
var frameImages: [Int: UIImage] = [:]
/// 總圖片數(shù)
var frameTotalCount: Int = 0
/// 兼容之前的 UIImage 使用
var image: UIImage?
/// 全局配置
struct GlobalSetting {
/// 配置預(yù)加載幀的數(shù)量
static var prefetchNumber: Int = 10
static var minFrameDuration: TimeInterval = 0.01
}
/// 兼容 UIImage named 調(diào)用
convenience init?(named name: String!) {
guard let path = Bundle.main.path(forResource: name, ofType: ".gif") else { return nil }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil }
self.init(data: data)
}
/// 兼容 UIImage contentsOfFile 調(diào)用
convenience init?(contentsOfFile path: String) {
guard let url = URL(string: path) else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
self.init(data: data)
}
/// 兼容 UIImage contentsOf 調(diào)用
convenience init?(contentsOf url: URL) {
guard let data = try? Data(contentsOf: url) else { return nil }
self.init(data: data)
}
/// 兼容 UIImage data 調(diào)用
convenience init?(data: Data) {
self.init(data: data, scale: 1.0)
}
/// 根據(jù)二進(jìn)制數(shù)據(jù)初始化【核心初始化方法】
init?(data: Data, scale: CGFloat) {
guard let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return }
self.cgImageSource = cgImageSource
if GIFImage.isCGImageSourceContainAnimatedGIF(cgImageSource: cgImageSource) {
initGIFSource(cgImageSource: cgImageSource)
} else {
image = UIImage(data: data, scale: scale)
}
}
/// 判斷圖片數(shù)據(jù)源包含 GIF 信息
fileprivate class func isCGImageSourceContainAnimatedGIF(cgImageSource: CGImageSource) -> Bool {
guard let type = CGImageSourceGetType(cgImageSource) else { return false }
let isGIF = UTTypeConformsTo(type, kUTTypeGIF)
let imgCount = CGImageSourceGetCount(cgImageSource)
return isGIF && imgCount > 1
}
/// 獲取圖片數(shù)據(jù)源的第 index 幀圖片的動(dòng)畫(huà)時(shí)間
fileprivate class func getCGImageSourceGifFrameDelay(imageSource: CGImageSource, index: Int) -> TimeInterval {
var delay = 0.0
guard let imgProperties: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) else { return delay }
// 獲取該幀圖片的屬性字典
if let property = imgProperties[kCGImagePropertyGIFDictionary as String] as? NSDictionary {
// 獲取該幀圖片的動(dòng)畫(huà)時(shí)長(zhǎng)
if let unclampedDelayTime = property[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber {
delay = unclampedDelayTime.doubleValue
if delay <= 0, let delayTime = property[kCGImagePropertyGIFDelayTime as String] as? NSNumber {
delay = delayTime.doubleValue
}
}
}
return delay
}
/// 根據(jù)圖片數(shù)據(jù)源初始化,設(shè)置動(dòng)畫(huà)總時(shí)長(zhǎng)、總幀數(shù)等屬性
fileprivate func initGIFSource(cgImageSource: CGImageSource) {
let numOfFrames = CGImageSourceGetCount(cgImageSource)
frameTotalCount = numOfFrames
for index in 0..<numOfFrames {
// 獲取每一幀的動(dòng)畫(huà)時(shí)長(zhǎng)
let frameDuration = GIFImage.getCGImageSourceGifFrameDelay(imageSource: cgImageSource, index: index)
self.frameDurations[index] = max(GlobalSetting.minFrameDuration, frameDuration)
self.totalDuration += frameDuration
// 一開(kāi)始初始化預(yù)加載一定數(shù)量的圖片,而不是全部圖片
if index < GlobalSetting.prefetchNumber {
if let cgimage = CGImageSourceCreateImageAtIndex(cgImageSource, index, nil) {
let image: UIImage = UIImage(cgImage: cgimage)
if index == 0 {
self.image = image
}
self.frameImages[index] = image
}
}
}
}
/// 獲取某一幀圖片
func getFrame(index: Int) -> UIImage? {
guard index < frameTotalCount else { return nil }
// 取當(dāng)前幀圖片
let currentImage = self.frameImages[index] ?? self.image
// 如果總幀數(shù)大于預(yù)加載數(shù),需要加載后面未加載的幀圖片
if frameTotalCount > GlobalSetting.prefetchNumber {
// 清除當(dāng)前幀圖片緩存數(shù)據(jù),空出內(nèi)存
if index != 0 {
self.frameImages[index] = nil
}
// 加載后面幀圖片到內(nèi)存
for i in 1...GlobalSetting.prefetchNumber {
let idx = (i + index) % frameTotalCount
if self.frameImages[idx] == nil {
// 默認(rèn)加載第一張幀圖片為占位,防止多次加載
self.frameImages[idx] = self.frameImages[0]
self.readFrameQueue.async { [weak self] in
guard let strongSelf = self, let cgImageSource = strongSelf.cgImageSource else { return }
guard let cgImage = CGImageSourceCreateImageAtIndex(cgImageSource, idx, nil) else { return }
strongSelf.frameImages[idx] = UIImage(cgImage: cgImage)
}
}
}
}
return currentImage
}
}
BasicGIFImageView.swift 文件如下:
// BasicGIFImageView.swift
import UIKit
import QuartzCore
class BasicGIFImageView: UIImageView {
/// 后臺(tái)下載圖片隊(duì)列
fileprivate lazy var downloadImageQueue: DispatchQueue = DispatchQueue(label: "image.gif.downloadImageQueue", qos: .background)
/// 累加器,用于計(jì)算一個(gè)定時(shí)循環(huán)中的可用動(dòng)畫(huà)時(shí)間
fileprivate var accumulator: TimeInterval = 0.0
/// 當(dāng)前正在顯示的圖片幀索引
fileprivate var currentFrameIndex: Int = 0
/// 當(dāng)前正在顯示的圖片
fileprivate var currentFrame: UIImage?
/// 動(dòng)畫(huà)圖片存儲(chǔ)屬性
fileprivate var animatedImage: GIFImage?
/// 定時(shí)器
fileprivate var displayLink: CADisplayLink!
/// 當(dāng)前將要顯示的 GIF 圖片資源路徑
fileprivate var gifUrl: URL?
/// 重載初始化,初始化定時(shí)器
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupDisplayLink()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupDisplayLink()
}
override init(image: UIImage?) {
super.init(image: image)
setupDisplayLink()
}
override init(image: UIImage?, highlightedImage: UIImage!) {
super.init(image: image, highlightedImage: highlightedImage)
setupDisplayLink()
}
/// 當(dāng)設(shè)置該屬性時(shí),將不顯示 GIF 動(dòng)效
override var image: UIImage? {
get {
if let animatedImage = self.animatedImage {
return animatedImage.getFrame(index: 0)
} else {
return super.image
}
}
set {
if image === newValue {
return
}
super.image = newValue
self.gifImage = nil
}
}
/// 設(shè)置 GIF 圖片
var gifImage: GIFImage? {
get {
return self.animatedImage
}
set {
if animatedImage === newValue {
return
}
self.stopAnimating()
self.currentFrameIndex = 0
self.accumulator = 0.0
if let newAnimatedImage = newValue {
self.animatedImage = newAnimatedImage
if let currentImage = newAnimatedImage.getFrame(index: 0) {
super.image = currentImage
self.currentFrame = currentImage
}
self.startAnimating()
} else {
self.animatedImage = nil
}
self.layer.setNeedsDisplay()
}
}
/// 當(dāng)顯示 GIF 時(shí),不處理高亮狀態(tài)
override var isHighlighted: Bool {
get {
return super.isHighlighted
}
set {
if self.animatedImage == nil {
super.isHighlighted = newValue
}
}
}
/// 獲取是否正在動(dòng)畫(huà)
override var isAnimating: Bool {
if self.animatedImage != nil {
return !self.displayLink.isPaused
} else {
return super.isAnimating
}
}
/// 開(kāi)啟定時(shí)器
override func startAnimating() {
if self.animatedImage != nil {
self.displayLink.isPaused = false
} else {
super.startAnimating()
}
}
/// 暫停定時(shí)器
override func stopAnimating() {
if self.animatedImage != nil {
self.displayLink.isPaused = true
} else {
super.stopAnimating()
}
}
/// 當(dāng)前顯示內(nèi)容為 GIF 當(dāng)前幀圖片
override func display(_ layer: CALayer) {
if self.animatedImage != nil {
if let frame = self.currentFrame {
layer.contents = frame.cgImage
}
}
}
/// 初始化定時(shí)器
fileprivate func setupDisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(BasicGIFImageView.changeKeyFrame))
self.displayLink.add(to: RunLoop.main, forMode: .commonModes)
self.displayLink.isPaused = true
}
/// 動(dòng)態(tài)改變圖片動(dòng)畫(huà)幀
@objc fileprivate func changeKeyFrame() {
if let animatedImage = self.animatedImage {
guard self.currentFrameIndex < animatedImage.frameTotalCount else { return }
self.accumulator += min(1.0, displayLink.duration)
var frameDuration = animatedImage.frameDurations[self.currentFrameIndex] ?? displayLink.duration
while self.accumulator >= frameDuration {
self.accumulator -= frameDuration
self.currentFrameIndex += 1
if self.currentFrameIndex >= animatedImage.frameTotalCount {
self.currentFrameIndex = 0
}
if let currentImage = animatedImage.getFrame(index: self.currentFrameIndex) {
self.currentFrame = currentImage
}
self.layer.setNeedsDisplay()
if let newFrameDuration = animatedImage.frameDurations[self.currentFrameIndex] {
frameDuration = min(displayLink.duration, newFrameDuration)
}
}
} else {
self.stopAnimating()
}
}
/// 顯示本地 GIF 圖片
func showLocalGIF(name: String?) {
guard let name = name else { return }
self.gifImage = GIFImage(named: name)
}
/// 根據(jù) urlStr 顯示網(wǎng)絡(luò) GIF 圖片
func showNetworkGIF(urlStr: String?) {
guard let urlStr = urlStr else { return }
guard let url = URL(string: urlStr) else { return }
showNetworkGIF(url: url)
}
/// 根據(jù) url 顯示網(wǎng)絡(luò) GIF 圖片
func showNetworkGIF(url: URL) {
guard let fileName = url.absoluteString.encodeMD5, let directoryPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { return }
let filePath = (directoryPath as NSString).appendingPathComponent("\(fileName).gif") as String
let fileUrl = URL(fileURLWithPath: filePath)
self.gifUrl = fileUrl
// 后臺(tái)下載網(wǎng)絡(luò)圖片或者加載本地緩存圖片
self.downloadImageQueue.async { [weak self] in
if FileManager.default.fileExists(atPath: filePath) { // 本地緩存
let gifImage = GIFImage(contentsOf: fileUrl)
DispatchQueue.main.async { [weak self] in
if let strongSelf = self, strongSelf.gifUrl == fileUrl {
strongSelf.gifImage = gifImage
}
}
} else { // 網(wǎng)絡(luò)加載
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in
guard let data = data else { return }
do {
try data.write(to: fileUrl, options: .atomic)
} catch {
debugPrint(error)
}
let gifImage = GIFImage(data: data)
DispatchQueue.main.async { [weak self] in
if let strongSelf = self, strongSelf.gifUrl == fileUrl {
strongSelf.gifImage = gifImage
}
}
})
task.resume()
}
}
}
}
使用如下:
// ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var networkImageView: BasicGIFImageView!
@IBOutlet weak var localImageView: BasicGIFImageView!
override func viewDidLoad() {
super.viewDidLoad()
// 加載網(wǎng)絡(luò) GIF 圖片
let testUrlStr = "https://images.ifanr.cn/wp-content/uploads/2018/05/2018-05-09-17_22_48.gif"
networkImageView.showNetworkGIF(urlStr: testUrlStr)
// 加載本地 GIF 圖片
localImageView.showLocalGIF(name: "test")
}
}
Demo 源代碼在這:GIFImageLoadDemo
有什么問(wèn)題可以在下方評(píng)論區(qū)提出,寫(xiě)得不好可以提出你的意見(jiàn),我會(huì)合理采納的,O(∩_∩)O哈哈~,求關(guān)注求贊