圖像IO之圖片加載跪妥、解碼声滥,緩存

CoreAnimationXmind.png

這是 Core Animation 的系列文章,介紹了 Core Animation 的用法纽疟,以及如何進行性能優(yōu)化憾赁。

  1. CoreAnimation基本介紹
  2. CGAffineTransform和CATransform3D
  3. CALayer及其各種子類
  4. CAAnimation:屬性動畫CABasicAnimation、CAKeyframeAnimation以及過渡動畫膘壶、動畫組
  5. 圖層時間CAMediaTiming
  6. 計時器CADisplayLink
  7. 影響動畫性能的因素及如何使用 Instruments 檢測
  8. 圖像IO之圖片加載、解碼顷锰,緩存
  9. 圖層性能之離屏渲染亡问、柵格化州藕、回收池

上一篇文章介紹了 Core Animation 如何進行渲染,可能的性能瓶頸點毁涉,以及如何使用 Instruments 檢測修復锈死。這一篇文章介紹如何優(yōu)化加載、渲染磁盤待牵、網(wǎng)絡中的圖片其屏。

1. 加載和延遲

圖片繪制通常不是最影響性能的部分缨该。圖片很消耗內(nèi)存,因此不太可能把所有需要顯示的圖片都保留在內(nèi)存中蛤袒,app 運行時需不斷加載汗盘、釋放圖片询一。

圖片加載速度不僅受 CPU 制約,還受 IO(Input/Output)影響菱阵。磁盤讀取速度比 RAM 慢很多缩功,需謹慎管理加載嫡锌,減少延遲琳钉。點擊按鈕到收到響應之間最好不超過200ms蛛倦,否則會有卡頓的感覺溯壶。

通常,不能把所有圖片加載到內(nèi)存中验烧。例如又跛,輪播的圖片可能有很多張效扫,都加載到內(nèi)存中太占用內(nèi)存直砂。此外,很多時候圖片來自網(wǎng)絡济丘,下載會很耗時摹迷,甚至失敗郊供。

1.1 加載線程

上一篇文章的圖片很小,直接在主線程進行了加載鲫寄。如果直接在主線程加載大圖疯淫,會導致主界面失去響應熙掺。滑動動畫在主線程的UITrackingRunLoopMode執(zhí)行蜡秽,比在 render tree 執(zhí)行的CAAnimation更容易出現(xiàn)卡頓。

下面代碼使用UICollectionView實現(xiàn)了圖片播放肌似,在collectionView(_:cellForItemAt:)方法中使用主線程同步加載圖片川队。如下所示:

class ImageIOViewController: BaseViewController {
    
    let flowLayout: UICollectionViewFlowLayout = {
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.scrollDirection = .horizontal
        flowLayout.itemSize = UIScreen.main.bounds.size
        return flowLayout
    }()
    var collectionView: UICollectionView?
    
    private let cellIdentifier = "cellIdentifier"
    var imagePaths = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        imagePaths = Bundle.main.paths(forResourcesOfType: "png", inDirectory: "Vacation Photos")
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
        collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
        collectionView?.dataSource = self
        view.addSubview(collectionView!)
        
        collectionView?.translatesAutoresizingMaskIntoConstraints = false
        collectionView?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView?.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView?.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        collectionView?.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    }
}

extension ImageIOViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return imagePaths.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        
        // Add image view
        let imageTag = 99
        var imageView = cell.viewWithTag(imageTag) as? UIImageView
        if imageView == nil {
            imageView = UIImageView(frame: cell.contentView.bounds)
            imageView!.tag = imageTag
            cell.contentView.addSubview(imageView!)
        }
        
        // Set image
        let imagePath = imagePaths[indexPath.item]
        imageView?.image = UIImage(contentsOfFile: imagePath)
        
        return cell
    }
}

運行后如下:

CASyncImage.png

使用 Instruments 分析可以發(fā)現(xiàn)瓶頸在于collectionView(_:cellForItemAt:)中的UIImage(contentsOfFile: )方法,如下所示:

CAIOTimeProfiler.png

把加載圖片任務轉(zhuǎn)移到其他線程斗躏,這樣主線程就可以處理交互任務昔脯,滑動時就不會出現(xiàn)卡頓。

