天天看點

iOS——Core Animation 知識摘抄(四)

原文位址http://www.cocoachina.com/ios/20150106/10840.html

延遲解壓

一旦圖檔檔案被加載就必須要進行解碼,解碼過程是一個相當複雜的任務,需要消耗非常長的時間。解碼後的圖檔将同樣使用相當大的記憶體。

用于加載的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),你會發現滑動更加平滑。

- (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 == imageView.tag) {
                imageView.image = image;
            }
        });
    });
    return cell;
}      

動畫的舞台

Core Animation處在iOS的核心地位:應用内和應用間都會用到它。一個簡單的動畫可能同步顯示多個app的内容,例如當在iPad上多個程式之間使用手勢切換,會使得多個程式同時顯示在螢幕上。在一個特定的應用中用代碼實作它是沒有意義的,因為在iOS中不可能實作這種效果(App都是被沙箱管理,不能通路别的視圖)。

動畫和螢幕上組合的圖層實際上被一個單獨的程序管理,而不是你的應用程式。這個程序就是所謂的渲染服務。在iOS5和之前的版本是SpringBoard程序(同時管理着iOS的主屏)。在iOS6之後的版本中叫做BackBoard。

當運作一段動畫時候,這個過程會被四個分離的階段被打破:

  • 布局 - 這是準備你的視圖/圖層的層級關系,以及設定圖層屬性(位置,背景色,邊框等等)的階段。
  • 顯示 - 這是圖層的寄宿圖檔被繪制的階段。繪制有可能涉及你的-drawRect:和-drawLayer:inContext:方法的調用路徑。
  • 準備 - 這是Core Animation準備發送動畫資料到渲染服務的階段。這同時也是Core Animation将要執行一些别的事務例如解碼動畫過程中将要顯示的圖檔的時間點。
  • 送出 - 這是最後的階段,Core Animation打包所有圖層和動畫屬性,然後通過IPC(内部處理通信)發送到渲染服務進行顯示。

但是這些僅僅階段僅僅發生在你的應用程式之内,在動畫在螢幕上顯示之前仍然有更多的工作。一旦打包的圖層和動畫到達渲染服務程序,他們會被反序列化來形成另一個叫做渲染樹的圖層樹(在第一章“圖層樹”中提到過)。使用這個樹狀結構,渲染服務對動畫的每一幀做出如下工作:

  • 對所有的圖層屬性計算中間值,設定OpenGL幾何形狀(紋理化的三角形)來執行渲染
  • 在螢幕上渲染可見的三角形

是以一共有六個階段;最後兩個階段在動畫過程中不停地重複。前五個階段都在軟體層面處理(通過CPU),隻有最後一個被GPU執行。而且,你真正隻能控制前兩個階段:布局和顯示。Core Animation架構在内部處理剩下的事務,你也控制不了它。

這并不是個問題,因為在布局和顯示階段,你可以決定哪些由CPU執行,哪些交給GPU去做。那麼改如何判斷呢?

GPU相關的操作

但是有一些事情會降低(基于GPU)圖層繪制,比如:

  • 太多的幾何結構 - 這發生在需要太多的三角闆來做變換,以應對處理器的栅格化的時候。現代iOS裝置的圖形晶片可以處理幾百萬個三角闆,是以在Core Animation中幾何結構并不是GPU的瓶頸所在。但由于圖層在顯示之前通過IPC發送到渲染伺服器的時候(圖層實際上是由很多小物體組成的特别重量級的對象),太多的圖層就會引起CPU的瓶頸。這就限制了一次展示的圖層個數(見本章後續“CPU相關操作”)。
  • 重繪 - 主要由重疊的半透明圖層引起。GPU的填充比率(用顔色填充像素的比率)是有限的,是以需要避免重繪(每一幀用相同的像素填充多次)的發生。在現代iOS裝置上,GPU都會應對重繪;即使是iPhone 3GS都可以處理高達2.5的重繪比率,并任然保持60幀率的渲染(這意味着你可以繪制一個半的整屏的備援資訊,而不影響性能),并且新裝置可以處理更多。
  • 離屏繪制 - 這發生在當不能直接在螢幕上繪制,并且必須繪制到離屏圖檔的上下文中的時候。離屏繪制發生在基于CPU或者是GPU的渲染,或者是為離屏圖檔配置設定額外記憶體,以及切換繪制上下文,這些都會降低GPU性能。對于特定圖層效果的使用,比如圓角,圖層遮罩,陰影或者是圖層光栅化都會強制Core Animation提前渲染圖層的離屏繪制。但這不意味着你需要避免使用這些效果,隻是要明白這會帶來性能的負面影響。
  • 過大的圖檔 - 如果視圖繪制超出GPU支援的2048x2048或者4096x4096尺寸的紋理,就必須要用CPU在圖層每次顯示之前對圖檔預處理,同樣也會降低性能。

CPU相關的操作

大多數工作在Core Animation的CPU都發生在動畫開始之前。這意味着它不會影響到幀率,是以很好,但是他會延遲動畫開始的時間,讓你的界面看起來會比較遲鈍。

以下CPU的操作都會延遲動畫的開始時間:

  • 布局計算 - 如果你的視圖層級過于複雜,當視圖呈現或者修改的時候,計算圖層幀率就會消耗一部分時間。特别是使用iOS6的自動布局機制尤為明顯,它應該是比老版的自動調整邏輯加強了CPU的工作。
  • 視圖懶加載 - iOS隻會當視圖控制器的視圖顯示到螢幕上時才會加載它。這對記憶體使用和程式啟動時間很有好處,但是當呈現到螢幕上之前,按下按鈕導緻的許多工作都會不能被及時響應。比如控制器從資料庫中擷取資料,或者視圖從一個nib檔案中加載,或者涉及IO的圖檔顯示(見後續“IO相關操作”),都會比CPU正常操作慢得多。
  • Core Graphics繪制 - 如果對視圖實作了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那麼在繪制任何東西之前都會産生一個巨大的性能開銷。為了支援對圖層内容的任意繪制,Core Animation必須建立一個記憶體中等大小的寄宿圖檔。然後一旦繪制結束之後,必須把圖檔資料通過IPC傳到渲染伺服器。在此基礎上,Core Graphics繪制就會變得十分緩慢,是以在一個對性能十分挑剔的場景下這樣做十分不好。
  • 解壓圖檔 - PNG或者JPEG壓縮之後的圖檔檔案會比同品質的位圖小得多。但是在圖檔繪制到螢幕上之前,必須把它擴充成完整的未解壓的尺寸(通常等同于圖檔寬 x 長 x 4個位元組)。為了節省記憶體,iOS通常直到真正繪制的時候才去解碼圖檔(14章“圖檔IO”會更詳細讨論)。根據你加載圖檔的方式,第一次對圖層内容指派的時候(直接或者間接使用UIImageView)或者把它繪制到Core Graphics中,都需要對它解壓,這樣的話,對于一個較大的圖檔,都會占用一定的時間。

當圖層被成功打包,發送到渲染伺服器之後,CPU仍然要做如下工作:為了顯示螢幕上的圖層,Core Animation必須對渲染樹種的每個可見圖層通過OpenGL循環轉換成紋理三角闆。由于GPU并不知曉Core Animation圖層的任何結構,是以必須要由CPU做這些事情。這裡CPU涉及的工作和圖層個數成正比,是以如果在你的層級關系中有太多的圖層,就會導緻CPU沒一幀的渲染,即使這些事情不是你的應用程式可控的。