Core Animation(下)─── 性能優(yōu)化

一. 渲染原理和GPU、CPU的作用

我們在前面學到了Core Animation的功能(繪圖和動畫),接下來要學習關于性能相關的知識。在Core Animation性能優(yōu)化方面,首先要了解CPU、GPU、IO相關的操作。
關于繪圖和動畫有兩種處理方式:CPU(中央處理器)和GPU(圖形處理器)。我們可以說CPU所做的工作都在軟件層面,而GPU在硬件層面。我們可以用軟件(使用GPU)做任何事情,但是對于圖像處理,通常用硬件會更快,因為GPU使用圖像對高度并行浮點預算做了優(yōu)化。
動畫和屏幕上組合的圖層實際上被一個單獨的進程管理,而不是你的應用程序,這個進程就是所謂的渲染服務
當運行一段動畫時,分成以下階段:

  • 布局
    這是準備你的視圖/圖層的層級關系,以及設置圖層的屬性(位置,背景色,邊框)的階段
  • 顯示
    這是圖層的寄宿圖片被繪制的階段。繪制可能會涉及到你的-drawRect:-drawLayer:inContext:方法的調(diào)用路徑。
  • 準備
    這是Core Animation準備發(fā)送動畫數(shù)據(jù)到渲染服務的階段。這同時也是
    Core Animation將要執(zhí)行一些別的事務例如解碼動畫過程中將要顯示的圖片的時間點。
  • 提交
    這是最后的階段,Core Animation打包所有的圖層和動畫屬性,通過IPC(內(nèi)部處理通訊)發(fā)送到渲染服務進行顯示。

不過這些階段僅僅發(fā)生在你的應用程序中,一旦打包的圖層和動畫到達渲染服務進程,它們會被反序列化來形成一個叫渲染樹的圖層樹。使用這個樹狀結(jié)構,渲染服務對動畫的每一幀做出如下動作:

  • 對所有的圖層屬性計算中間值,設置OpenGL幾何形狀來執(zhí)行渲染
  • 在屏幕上渲染可見的三角形

所以一共六個階段,最后兩個階段在動畫過程中不斷重復。前五個階段都在軟件層面處理(通過CPU),只有最后一個被GPU執(zhí)行。而我們真正能控制的就前兩個階段:布局和顯示。

那在布局和顯示階段,哪些是影響CPU或者GPU的因素呢?

CPU相關的操作

CPU大多數(shù)工作都在動畫開始之前,所以它不會影響到幀率。但是它會影響到動畫開始的時間,造成界面的遲鈍。以下CPU操作會延遲動畫開始時間:

  • 布局計算
  • 視圖懶加載
  • Core Graphics繪制
  • 解壓圖片
  • 太多的圖層
GPU相關的操作

GPU用來采集圖片和形狀(三角形),運行變換,應用紋理和混合然后把它們輸送到屏幕上。大多數(shù)CALayer的屬性都是GPU來繪制的。

  • 太多的幾何結(jié)構
  • 重繪
  • 離屏繪制
  • 過大的圖片

還有一項影響性能的就是IO相關的操作。IO(輸入/輸出)指的是例如從閃存或者網(wǎng)絡接口的硬件訪問。IO比內(nèi)存訪問更慢,如果動畫設計到IO,就需要使用一些優(yōu)化(比如多線程、緩存、提前加載)

如果知道了哪些點會影響性能,也要進行正確的測試。通過測試才能合理地優(yōu)化。測試時也應該要使用真機測試,并且使用發(fā)布配置,最好能在支持設備中最差的的設備上測試。我們可以通過Instruments工具集來測試。我們主要用到這幾個:

  • 時間分析器(用來測量被方法/函數(shù)打斷的CPU使用情況)
  • Core Animation(用來調(diào)試各種Core Animation性能問題)
  • OpenGL ES驅(qū)動(用來調(diào)試GPU性能問題)

二. 性能陷阱和優(yōu)化

前面我們學習了Core Animation如何渲染,以及GPU、CPU的作用。接下來要具體分析具體的性能陷阱和優(yōu)化技巧。
我們這邊主要分成三大塊:
① 繪圖相關的優(yōu)化
② IO操作相關的優(yōu)化
③ 圖層相關的優(yōu)化