使用后臺線程加載圖片雖然可以解決卡頓問題云稚,但圖片加載時長并沒有減少静陈,甚至可能因為后臺線程優(yōu)先級低圖片加載時長變長,但可以充分發(fā)揮設備多核優(yōu)勢拐格。點擊多線程簡述了解更多刑赶。

可以使用 GCD撞叨、OperationQueue等多線程方案后臺加載圖片,也可以使用CATiledLayer異步加載圖片谒所。如果是網(wǎng)絡圖片劣领,可以使用URLSession異步加載。

1.2 GCD

collectionView(_:cellForItemAt:)方法中使用GCD的后臺線程加載圖片奕锌,圖片加載完成后切換至主線程更新UI。UICollectionView會回收復用 cell饼丘,圖片加載完成時 cell 可能已經(jīng)被復用肄鸽。為避免圖片顯示到錯誤 cell油啤,顯示圖片前為視圖添加 tag,設置圖片前檢查 tag 是否存在逮诲。更新后如下:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        
        // Add image view
        let imageTag = 99
        var imageView = cell.viewWithTag(imageTag) as? UIImageView
        if imageView == nil {
            imageView = UIImageView(frame: cell.contentView.bounds)
            imageView!.tag = imageTag
            cell.contentView.addSubview(imageView!)
        }
        
        // Tag cell with index and clear current image
        cell.tag = indexPath.row
        imageView?.image = nil
        
        // Switch to background thread
        DispatchQueue.global(qos: .utility).async {
            // Load image
            let idx = indexPath.item
            let imagePath = self.imagePaths[idx]
            let img = UIImage(contentsOfFile: imagePath)
            
            // Set image on main thread, but only if index still matches up
            DispatchQueue.main.async {
                if idx == cell.tag {
                    imageView?.image = img
                }
            }
        }
        
        return cell
    }

再次使用 Instruments 分析梅鹦,已經(jīng)看不到UIImage(contentsOfFile:)方法了齐唆。性能有了一定程度提升蒿讥,但仍有掉幀抛腕。

1.3 延遲解碼

圖片加載后還需解碼担敌,解碼過程可能需要復雜計算,會耗費一定時間马昙。解碼后的圖片也會占用更多內(nèi)存資源刹悴。

加載和解碼所需 CPU 時間因圖片格式而異土匀。png 文件大,加載時間長证杭,但解碼速度快,Xcode 會對項目內(nèi) png 圖片進行壓縮優(yōu)化镇饺。JPEG 文件小送讲,加載快哼鬓,但因編碼算法復雜,解碼速度慢秸侣。

為了減少內(nèi)存占用宠互,系統(tǒng)會延遲解碼予跌,直到需要繪制時才解碼,但這樣也容易引起性能問題频轿。

有以下三種立即解碼的方案:

  • 使用UIImage(named:)方法加載圖片烁焙。與UIImage(contentsOfFile:)不同骄蝇,UIImage(named:)加載后立即解碼。UIImage(named:)加載圖片有以下特點:
    • UIImage(named:)只加載 bundle 內(nèi)圖片赚窃,不適用于用戶生成的岔激、網(wǎng)絡獲取的圖片虑鼎。
    • UIImage(named:)方法會自動緩存圖片冀惭,后續(xù)使用時直接從內(nèi)存讀取散休。系統(tǒng)的按鈕乐尊、背景等都是使用UIImage(named:)方法加載的圖片。如果使用UIImage(named:)加載大圖限府,系統(tǒng)可能移除界面控件圖片緩存胁勺,當導航回這些界面時独旷,需重新加載這些圖片嵌洼。使用單獨緩存可以解耦圖片緩存與 app 生命周期。
    • UIImage(named:)緩存是私有的褐啡,不能查詢圖片是否在緩存中鳖昌,也不能從緩存中移除不再使用的圖片许昨。
  • 為圖層的contents屬性設置圖片,或為UIImageviewimage屬性賦值允粤,但這些操作都必須在主線程進行翼岁,也就是不能用來解決性能問題琅坡。
  • 繞過UIKit框架残家,直接使用ImageIO框架。
  • 使用UIKit框架加載圖片陪捷,加載后立即在CGContext中繪制诺擅。

