背景
之前公司APP中g(shù)if下載的主要代碼就一句話NSData *gifData = [NSData dataWithContentsOfURL:[NSURL URLWithString:newUrlString]];所有的下載都交給dataWithContentsOfURL:處理。這種做法太過簡單,隨著業(yè)務(wù)的壯大,很多需求滿足不了,并且這種方式不支持https,并且該方法阻塞線程,很多gif圖片較大會有明顯卡頓,因此急需自己創(chuàng)建一套gif下載器,用來展示gif圖片。
思路
gif圖片加載
gif圖片加載和普通圖片加載顯示過程基本一致,無非就是先從緩存中獲取,如果沒命中就從沙盒緩存中獲取,如果還沒命中就從遠(yuǎn)程下載并緩存。唯一有區(qū)別的是,gif在列表中只需要顯示一張靜態(tài)的圖片,在查看詳情的時(shí)候需要加載動圖,所以一個(gè)gif下載完成需要緩存2種圖片。
gif圖片緩存
緩存分兩種,內(nèi)存緩存和本地緩存。由于我們項(xiàng)目中使用SDWebImage,并且設(shè)置圖片最大開銷為100M,為了統(tǒng)一方便內(nèi)存管理,我們直接使用SDImageCache進(jìn)行讀取和存儲緩存。由于一張gif對應(yīng)一個(gè)靜態(tài)圖片和一個(gè)動態(tài)圖片,所以需要用兩個(gè)key來區(qū)別是動態(tài)圖還是靜態(tài)圖,但是一般一次下載圖片只有一個(gè)key值(默認(rèn)URL地址),所以在緩存圖片時(shí),在原來key值上加上前綴static_和dynamic_來區(qū)別。

沙盒緩存就比較簡單了,我們是創(chuàng)建了2個(gè)目錄,一個(gè)存放動態(tài)圖,一個(gè)存放靜態(tài)圖。由于iOS本地沙盒文件名稱不能太長(之前被坑過),所以我們采用md5(key+圖片唯一標(biāo)志)來保證名稱唯一性和長度。
gif圖片下載
下載的方式有很多種,這里我們打算采用AFNetworking中封裝好的AFURLSessionManager的downloadTaskWithRequest:progress:destination:completionHandler:方法進(jìn)行下載,因?yàn)槠浜芎玫姆庋b了下載進(jìn)度和指定存儲地址,這正是我們業(yè)務(wù)上需要用到的。這里需要注意,AFURLSessionManager最好寫成單例,否則有內(nèi)存泄漏。

靜態(tài)gif生成
老版本中SDWebImage顯示gif圖片都是靜態(tài)的,不知道什么版本起就變成動態(tài)的了,這里用原始gif生成靜態(tài)圖片的代碼就是參考老版本SDWebImage庫中的代碼。

圖片的展示形式
由于我們項(xiàng)目中是采用SDWebImage進(jìn)行圖片展示,因此為了代碼上的統(tǒng)一,gif也采用UIImageView類別的形式進(jìn)行加載圖片。

可以看到,加載方式比較簡單明了,兩個(gè)set方法和一個(gè)cancel方法就搞定動態(tài)和靜態(tài)gif,還帶有下載進(jìn)度。
cell復(fù)用等問題
為了解決cell復(fù)用問題,實(shí)際上就是再每次賦值時(shí)取消UIImageView上次任務(wù)的block回調(diào),用新的回調(diào)替換它。我們可以創(chuàng)建一個(gè)隊(duì)列,可以采用NSOperationQueue和信號量結(jié)合,實(shí)現(xiàn)控制每次最多只有3個(gè)線程下載圖片,每次下載任務(wù)都是以NSBlockOperation形式加入隊(duì)列,每次下載任務(wù)前檢查下任務(wù)是否被取消,如果取消了就跳過該任務(wù)。這樣就可以簡單得實(shí)現(xiàn)cell復(fù)用問題的規(guī)避。這個(gè)原理和SDWebImage有點(diǎn)類似,具體可以參考其內(nèi)部實(shí)現(xiàn)。
代碼實(shí)現(xiàn)后遇到的問題
SDImageCache計(jì)算內(nèi)存cost不準(zhǔn)確
之前為了方便內(nèi)存管理和圖方便,采用SDImageCache進(jìn)行緩存gif靜態(tài)圖和原圖。但是實(shí)際測試后發(fā)現(xiàn),明明限制最大內(nèi)存消耗為100M,但是實(shí)際上會遠(yuǎn)遠(yuǎn)超過。排查后發(fā)現(xiàn)問題出現(xiàn)在這:

memCache計(jì)算cost主要是靠SDCacheCostForImage()方法,而SD計(jì)算Image在內(nèi)存中的大小居然只是簡單的根據(jù)公式:圖片大小=圖片長度x圖片高度x圖片比例x圖片比例

一個(gè)gif原圖里面有多張圖片組成,如果按這個(gè)計(jì)算方法,只能算計(jì)出一張圖片的大小,其誤為gif有多少張就差多少倍(還不算其他時(shí)間等信息)。
再后來進(jìn)過嘗試,如果把Gif原圖存內(nèi)存中,隨便一張網(wǎng)上下載的gif圖片就占用了200M內(nèi)存,遠(yuǎn)超限制100M,所以將gif原圖存儲到內(nèi)存中不是一個(gè)明智的選擇,所以我們對gif只采用沙盒緩存,不采用內(nèi)存緩存,實(shí)際測試效果不會差太多。
Cell復(fù)用,劃出屏幕的gif動圖未釋放
上面所述,gif顯示會占用大量內(nèi)存,一般都放在cell中的UIImageView中。這里叫考慮cell的復(fù)用了,由于cell復(fù)用機(jī)制,cell的imageView自然也不會釋放,那么image也不會釋放,存在內(nèi)存中,所以如果是動態(tài)gif圖片顯示,還需要主要當(dāng)cell劃出顯示區(qū)域后手動將UIImageView.image置為nil,減少內(nèi)存占用。
總結(jié)
總體下載,本次嘗試還是比較順利的,中間遇到一些問題,最后也能得到很好的解決。由于gif圖片較大,還測試出很多問題,比如長圖,超大圖片都有類似的內(nèi)存問題。所以之后在內(nèi)存管理方面還是要多留一個(gè)心眼,多測試極端情況。