① 繪圖相關的優(yōu)化

在iOS中繪圖通常由Core Graphics框架完成,在一些情況下相比Core
Animation 和OpenGL要慢不少。繪圖不僅效率低,還有消耗可觀的內(nèi)存。CALayer只需要一些與自己相關的內(nèi)存,只有它的寄宿圖會消耗一定的內(nèi)存空間,即使直接賦值給contens一張圖片,也不會消耗額外的圖片存儲大小。但是,一旦實現(xiàn)了CALayerDelegate協(xié)議中的-drawLayer:inContext:(或者UIView的- drawRect:,該方法是前者的包裝方法),圖層就得創(chuàng)建一個繪制上下文,內(nèi)存大小為 圖層高 x 圖層寬 x 4字節(jié),寬高單位均為像素。對于一個Retina的全屏圖層,所需的內(nèi)存都要幾M以上,圖層每次重繪時都需要抹掉內(nèi)存重新分配。

所以,有以下幾種優(yōu)化方案:
(1) 除了一些特殊情況,可以使用Core Animation的為圖形類型的繪制提供的類來代替。比如,CAShapeLayer可以繪制多邊形,直線,曲線。CATextLayer可以繪制文本。CAGradientLayer用來繪制漸變。這些總體都比Core Graphics快,同時它們也避免創(chuàng)建一個寄宿圖。
(2) 只重繪制臟矩形
為了減少不必要的繪制,iOS將屏幕分為需要重繪的區(qū)域和不需要重繪的區(qū)域。那些需要重繪的部分被稱為“臟矩形”,當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接調(diào)用-setNeedsDisplayInRect:來標記它,然后將影響的矩形作為參數(shù)傳入。這樣視圖刷新時就會去刷新該區(qū)域。
(3) 異步繪制
可以提前在另外一個線程繪制內(nèi)容,然后將由此繪制出的圖片直接設置為圖層的內(nèi)容。Core Animation提供了兩種選擇:CATiledLayer和drawsAsynchronously屬性。

② IO操作相關的優(yōu)化

圖片的加載和解壓也是一個很影響性能的因素。
圖片文件加載的速度被CPU和IO(輸入/輸出)同時影響,iOS設備中的閃存雖然比硬盤快很多,但是仍然比RAM慢很多,所以就需要管理加載,來避免延遲。
可以使用多線程加載,開辟一個新線程加載圖片,在主線程賦值給imageView:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
          //加載圖片
          NSInteger index = indexPath.row;
          NSString *imagePath = self.imagePaths[index];
          UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
          //在主線程賦值給imageView
          dispatch_async(dispatch_get_main_queue(), ^{
              if (index == cell.tag) {
                  imageView.image = image;
          }
});

實際上,這樣的優(yōu)化并不完美,因為還有一個影響性能的問題----解壓。一旦圖片文件被加載就必須要進行解碼,解碼過程是一個相當復雜的任務,需要消耗非常長的時間。解碼后的圖片將同樣使用相當大的內(nèi)存。圖片格式不同,用于加載和解碼的時間也不同。PNG圖片文件更大,所以加載會比JPEG更長,但是解碼會相對較快,因為解碼算法比JPEG簡單。
當加載圖片的時候,iOS通常會延遲解壓圖片的時間,直到加載到內(nèi)存中(意思是直到把圖片設置成圖層內(nèi)容,或是賦值給UIImageView才去解壓)。所以就需要用以下方法來避免延遲解壓:
(1)最簡單的就是使用UIImage的+imageNamed:方法避免延遲解壓。不同于UIImage的其他加載方法,這個方法會在加載圖片后立刻進行解壓。
(2)使用ImageIO框架

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);

(3)使用UIKit加載圖片后,立刻繪制到CGContext中。因為圖片必須要在繪制之前解壓,另外也可以將繪制和加載放在多線程中。如果要想顯示圖片到比原始尺寸小的容器中,那么一次性在后臺線程重新繪制到正確的尺寸會比每次顯示的時候做縮放更快。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    //加載圖片
    NSInteger index = indexPath.row;
    NSString *imagePath = self.imagePaths[index];
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

    //將圖片繪制到上下文
    UIGraphicsBeginImageContextWithOptions(imageView.bounds, YES, 0);
    image drawInRect:imageView.bounds];
    image = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext();

    //在主線程設置圖片
    dispatch_async(dispatch_get_main_queue(), ^{
        if (index == cell.tag) {
            imageView.image = image;
        } });
});

