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

lihaiyin發表於2015-04-24

原文地址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沒一幀的渲染,即使這些事情不是你的應用程式可控的。

相關文章