使用ImageIO解碼方案如下:

            // Load image
            let idx = indexPath.item
            let imageUrl = URL(fileURLWithPath: self.imagePaths[idx])
            
            let options = [kCGImageSourceShouldCache : true]
            let source = CGImageSourceCreateWithURL(imageUrl as CFURL, nil)
            var img = UIImage()
            if source != nil {
                let imageRef = CGImageSourceCreateImageAtIndex(source!, 0, options as CFDictionary)
                if imageRef != nil {
                    img = UIImage(cgImage: imageRef!)
                }
            }

kCGImageSourceShouldCache選項會緩存解碼的圖片烁涌,直到圖片被銷毀。

圖片繪制前必須解碼微峰,繪制可以像加載一樣在后臺線程進行蜓肆,避免堵塞主線程谋币。為了實現(xiàn)立即解碼瑞信,有以下兩種方案:

  • 在一像素的CGContext中繪制一像素的圖片。這樣繪制速度極快逼友,也會對整個圖片進行解碼秤涩。缺點在于繪制不能針對設備優(yōu)化筐眷,后續(xù)繪制時耗時可能較長。系統(tǒng)為了優(yōu)化內(nèi)存可能釋放掉已解碼的圖片照棋。
  • 把整張圖片繪制到CGContext烈炭,使用 context 的 content 替換原始圖片宝恶。這會比繪制一像素耗費性能,但繪制會針對設備進行優(yōu)化霹疫。由于原始圖片已經(jīng)被舍棄丽蝎,系統(tǒng)無法決定是否釋放掉內(nèi)存中解碼的圖片征峦。

系統(tǒng)并不推薦使用這些技巧立即解碼圖片,但如果你的 app 有大量圖片类腮,你可能用得到這些技巧蛉加。

如果圖片顯示大小與自身大小不一致针饥,使用后臺線程繪制為顯示大小更有利于性能,而非每次顯示時進行縮放筷凤。更新collectionView(_:cellForItemAt:)方法藐守,顯示圖片前進行重繪蹂风,更新后如下:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        ...
        
        // Switch to background thread
        DispatchQueue.global(qos: .utility).async {
            // Load image
            let idx = indexPath.item
            let imagePath = self.imagePaths[idx]
            var img = UIImage(contentsOfFile: imagePath)
            
            // Redraw image using device context
            UIGraphicsBeginImageContextWithOptions(imgSize ?? CGSize.zero, true, 0)
            img?.draw(in: imgBounds ?? CGRect.zero)
            img = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            // Set image on main thread, but only if index still matches up
            DispatchQueue.main.async {
                if idx == cell.tag {
                    imageView?.image = img
                }
            }
        }
        
        return cell
    }

再次滑動 collection view惠啄,滑動已經(jīng)非常流暢撵渡。

2. NSCache

創(chuàng)建自定義的緩存系統(tǒng)是必要的。創(chuàng)建自定義緩存涉及以下幾方面:

  • 選擇緩存鍵丹鸿。緩存鍵用來標記緩存中的資源靠欢。在上述示例中铜跑,可以使用圖片名稱锅纺、cell index作為 key。
  • 提前緩存坦弟。如果加載官地、生成數(shù)據(jù)比較慢岁疼,可以在使用前提前加載缎岗。在上述示例中莺褒,可以根據(jù)當前滑動位置遵岩、方向判斷將要顯示哪個圖片巡通,提前加載扁达。
  • 緩存失效。緩存的圖片更新后炉旷,如何更新緩存中圖片窘行?可以在緩存圖片時添加時間戳图仓,提取時比較文件修改日期救崔。
  • 緩存回收捏顺。緩存將要耗盡時幅骄,先移除哪些緩存本今?這需要一套算法冠息,根據(jù)使用頻率、加載耗費資源等決定躏碳。幸運的是 Apple 提供的NSCache類已經(jīng)自動處理了這些問題唐断。

NSCacheNSDictionary有很多相似之處脸甘,可以通過setObject(_:forKey:)偏灿、object(forKey:)方法設置翁垂、獲取緩存中的資源,但NSCache在內(nèi)存不足時會自動釋放緩存中資源枚荣。

NSCache文檔并沒有介紹管理緩存的算法,但可以使用countLimit設置緩存對象數(shù)量上限啼肩,使用totalCostLimit設置緩存大小限制橄妆。

NSCache是通用緩存解決方案。雖然我們可以創(chuàng)建自定義類祈坠,針對滑動圖片進行優(yōu)化害碾,但NSCache已經(jīng)能夠滿足當前需求,無需過早優(yōu)化赦拘。

