我們的 iOS 應用都包含了大量的圖像。創建富有吸引力的視圖,主要依賴于大量的裝飾圖片,所有這些首先必須從遠程服務器獲取。如果每次打開應用都要從服務器一次又一次的獲取每個圖像,那么用戶體驗肯定達不到好的效果,所以本地緩存遠程圖像是非常有必要的。
版本1-尋找一張圖片,并從磁盤上讀取它
我們的第一個圖片緩存是簡單但是有效的。從緩存中尋找每一個我們曾經訪問過的圖片,用遠程的URL作為緩存的鍵值。如果本地的磁盤緩存時有效的,從磁盤中讀取文件并創建UIImage,并立即返回。如果再磁盤上沒有找到文件,異步的從遠程URL獲取文件,緩存到磁盤,然后返回一個新建的UIImage。
目前這對于我們的使用是完全滿足了。但是它有一個不必要的弱點:每次緩存請求都需要從磁盤讀取圖片,性能都消耗在對磁盤的訪問和圖片文件的解碼上了。
版本 2 - 內存緩存
謝天謝地,蘋果的UIImage有內置的內存緩存。所以,你只需修改一行代碼,圖片就能從磁盤緩存改為內存緩存。
當你使用imageNamed:獲取UIImage的時候,它第一步就是檢查自己的內存看看是否已經加載過這個圖片。如果這樣,你不用任何系統花銷就能得到一個UIImage實例。所以把原本這樣的寫法替換掉:
return [UIImage imageWithContentsOfFile:[self absolutePathForURL:url]];
我們可以零花銷去訪問內存緩存,用下面的代碼就可以了:
return [UIImage imageNamed:[self relativePathForURL:url]];
UIImage 會查找它的內存緩存,如果找到,就會零消耗地返回這個照片. 如果沒有找到,這張照片就會從磁盤加載,消耗一定的系統性能。
版本3 —— 獲取隊列、數據預取和變量催促
當改進應用程序設計時我們渴望更多的圖片元素,渴望更炫的畫面,更大的圖片,更多的圖片。
讓這些大圖盡可能快的顯示在屏幕上對用戶體驗來說是至關重要的,而只是簡單地在每次需要顯示的時候去緩存當中抓取圖片數據是不能夠解決問題的。大圖需要更多的時間才能從網絡上加載回來,并且一次請求太多的圖片會導致所有圖片都來不及加載。需要仔細考慮什么時候去檢查緩沖當中有無圖片數據以及什么時候從網絡上抓取圖片。我們需要預緩存和抓取列陣。
快速隊列和慢速隊列
我們設置了兩個隊列,一個串行,一個并行。在屏幕上被迫切要求的圖片進入并行隊列(fastQueue),可能晚點才需要的圖片進入串行隊列(slowQueue)。
就UITableView的實現而言,這意味著在屏幕上的表格單元從fastQueue獲取圖片, 每個關閉的屏幕行的圖片從slowQueue預加載。
現在不需要處理圖片
假設我們要從服務器上請求包含30條事件的一頁資訊回來,一旦這些內容請求回來時我們就可以排隊等待預取其中的每一張圖。
- (void)pageLoaded:(NSArray *)newEvents {
for (SGEvent *event in newEvents) {
[SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];
}
}
slowGetImageForURL:這個方法將圖片添加到slowQueue這個隊列當中,允許它們在不阻塞網絡通信的前提下被一張一張的取出來。
thenDo:這個代碼塊在這里是沒有被實現,是因為我們目前還不需要對圖片做任何事情。所有我們需要做的就是確保它們在本地磁盤緩存當中,并且隨時準備在屏幕上滑動表格時來使用。
現在就要處理圖片
顯示在屏幕上的表格希望立即顯示它們的圖片,所以在table cell子類當中實現:
- (void)setEvent:(SGEvent *)event {
__weak SGEventCell *me = self;
[SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) {
me.imageView.image = image; }
];
}
getImageForURL:這個方法將抓取圖片的過程添加到fastQueue這個隊列當中,意味著只要iOS系統允許,它們會并行被地執行。如果抓取圖片的過程已經存在于slowQueue隊列當中,它會被移動到fastQueue隊列中,從而避免重復請求。
一直異步
等等,getImageForURL:不是一個異步方法嗎?如果你明知道圖片已經在緩存中,但是卻不想在主線程上立即使用它嗎?直覺告訴你那是錯誤的。
從磁盤上加載圖片太費資源,同樣解壓圖片也會費很多資源??梢栽诨瑒拥倪^程當中進行配置和添加表格,這最后一件你想在滑動表格時做的事是很危險地,因為它會阻塞主線程,會有卡頓的現象出現。
使用getImageForURL:可以讓磁盤加載的動作脫離主線程,于是當thenDo:這個用于收尾工作的代碼塊執行的時候它已經有了一個UIImage實例,從而不會有滑動卡頓的危險。如果圖片已經存在于本地緩存當中,用于收尾工作的代碼塊會在下一次運行周期執行,并且用戶不會注意到兩者之間的差別。他們會注意到的是滑動不會卡頓了。
現在,不需要你快速執行
如果用戶很快的滑動表格到底部,幾十或幾百個表格單元會出現在屏幕上,并向fastQueue請求圖片數據,然后很快地從屏幕上消失。突然間這個并行地隊列會將大量實際上不再需要的圖片請求充斥進網絡。當用戶最終停止滑動時,那些當前屏幕上相應的表格單元視圖會將它們的圖片請求至于那些并不急需的請求后面,因此網絡阻塞了。
這就是 wheremoveTaskToSlowQueueForURL:這個方法的產生的原因.
// a table cell is going off screen-
(void)tableView:(UITableView *)table
didEndDisplayingCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath*)indexPath {
// we don't need it right now, so move it to the slow queue
[SGImageCache moveTaskToSlowQueueForURL:[[(id)cell event] imageURL]];}
這確保在fastQueue中的只會有真正需要被快速執行的任務。任何以前認為需要快速執行但現在不需要的任務會被移至slowQueue中。
重點和選擇
已經有相當多的iOS圖片緩存庫。它們中一些庫只針對某些應用場景,一些庫提供了不同場景一定的可擴展性。我們的庫即沒有專門針對某些應用場景,也沒有太多大而全的特性。針對我們的用戶我們有三類基本的重點:
重點 1: 最好的幀率
很多的庫都非常專注在這一點上,使用一些高度定制和復雜的方法,盡管基準沒有決定性地顯示這樣有效。我們發現最好的幀率由這些決定:
將對磁盤的訪問(并且幾乎其它的所有)脫離主線程。
使用UIImage的內存緩存來避免不必要的磁盤訪問和圖片解壓。
重點 2: 讓最最重要的圖片優先顯示
大多數的庫都考慮讓隊列管理成為別人關心的事。對于我們的應用,這幾乎是最重要的點。
讓正確的圖片在正確的時間顯示在屏幕上可以歸結為一個簡單的問題:“我們現在就需要它顯示還是過一會兒?”。那些需要立即顯示的圖片是并行加載地,而其它所有東西都被添加到串行隊列中。所有之前急迫的事但現在不急迫的話就會從fastQueue分到slowQueue中。并且當fastQueue在工作時,slowQueue是處于掛起狀態的。
這讓那些急需顯示的圖片可以單獨訪問網絡,同時也確保了一張非急需顯示的圖片可以在過一會成為一張急需顯示的圖片,因為它已經存到了緩存當中,隨時準備用于顯示。
重點 3: 盡可能簡單的API
大多數庫都做到了這一點。許多庫為了隱藏細節內容而提供了UIImageView的分類,并且許多庫讓抓取一張圖片的流程變得盡可能的便利。針對我們經常做的三件事,我們的庫選定了三個主要的方法:
快速抓到一張圖
__weak SGEventCell *me = self;[SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) { me.imageView.image = image;}];
排隊等待一張我們一會才需要的圖片
[SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];
通知緩存一張急需顯示的圖已經不需要立刻顯示
[SGImageCache moveTaskToSlowQueueForURL:event.imageURL];
結論
通過專注于預取,隊列管理,從主線程移除耗時的任務,并且依賴于UIImage內置的內存緩存,我們努力從一個簡單的軟件包中得到好的結果。
更多信息請查看IT技術專欄