# 加載和潛伏
????繪圖實際消耗的時間通常并不是影響性能的因素。圖片消耗很大一部分內存,而且不太可能把需要顯示的圖片都保留在內存中,所以需要在應用運行的時候周期性地加載和卸載圖片。
????圖片文件加載的速度被CPU和IO(輸入/輸出)同時影響。iOS設備中的閃存已經比傳統硬盤快很多了,但仍然比RAM慢將近200倍左右,這就需要很小心地管理加載,來避免延遲。
????只要有可能,試著在程序生命周期不易察覺的時候來加載圖片,例如啟動,或者在屏幕切換的過程中。按下按鈕和按鈕響應事件之間最大的延遲大概是200ms,這比動畫每一幀切換的16ms小得多。你可以在程序首次啟動的時候加載圖片,但是如果20秒內無法啟動程序的話,iOS檢測計時器就會終止你的應用(而且如果啟動大于2,3秒的話用戶就會抱怨了)。
????有些時候,提前加載所有的東西并不明智。比如說包含上千張圖片的圖片傳送帶:用戶希望能夠能夠平滑快速翻動圖片,所以就不可能提前預加載所有圖片;那樣會消耗太多的時間和內存。
????有時候圖片也需要從遠程網絡連接中下載,這將會比從磁盤加載要消耗更多的時間,甚至可能由于連接問題而加載失敗(在幾秒鐘嘗試之后)。你不能夠在主線程中加載網絡造成等待,所以需要后臺線程。
### 線程加載
????在第12章“性能調優”我們的聯系人列表例子中,圖片都非常小,所以可以在主線程同步加載。但是對于大圖來說,這樣做就不太合適了,因為加載會消耗很長時間,造成滑動的不流暢。滑動動畫會在主線程的run loop中更新,所以會有更多運行在渲染服務進程中CPU相關的性能問題。
????清單14.1顯示了一個通過`UICollectionView`實現的基礎的圖片傳送器。圖片在主線程中`-collectionView:cellForItemAtIndexPath:`方法中同步加載(見圖14.1)。
清單14.1 使用`UICollectionView`實現的圖片傳送器
~~~
#import "ViewController.h"
@interface ViewController()
@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
@end
@implementation ViewController
- (void)viewDidLoad
{
//set up data
self.imagePaths =
[[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds];
imageView.tag = imageTag;
[cell.contentView addSubview:imageView];
}
//set image
NSString *imagePath = self.imagePaths[indexPath.row];
imageView.image = [UIImage imageWithContentsOfFile:imagePath];
return cell;
}
@end
~~~

圖14.1 運行中的圖片傳送器
????傳送器中的圖片尺寸為800x600像素的PNG,對iPhone5來說,1/60秒要加載大概700KB左右的圖片。當傳送器滾動的時候,圖片也在實時加載,于是(預期中的)卡動就發生了。時間分析工具(圖14.2)顯示了很多時間都消耗在了`UIImage`的`+imageWithContentsOfFile:`方法中了。很明顯,圖片加載造成了瓶頸。

圖14.2 時間分析工具展示了CPU瓶頸
????這里提升性能唯一的方式就是在另一個線程中加載圖片。這并不能夠降低實際的加載時間(可能情況會更糟,因為系統可能要消耗CPU時間來處理加載的圖片數據),但是主線程能夠有時間做一些別的事情,比如響應用戶輸入,以及滑動動畫。
????為了在后臺線程加載圖片,我們可以使用GCD或者`NSOperationQueue`創建自定義線程,或者使用`CATiledLayer`。為了從遠程網絡加載圖片,我們可以使用異步的`NSURLConnection`,但是對本地存儲的圖片,并不十分有效。
### GCD和`NSOperationQueue`
????GCD(Grand Central Dispatch)和`NSOperationQueue`很類似,都給我們提供了隊列閉包塊來在線程中按一定順序來執行。`NSOperationQueue`有一個Objecive-C接口(而不是使用GCD的全局C函數),同樣在操作優先級和依賴關系上提供了很好的粒度控制,但是需要更多地設置代碼。
????清單14.2顯示了在低優先級的后臺隊列而不是主線程使用GCD加載圖片的`-collectionView:cellForItemAtIndexPath:`方法,然后當需要加載圖片到視圖的時候切換到主線程,因為在后臺線程訪問視圖會有安全隱患。
????由于視圖在`UICollectionView`會被循環利用,我們加載圖片的時候不能確定是否被不同的索引重新復用。為了避免圖片加載到錯誤的視圖中,我們在加載前把單元格打上索引的標簽,然后在設置圖片的時候檢測標簽是否發生了改變。
清單14.2 使用GCD加載傳送圖片
~~~
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell"
forIndexPath:indexPath];
//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: 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
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image; }
});
});
return cell;
}
~~~
????當運行更新后的版本,性能比之前不用線程的版本好多了,但仍然并不完美(圖14.3)。
????我們可以看到`+imageWithContentsOfFile:`方法并不在CPU時間軌跡的最頂部,所以我們的確修復了延遲加載的問題。問題在于我們假設傳送器的性能瓶頸在于圖片文件的加載,但實際上并不是這樣。加載圖片數據到內存中只是問題的第一部分。