即使使用了上述加載圖片和解壓的技術,有時候仍然會發(fā)現(xiàn)實時加載大圖還是有問題。除了在加載中同時顯示一個占位圖片,我們還可以使用如下方案:

(1)大小圖切換
如果需要快速加載和顯示移動大圖,可以在移動時顯示一個小圖(或低分辨率的圖),然后當停止時再換成大圖。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,肉眼很難察覺替換的過程。
如果沒有低分辨率版本的圖片,可以自己生成,動態(tài)將大圖繪制到較小的CGContext,然后存儲到某處以備復用。以下兩個方法是用來判斷是否滾動的:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

(2)緩存
緩存就是存儲昂貴計算后的結(jié)果(或者是從閃存或者網(wǎng)絡加載的問價)在內(nèi)存中,以便后續(xù)使用,這樣訪問起來就很快。之前提到的使用[UIImage imageNamed:] 加載圖片除了可以立刻解壓圖片而不用等到繪制的時候,也有另一個優(yōu)點:它在內(nèi)存中自動緩存了解壓后的圖片,即使你還沒用到它。但是[UIImage imageNamed:]并不適用任何情況,也有如下幾個局限性:
(1)僅僅適用于在應用程序資源目錄下的圖片。
(2)只適用于按鈕、背景這種圖片,對于照片這種大圖,系統(tǒng)可能會移除這些圖片來節(jié)省內(nèi)存。
(3)緩存機制不是公開的,不能很好控制它,不如設置緩存大小,從緩存移除沒用的圖片。
所以,就需要我們?nèi)プ远x緩存,而iOS中就可以使用NSCache。

- (UIImage *)loadImageAtIndex:(NSUInteger)index{
    //set up cache
    static NSCache *cache = nil;
    if (!cache) {
        cache = [[NSCache alloc] init];
    }
    //if already cached, return immediately
    UIImage *image = [cache objectForKey:@(index)];
    if (image) {
        return [image isKindOfClass:[NSNull class]]? nil: image;
    }
    //set placeholder to avoid reloading image multiple times
    [cache setObject:[NSNull null] forKey:@(index)];
    //switch to background thread
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        //load image
        NSString *imagePath = self.imagePaths[index];
        UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
        //redraw image using device context
        UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
        [image drawAtPoint:CGPointZero];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        //set image for correct image view
        dispatch_async(dispatch_get_main_queue(), ^{ //cache the image
            [cache setObject:image forKey:@(index)];
            //display the image
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem: index inSection:0]; UICollectionV UIImageView *imageView = [cell.contentView.subviews lastObject];
            imageView.image = image;
        }); });
    //not loaded yet
    return nil;
    
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
    //dequeue cell
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell"];
    UIImageView *imageView = [cell.contentView.subviews lastObject];
    if (!imageView) {
        imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [cell.contentView addSubview:imageView];
    }
    //set or load image for this index
    imageView.image = [self loadImageAtIndex:indexPath.item];
    //preload image for previous and next index
    if (indexPath.item < [self.imagePaths count] - 1) {
        [self loadImageAtIndex:indexPath.item + 1]; }
    if (indexPath.item > 0) {
        [self loadImageAtIndex:indexPath.item - 1];
    }
    return cell;
}

③ 圖層相關的優(yōu)化

只有注重圖層樹本身,才能挖掘更好的性能。

減少重繪

寄宿圖可以通過Core Graphics直接繪制,也可以直接載入一個圖片文件并賦值給contents屬性,或事先繪制一個屏幕外的CGContext上下文。除了圖形外,CATextLayer和UILabel都是直接將文本繪制在圖層的寄宿圖中,雖然渲染方式不一樣(UILabel用WebKit的HTML渲染引擎來繪制文本,CATextLayer用的是CoreText,后者更迅速,所以繪制大量文本有限使用CATextLayer),但都是用軟件的繪制方式,實際上比硬件加速合成的方式要慢。
不論如何,都要盡量避免重繪。比如盡量避免改變那些包含文本的視圖的frame,因為這樣文本就需要重繪。如果該圖層經(jīng)常改動,可以把這靜態(tài)的文本放在一個子圖層中。