下面代碼使用NSCache提前加載圖片慌随,查看滾動效果是否更好。

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        
        var imageView = cell.contentView.subviews.last as? UIImageView
        if imageView == nil {
            imageView = UIImageView(frame: cell.contentView.bounds)
            imageView?.contentMode = .scaleAspectFit
            cell.contentView.addSubview(imageView!)
        }
        
        // Set or load iamge for this index
        imageView?.image = loadImage(at: indexPath.item)
        
        // Preload image for previous and next index.
        if indexPath.item < self.imagePaths.count - 1 {
            loadImage(at: indexPath.item + 1)
        }
        if indexPath.item > 0 {
            loadImage(at: indexPath.item - 1)
        }
        
        return cell
    }
    
    @discardableResult
    func loadImage(at index: Int) -> UIImage? {
        let image: UIImage? = cache.object(forKey: CustomKey(int: index, string: String(index))) as? UIImage
        
        if image != nil {
            if image!.isKind(of: NSNull.self) {
                return nil
            } else {
                return image
            }
        }
        
        // Set placeholder to avoid reloading image multiple times
        cache.setObject(NSNull.self, forKey: CustomKey(int: index, string: String(index)))
        
        // Switch to background thread
        DispatchQueue.global().async {
            // Load Image
            let imagePath = self.imagePaths[index]
            
            var image = UIImage(contentsOfFile: imagePath)
            
            // Redraw image using device context
            UIGraphicsBeginImageContextWithOptions(image?.size ?? CGSize.zero, true, 0)
            image?.draw(at: CGPoint.zero)
            image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            // Set image for correct image view
            DispatchQueue.main.async {
                self.cache.setObject(image ?? NSNull.self, forKey: CustomKey(int: index, string: String(index)))
                
                // Display the image
                let indexPath = NSIndexPath(item: index, section: 0) as IndexPath
                let cell = self.collectionView?.cellForItem(at: indexPath)
                
                let imageView = cell?.contentView.subviews.last as? UIImageView
                imageView?.image = image
            }
        }
        
        return nil
    }

再次滑動 collection view阁猜,效果非常好丸逸。這里提前加載邏輯非常粗暴,其實還可以把滑動方向和速度考慮進來蹦漠。

總結(jié)

這篇文章介紹了圖片加載椭员、解碼可能涉及到的性能問題车海,并提供了一些解決方案笛园。下一篇文章圖層性能之離屏渲染、柵格化侍芝、回收池將介紹圖層渲染和圖層組合相關的性能問題研铆。

上一篇:影響動畫性能的因素及如何使用 Instruments 檢測

下一篇:圖層性能之離屏渲染、柵格化州叠、回收池

Demo名稱:CoreAnimation
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreAnimation

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/圖像IO之圖片加載棵红、解碼,緩存.md

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咧栗,一起剝皮案震驚了整個濱河市逆甜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌致板,老刑警劉巖交煞,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異斟或,居然都是意外死亡素征,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門萝挤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來御毅,“玉大人,你說我怎么就攤上這事怜珍《饲” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵酥泛,是天一觀的道長今豆。 經(jīng)常有香客問我,道長揭璃,這世上最難降的妖魔是什么晚凿? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瘦馍,結(jié)果婚禮上歼秽,老公的妹妹穿的比我還像新娘。我一直安慰自己情组,他們只是感情好燥筷,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布箩祥。 她就那樣靜靜地躺著,像睡著了一般肆氓。 火紅的嫁衣襯著肌膚如雪袍祖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天谢揪,我揣著相機與錄音蕉陋,去河邊找鬼。 笑死拨扶,一個胖子當著我的面吹牛凳鬓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播患民,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缩举,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匹颤?” 一聲冷哼從身側(cè)響起仅孩,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎印蓖,沒想到半個月后辽慕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡另伍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年鼻百,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摆尝。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡温艇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出堕汞,到底是詐尸還是另有隱情勺爱,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布讯检,位于F島的核電站琐鲁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏人灼。R本人自食惡果不足惜围段,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望投放。 院中可真熱鬧奈泪,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冯遂,卻和暖如春蕊肥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蛤肌。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工壁却, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寻定。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓儒洛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親狼速。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容