圖14.3 使用后臺線程加載圖片來提升性能
### 延遲解壓
????一旦圖片文件被加載就必須要進行解碼,解碼過程是一個相當復雜的任務,需要消耗非常長的時間。解碼后的圖片將同樣使用相當大的內存。
????用于加載的CPU時間相對于解碼來說根據圖片格式而不同。對于PNG圖片來說,加載會比JPEG更長,因為文件可能更大,但是解碼會相對較快,而且Xcode會把PNG圖片進行解碼優化之后引入工程。JPEG圖片更小,加載更快,但是解壓的步驟要消耗更長的時間,因為JPEG解壓算法比基于zip的PNG算法更加復雜。
????當加載圖片的時候,iOS通常會延遲解壓圖片的時間,直到加載到內存之后。這就會在準備繪制圖片的時候影響性能,因為需要在繪制之前進行解壓(通常是消耗時間的問題所在)。
????最簡單的方法就是使用`UIImage`的`+imageNamed:`方法避免延時加載。不像`+imageWithContentsOfFile:`(和其他別的`UIImage`加載方法),這個方法會在加載圖片之后立刻進行解壓(就和本章之前我們談到的好處一樣)。問題在于`+imageNamed:`只對從應用資源束中的圖片有效,所以對用戶生成的圖片內容或者是下載的圖片就沒法使用了。
????另一種立刻加載圖片的方法就是把它設置成圖層內容,或者是`UIImageView`的`image`屬性。不幸的是,這又需要在主線程執行,所以不會對性能有所提升。
????第三種方式就是繞過`UIKit`,像下面這樣使用ImageIO框架:
~~~
NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);
~~~
????這樣就可以使用`kCGImageSourceShouldCache`來創建圖片,強制圖片立刻解壓,然后在圖片的生命周期保留解壓后的版本。
????最后一種方式就是使用UIKit加載圖片,但是立刻會知道`CGContext`中去。圖片必須要在繪制之前解壓,所以就強制了解壓的及時性。這樣的好處在于繪制圖片可以再后臺線程(例如加載本身)執行,而不會阻塞UI。
????有兩種方式可以為強制解壓提前渲染圖片:
* 將圖片的一個像素繪制成一個像素大小的`CGContext`。這樣仍然會解壓整張圖片,但是繪制本身并沒有消耗任何時間。這樣的好處在于加載的圖片并不會在特定的設備上為繪制做優化,所以可以在任何時間點繪制出來。同樣iOS也就可以丟棄解壓后的圖片來節省內存了。
* 將整張圖片繪制到`CGContext`中,丟棄原始的圖片,并且用一個從上下文內容中新的圖片來代替。這樣比繪制單一像素那樣需要更加復雜的計算,但是因此產生的圖片將會為繪制做優化,而且由于原始壓縮圖片被拋棄了,iOS就不能夠隨時丟棄任何解壓后的圖片來節省內存了。
????需要注意的是蘋果特別推薦了不要使用這些詭計來繞過標準圖片解壓邏輯(所以也是他們選擇用默認處理方式的原因),但是如果你使用很多大圖來構建應用,那如果想提升性能,就只能和系統博弈了。
????如果不使用`+imageNamed:`,那么把整張圖片繪制到`CGContext`可能是最佳的方式了。盡管你可能認為多余的繪制相較別的解壓技術而言性能不是很高,但是新創建的圖片(在特定的設備上做過優化)可能比原始圖片繪制的更快。
????同樣,如果想顯示圖片到比原始尺寸小的容器中,那么一次性在后臺線程重新繪制到正確的尺寸會比每次顯示的時候都做縮放會更有效(盡管在這個例子中我們加載的圖片呈現正確的尺寸,所以不需要多余的優化)。
????????如果修改了`-collectionView:cellForItemAtIndexPath:`方法來重繪圖片(清單14.3),你會發現滑動更加平滑。
清單14.3 強制圖片解壓顯示
~~~
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
?{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
...
//switch to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});
return cell;
}
~~~
### `CATiledLayer`
????如第6章“專用圖層”中的例子所示,`CATiledLayer`可以用來異步加載和顯示大型圖片,而不阻塞用戶輸入。但是我們同樣可以使用`CATiledLayer`在`UICollectionView`中為每個表格創建分離的`CATiledLayer`實例加載傳動器圖片,每個表格僅使用一個圖層。
????這樣使用`CATiledLayer`有幾個潛在的弊端:
* `CATiledLayer`的隊列和緩存算法沒有暴露出來,所以我們只能祈禱它能匹配我們的需求
* `CATiledLayer`需要我們每次重繪圖片到`CGContext`中,即使它已經解壓縮,而且和我們單元格尺寸一樣(因此可以直接用作圖層內容,而不需要重繪)。
????我們來看看這些弊端有沒有造成不同:清單14.4顯示了使用`CATiledLayer`對圖片傳送器的重新實現。
清單14.4 使用`CATiledLayer`的圖片傳送器
~~~
#import "ViewController.h"
#import
@interface ViewController()
@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
@end
@implementation ViewController
- (void)viewDidLoad
{
//set up data
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:@"Vacation Photos"];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add the tiled layer
CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject];
if (!tileLayer) {
tileLayer = [CATiledLayer layer];
tileLayer.frame = cell.bounds;
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
tileLayer.delegate = self;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[cell.contentView.layer addSublayer:tileLayer];
}
//tag the layer with the correct index and reload
tileLayer.contents = nil;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[tileLayer setNeedsDisplay];
return cell;
}
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//get image index
NSInteger index = [[layer valueForKey:@"index"] integerValue];
//load tile image
NSString *imagePath = self.imagePaths[index];
UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
//calculate image rect
CGFloat aspectRatio = tileImage.size.height / tileImage.size.width;
CGRect imageRect = CGRectZero;
imageRect.size.width = layer.bounds.size.width;
imageRect.size.height = layer.bounds.size.height * aspectRatio;
imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2;
//draw tile
UIGraphicsPushContext(ctx);
[tileImage drawInRect:imageRect];
UIGraphicsPopContext();
}
@end
~~~
????需要解釋幾點:
* `CATiledLayer`的`tileSize`屬性單位是像素,而不是點,所以為了保證瓦片和表格尺寸一致,需要乘以屏幕比例因子。
* 在`-drawLayer:inContext:`方法中,我們需要知道圖層屬于哪一個`indexPath`以加載正確的圖片。這里我們利用了`CALayer`的KVC來存儲和檢索任意的值,將圖層和索引打標簽。
????結果`CATiledLayer`工作的很好,性能問題解決了,而且和用GCD實現的代碼量差不多。僅有一個問題在于圖片加載到屏幕上后有一個明顯的淡入(圖14.4)。