合理利用光柵化

啟用shouldRasterize屬性會將圖層繪制到一個屏幕之外的圖像,然后這個圖像就會被緩存起來并繪制到實際圖層的contens和子圖層(通俗講就是將所有圖層合成起來)。如果有很多子圖層或者復雜的效果應用,這樣會比重繪所有事務的所有幀效果好,但是光柵化原始圖像需要時間,還會消耗額外的內(nèi)存。當我們使用得當,光柵化可以提供很大的性能優(yōu)勢,但是要避免作用在不斷變化的圖層上。

減少離屏渲染

當圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制時,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制,但它意味著圖層
必須在顯示之前在一個屏幕外上下文中被渲染(無論CPU還是GPU)。圖層的以下屬性會造成離屏渲染:

  • 圓角(和maskToBounds一起使用時)
  • 圖層蒙版(mask)
  • 陰影

屏幕外渲染和啟用光柵化相似,除了它沒有像光柵化圖層消耗大,子圖層也并沒有被影響,結(jié)果也沒有被緩存,所以不會有長期的內(nèi)存占用。但是如果太多圖層在屏幕外渲染會影響到性能。
所以,有時候我們可以把那些需要屏幕外繪制的圖層開啟光柵化作為一個優(yōu)化方式(前提是這些圖層不會被頻繁重繪)。對于那些需要動畫而且要在屏幕外渲染的圖層,我們可以使用CAShapeLayer(設置圓角),contentsCenter(創(chuàng)建一個可伸縮圖片,可以繪制成任意邊框效果而不需要額外的性能損耗)或者shadowPath來減少對性能的影響。

//contentsCenter的例子
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0); blueLayer.contentsScale = [UIScreen mainScreen].scale;
blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage; 
[self.layerView.layer addSublayer:blueLayer];
避免混合和過度繪制

GPU每一幀可以繪制的像素有個最大限制,如果由于重復圖層的關系需要不斷重繪同一區(qū)域,可能會超出限制,造成掉幀。GPU會放棄繪制那些完全被其他圖層遮蓋的像素,但是計算一個圖層是否被遮擋也是相當復雜并且消耗處理器資源的。同樣,合并不同圖層的透明重疊像素(混合)也是十分消耗資源的。所以為加快處理進程,不到必要時不要使用透明圖層。所以,我們要給視圖的backgroundColor屬性設置一個不透明的顏色,并設置opaque屬性為YES。這樣就會避免過度繪制,因為Core Animation可以舍棄所有被完全遮擋住d額圖層,不用每個像素都去計算一遍。
另外,合理使用shouldRasterize屬性,將一個固定的圖層體系合成單張照片,也會避免子圖層的混合和過度繪制的性能問題。

減少圖層數(shù)量

初始化圖層,處理圖層,打包通過IPC發(fā)送給渲染引擎,轉(zhuǎn)成OpenGL幾何圖形,這是一個圖層大致的資源開銷。事實上,一次能夠在屏幕上顯示的最大圖層數(shù)量也是有限的。(一般幾百上千個,取決于設備,圖層內(nèi)容等)

合理利用Core Graphics繪制

在圖層數(shù)量影響性能的情況下,軟件繪制很可能會提高性能,因為它避免了圖層分配和操作問題(比如一個多個UILabel和UIImageView的復合視圖,可以替換成一個單獨的視圖,用-drawRect:繪制出來)。不過這樣繪制雖然快,但是使用UIView實例更為簡單,快捷。
使用CALayer的-renderInContext:方法,可以將圖層及其子圖層快照進一個Core Graphics上下文然后得到一張圖片。不同于shouldRasterize,這個方法沒有持續(xù)的性能消耗,相對于讓Core Animation處理一個復雜的圖層樹,可以節(jié)省可觀的性能。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內(nèi)容