圖14.4 加載圖片之后的淡入
????我們可以調整`CATiledLayer`的`fadeDuration`屬性來調整淡入的速度,或者直接將整個漸變移除,但是這并沒有根本性地去除問題:在圖片加載到準備繪制的時候總會有一個延遲,這將會導致滑動時候新圖片的跳入。這并不是`CATiledLayer`的問題,使用GCD的版本也有這個問題。
????即使使用上述我們討論的所有加載圖片和緩存的技術,有時候仍然會發現實時加載大圖還是有問題。就和13章中提到的那樣,iPad上一整個視網膜屏圖片分辨率達到了2048x1536,而且會消耗12MB的RAM(未壓縮)。第三代iPad的硬件并不能支持1/60秒的幀率加載,解壓和顯示這種圖片。即使用后臺線程加載來避免動畫卡頓,仍然解決不了問題。
????我們可以在加載的同時顯示一個占位圖片,但這并沒有根本解決問題,我們可以做到更好。
### 分辨率交換
????視網膜分辨率(根據蘋果市場定義)代表了人的肉眼在正常視角距離能夠分辨的最小像素尺寸。但是這只能應用于靜態像素。當觀察一個移動圖片時,你的眼睛就會對細節不敏感,于是一個低分辨率的圖片和視網膜質量的圖片沒什么區別了。
????如果需要快速加載和顯示移動大圖,簡單的辦法就是欺騙人眼,在移動傳送器的時候顯示一個小圖(或者低分辨率),然后當停止的時候再換成大圖。這意味著我們需要對每張圖片存儲兩份不同分辨率的副本,但是幸運的是,由于需要同時支持Retina和非Retina設備,本來這就是普遍要做到的。
????如果從遠程源或者用戶的相冊加載沒有可用的低分辨率版本圖片,那就可以動態將大圖繪制到較小的`CGContext`,然后存儲到某處以備復用。
????為了做到圖片交換,我們需要利用`UIScrollView`的一些實現`UIScrollViewDelegate`協議的委托方法(和其他類似于`UITableView`和`UICollectionView`基于滾動視圖的控件一樣):
~~~
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
~~~
????你可以使用這幾個方法來檢測傳送器是否停止滾動,然后加載高分辨率的圖片。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,你會很難察覺到替換的過程(確保在同一臺機器使用相同的圖像程序或者腳本生成這些圖片)。
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結