iOS核心動畫高級技巧 - 7

来源:https://www.cnblogs.com/Julday/archive/2019/12/10/12016107.html
-Advertisement-
Play Games

13. 高效繪圖 高效繪圖 不必要的效率考慮往往是性能問題的萬惡之源。 ——William Allan Wulf 在第12章『速度的曲率』我們學習如何用Instruments來診斷Core Animation性能問題。在構建一個iOS app的時候會遇到很多潛在的性能陷阱,但是在本章我們將著眼於有關 ...


13. 高效繪圖

高效繪圖

不必要的效率考慮往往是性能問題的萬惡之源。 ——William Allan Wulf

在第12章『速度的曲率』我們學習如何用Instruments來診斷Core Animation性能問題。在構建一個iOS app的時候會遇到很多潛在的性能陷阱,但是在本章我們將著眼於有關繪製的性能問題。

13.1 軟體繪圖

軟體繪圖

術語繪圖通常在Core Animation的上下文中指代軟體繪圖(意即:不由GPU協助的繪圖)。在iOS中,軟體繪圖通常是由Core Graphics框架完成來完成。但是,在一些必要的情況下,相比Core Animation和OpenGL,Core Graphics要慢了不少。

軟體繪圖不僅效率低,還會消耗可觀的記憶體。CALayer只需要一些與自己相關的記憶體:只有它的寄宿圖會消耗一定的記憶體空間。即使直接賦給contents屬性一張圖片,也不需要增加額外的照片存儲大小。如果相同的一張圖片被多個圖層作為contents屬性,那麼他們將會共用同一塊記憶體,而不是複製記憶體塊。

一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿裡面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。

但是一旦你實現了CALayerDelegate協議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實就是前者的包裝方法),圖層就創建了一個繪製上下文,這個上下文需要的大小的記憶體可從這個算式得出:圖層寬圖層高4位元組,寬高的單位均為像素。對於一個在Retina iPad上的全屏圖層來說,這個記憶體量就是 2048 15264位元組,相當於12MB記憶體,圖層每次重繪的時候都需要重新抹掉記憶體然後重新分配。

軟體繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖。提高繪製性能的秘訣就在於儘量避免去繪製。

13.2 矢量圖形

矢量圖形

我們用Core Graphics來繪圖的一個通常原因就是只是用圖片或是圖層效果不能輕易地繪製出矢量圖形。矢量繪圖包含一下這些:

  • 任意多邊形(不僅僅是一個矩形)

  • 斜線或曲線

  • 文本

  • 漸變

舉個例子,清單13.1 展示了一個基本的畫線應用。這個應用將用戶的觸摸手勢轉換成一個UIBezierPath上的點,然後繪製成視圖。我們在一個UIView子類DrawingView中實現了所有的繪製邏輯,這個情況下我們沒有用上view controller。但是如果你喜歡你可以在view controller中實現觸摸事件處理。圖13.1是代碼運行結果。

清單13.1 用Core Graphics實現一個簡單的繪圖應用

#import "DrawingView.h"

@interface DrawingView ()

@property (nonatomic, strong) UIBezierPath *path;

@end

@implementation DrawingView

- (void)awakeFromNib
{
    //create a mutable path
    self.path = [[UIBezierPath alloc] init];
    self.path.lineJoinStyle = kCGLineJoinRound;
    self.path.lineCapStyle = kCGLineCapRound;
    
    self.path.lineWidth = 5;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the starting point
    CGPoint point = [[touches anyObject] locationInView:self];

    //move the path drawing cursor to the starting point
    [self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the current point
    CGPoint point = [[touches anyObject] locationInView:self];

    //add a new line segment to our path
    [self.path addLineToPoint:point];

    //redraw the view
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    //draw path
    [[UIColor clearColor] setFill];
    [[UIColor redColor] setStroke];
    [self.path stroke];
}
@end

 

13.3 臟矩形

臟矩形

有時候用CAShapeLayer或者其他矢量圖形圖層替代Core Graphics並不是那麼切實可行。比如我們的繪圖應用:我們用線條完美地完成了矢量繪製。但是設想一下如果我們能進一步提高應用的性能,讓它就像一個黑板一樣工作,然後用『粉筆』來繪製線條。模擬粉筆最簡單的方法就是用一個『線刷』圖片然後將它粘貼到用戶手指碰觸的地方,但是這個方法用CAShapeLayer沒辦法實現。

我們可以給每個『線刷』創建一個獨立的圖層,但是實現起來有很大的問題。屏幕上允許同時出現圖層上線數量大約是幾百,那樣我們很快就會超出的。這種情況下我們沒什麼辦法,就用Core Graphics吧(除非你想用OpenGL做一些更複雜的事情)。

我們的『黑板』應用的最初實現見清單13.3,我們更改了之前版本的DrawingView,用一個畫刷位置的數組代替UIBezierPath。圖13.2是運行結果

清單13.3 簡單的類似黑板的應用

#import "DrawingView.h"
#import 
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
    //create array
    self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the starting point
    CGPoint point = [[touches anyObject] locationInView:self];

    //add brush stroke
    [self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the touch point
    CGPoint point = [[touches anyObject] locationInView:self];

    //add brush stroke
    [self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
    //add brush stroke to array
    [self.strokes addObject:[NSValue valueWithCGPoint:point]];

    //needs redraw
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    //redraw strokes
    for (NSValue *value in self.strokes) {
        //get point
        CGPoint point = [value CGPointValue];

        //get brush rect
        CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

        //draw brush stroke    
        [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
    }
}
@end

 

圖13.3 幀率和線條質量會隨時間下降。

為了減少不必要的繪製,Mac OS和iOS設備將會把屏幕區分為需要重繪的區域和不需要重繪的區域。那些需要重繪的部分被稱作『臟區域』。在實際應用中,鑒於非矩形區域邊界裁剪和混合的複雜性,通常會區分出包含指定視圖的矩形位置,而這個位置就是『臟矩形』。

當一個視圖被改動過了,TA可能需要重繪。但是很多情況下,只是這個視圖的一部分被改變了,所以重繪整個寄宿圖就太浪費了。但是Core Animation通常並不瞭解你的自定義繪圖代碼,它也不能自己計算出臟區域的位置。然而,你的確可以提供這些信息。

當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接調用-setNeedsDisplayInRect:來標記它,然後將影響到的矩形作為參數傳入。這樣就會在一次視圖刷新時調用視圖的-drawRect:(或圖層代理的-drawLayer:inContext:方法)。

傳入-drawLayer:inContext:CGContext參數會自動被裁切以適應對應的矩形。為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法來從上下文獲得大小。調用-drawRect()會更簡單,因為CGRect會作為參數直接傳入。

你應該將你的繪製工作限制在這個矩形中。任何在此區域之外的繪製都將被自動無視,但是這樣CPU花在計算和拋棄上的時間就浪費了,實在是太不值得了。

相比依賴於Core Graphics為你重繪,裁剪出自己的繪製區域可能會讓你避免不必要的操作。那就是說,如果你的裁剪邏輯相當複雜,那還是讓Core Graphics來代勞吧,記住:當你能高效完成的時候才這樣做。

清單13.4 展示了一個-addBrushStrokeAtPoint:方法的升級版,它只重繪當前線刷的附近區域。另外也會刷新之前線刷的附近區域,我們也可以用CGRectIntersectsRect()來避免重繪任何舊的線刷以不至於覆蓋已更新過的區域。這樣做會顯著地提高繪製效率(見圖13.4)

清單13.4 用-setNeedsDisplayInRect:來減少不必要的繪製

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
    //add brush stroke to array
    [self.strokes addObject:[NSValue valueWithCGPoint:point]];

    //set dirty rect
    [self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
    return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
    //redraw strokes
    for (NSValue *value in self.strokes) {
        //get point
        CGPoint point = [value CGPointValue];

        //get brush rect
        CGRect brushRect = [self brushRectForPoint:point];
        
        //only draw brush stroke if it intersects dirty rect
        if (CGRectIntersectsRect(rect, brushRect)) {
            //draw brush stroke
            [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
        }
    }
}

 

13.4 非同步繪製

非同步繪製

UIKit的單線程天性意味著寄宿圖通暢要在主線程上更新,這意味著繪製會打斷用戶交互,甚至讓整個app看起來處於無響應狀態。我們對此無能為力,但是如果能避免用戶等待繪製完成就好多了。

針對這個問題,有一些方法可以用到:一些情況下,我們可以推測性地提前在另外一個線程上繪製內容,然後將由此繪出的圖片直接設置為圖層的內容。這實現起來可能不是很方便,但是在特定情況下是可行的。Core Animation提供了一些選擇:CATiledLayerdrawsAsynchronously屬性。

CATiledLayer

我們在第六章簡單探索了一下CATiledLayer。除了將圖層再次分割成獨立更新的小塊(類似於臟矩形自動更新的概念),CATiledLayer還有一個有趣的特性:在多個線程中為每個小塊同時調用-drawLayer:inContext:方法。這就避免了阻塞用戶交互而且能夠利用多核心新片來更快地繪製。只有一個小塊的CATiledLayer是實現非同步更新圖片視圖的簡單方法。

drawsAsynchronously

iOS 6中,蘋果為CALayer引入了這個令人好奇的屬性,drawsAsynchronously屬性對傳入-drawLayer:inContext:的CGContext進行改動,允許CGContext延緩繪製命令的執行以至於不阻塞用戶交互。

它與CATiledLayer使用的非同步繪製並不相同。它自己的-drawLayer:inContext:方法只會在主線程調用,但是CGContext並不等待每個繪製命令的結束。相反地,它會將命令加入隊列,當方法返回時,在後臺線程逐個執行真正的繪製。

根據蘋果的說法。這個特性在需要頻繁重繪的視圖上效果最好(比如我們的繪圖應用,或者諸如UITableViewCell之類的),對那些只繪製一次或很少重繪的圖層內容來說沒什麼太大的幫助。

13.5 總結

總結

本章我們主要圍繞用Core Graphics軟體繪製討論了一些性能挑戰,然後探索了一些改進方法:比如提高繪製性能或者減少需要繪製的數量。第14章,『圖像IO』,我們將討論圖片的載入性能。

14. 圖像IO

圖像IO

潛伏期值得思考 - 凱文 帕薩特

在第13章“高效繪圖”中,我們研究了和Core Graphics繪圖相關的性能問題,以及如何修複。和繪圖性能相關緊密相關的是圖像性能。在這一章中,我們將研究如何優化從快閃記憶體驅動器或者網路中載入和顯示圖片。

14.1 載入和潛伏

載入和潛伏

繪圖實際消耗的時間通常並不是影響性能的因素。圖片消耗很大一部分記憶體,而且不太可能把需要顯示的圖片都保留在記憶體中,所以需要在應用運行的時候周期性地載入和卸載圖片。

圖片文件載入的速度被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.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.2 緩存

緩存

如果有很多張圖片要顯示,最好不要提前把所有都載入進來,而是應該當移出屏幕之後立刻銷毀。通過選擇性的緩存,你就可以避免來回滾動時圖片重覆性的載入了。

緩存其實很簡單:就是存儲昂貴計算後的結果(或者是從快閃記憶體或者網路載入的文件)在記憶體中,以便後續使用,這樣訪問起來很快。問題在於緩存本質上是一個權衡過程 - 為了提升性能而消耗了記憶體,但是由於記憶體是一個非常寶貴的資源,所以不能把所有東西都做緩存。

何時將何物做緩存(做多久)並不總是很明顯。幸運的是,大多情況下,iOS都為我們做好了圖片的緩存。

+imageNamed:方法

之前我們提到使用[UIImage imageNamed:]載入圖片有個好處在於可以立刻解壓圖片而不用等到繪製的時候。但是[UIImage imageNamed:]方法有另一個非常顯著的好處:它在記憶體中自動緩存瞭解壓後的圖片,即使你自己沒有保留對它的任何引用。

對於iOS應用那些主要的圖片(例如圖標,按鈕和背景圖片),使用[UIImage imageNamed:]載入圖片是最簡單最有效的方式。在nib文件中引用的圖片同樣也是這個機制,所以你很多時候都在隱式的使用它。

但是[UIImage imageNamed:]並不適用任何情況。它為用戶界面做了優化,但是並不是對應用程式需要顯示的所有類型的圖片都適用。有些時候你還是要實現自己的緩存機制,原因如下:

  • [UIImage imageNamed:]方法僅僅適用於在應用程式資源束目錄下的圖片,但是大多數應用的許多圖片都要從網路或者是用戶的相機中獲取,所以[UIImage imageNamed:]就沒法用了。

  • [UIImage imageNamed:]緩存用來存儲應用界面的圖片(按鈕,背景等等)。如果對照片這種大圖也用這種緩存,那麼iOS系統就很可能會移除這些圖片來節省記憶體。那麼在切換頁面時性能就會下降,因為這些圖片都需要重新載入。對傳送器的圖片使用一個單獨的緩存機制就可以把它和應用圖片的生命周期解耦。

  • [UIImage imageNamed:]緩存機制並不是公開的,所以你不能很好地控制它。例如,你沒法做到檢測圖片是否在載入之前就做了緩存,不能夠設置緩存大小,當圖片沒用的時候也不能把它從緩存中移除。

自定義緩存

構建一個所謂的緩存系統非常困難。菲爾 卡爾頓曾經說過:“在電腦科學中只有兩件難事:緩存和命名”。

如果要寫自己的圖片緩存的話,那該如何實現呢?讓我們來看看要涉及哪些方面:

  • 選擇一個合適的緩存鍵 - 緩存鍵用來做圖片的唯一標識。如果實時創建圖片,通常不太好生成一個字元串來區分別的圖片。在我們的圖片傳送帶例子中就很簡單,我們可以用圖片的文件名或者表格索引。

  • 提前緩存 - 如果生成和載入數據的代價很大,你可能想當第一次需要用到的時候再去載入和緩存。提前載入的邏輯是應用內在就有的,但是在我們的例子中,這也非常好實現,因為對於一個給定的位置和滾動方向,我們就可以精確地判斷出哪一張圖片將會出現。

  • 緩存失效 - 如果圖片文件發生了變化,怎樣才能通知到緩存更新呢?這是個非常困難的問題(就像菲爾 卡爾頓提到的),但是幸運的是當從程式資源載入靜態圖片的時候並不需要考慮這些。對用戶提供的圖片來說(可能會被修改或者覆蓋),一個比較好的方式就是當圖片緩存的時候打上一個時間戳以便當文件更新的時候作比較。

  • 緩存回收 - 當記憶體不夠的時候,如何判斷哪些緩存需要清空呢?這就需要到你寫一個合適的演算法了。幸運的是,對緩存回收的問題,蘋果提供了一個叫做NSCache通用的解決方案

NSCache

NSCacheNSDictionary類似。你可以通過-setObject:forKey:-object:forKey:方法分別來插入,檢索。和字典不同的是,NSCache在系統低記憶體的時候自動丟棄存儲的對象。

NSCache用來判斷何時丟棄對象的演算法並沒有在文檔中給出,但是你可以使用-setCountLimit:方法設置緩存大小,以及-setObject:forKey:cost:來對每個存儲的對象指定消耗的值來提供一些暗示。

指定消耗數值可以用來指定相對的重建成本。如果對大圖指定一個大的消耗值,那麼緩存就知道這些物體的存儲更加昂貴,於是當有大的性能問題的時候才會丟棄這些物體。你也可以用-setTotalCostLimit:方法來指定全體緩存的尺寸。

NSCache是一個普遍的緩存解決方案,我們創建一個比傳送器案例更好的自定義的緩存類。(例如,我們可以基於不同的緩存圖片索引和當前中間索引來判斷哪些圖片需要首先被釋放)。但是NSCache對我們當前的緩存需求來說已經足夠了;沒必要過早做優化。

使用圖片緩存和提前載入的實現來擴展之前的傳送器案例,然後來看看是否效果更好(見清單14.5)。

清單14.5 添加緩存

#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];
}

- (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]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
            UIImageView *imageView = [cell.contentView.subviews lastObject];
            imageView.image = image;
        });
    });
    //not loaded yet
    return nil;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //dequeue cell
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    //add image view
    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;
}

@end

 

果然效果更好了!當滾動的時候雖然還有一些圖片進入的延遲,但是已經非常罕見了。緩存意味著我們做了更少的載入。這裡提前載入邏輯非常粗暴,其實可以把滑動速度和方向也考慮進來,但這已經比之前沒做緩存的版本好很多了。

14.3 文件格式

文件格式

圖片載入性能取決於載入大圖的時間和解壓小圖時間的權衡。很多蘋果的文檔都說PNG是iOS所有圖片載入的最好格式。但這是極度誤導的過時信息了。

PNG圖片使用的無損壓縮演算法可以比使用JPEG的圖片做到更快地解壓,但是由於快閃記憶體訪問的原因,這些載入的時間並沒有什麼區別。

清單14.6展示了標準的應用程式載入不同尺寸圖片所需要時間的一些代碼。為了保證實驗的準確性,我們會測量每張圖片的載入和繪製時間來確保考慮到解壓性能的因素。另外每隔一秒重覆載入和繪製圖片,這樣就可以取到平均時間,使得結果更加準確。

清單14.6

#import "ViewController.h"

static NSString *const ImageFolder = @"Coast Photos";

@interface ViewController () 

@property (nonatomic, copy) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up image names
    self.items = @[@"2048x1536", @"1024x768", @"512x384", @"256x192", @"128x96", @"64x48", @"32x24"];
}

- (CFTimeInterval)loadImageForOneSec:(NSString *)path
{
    //create drawing context to use for decompression
    UIGraphicsBeginImageContext(CGSizeMake(1, 1));
    //start timing
    NSInteger imagesLoaded = 0;
    CFTimeInterval endTime = 0;
    CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
    while (endTime - startTime < 1) {
        //load image
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        //decompress image by drawing it
        [image drawAtPoint:CGPointZero];
        //update totals
        imagesLoaded ++;
        endTime = CFAbsoluteTimeGetCurrent();
    }
    //close context
    UIGraphicsEndImageContext();
    //calculate time per image
    return (endTime - startTime) / imagesLoaded;
}

- (void)loadImageAtIndex:(NSUInteger)index
{
    //load on background thread so as not to
    //prevent the UI from updating between runs dispatch_async(
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        //setup
        NSString *fileName = self.items[index];
        NSString *pngPath = [[NSBundle mainBundle] pathForResource:filename
                                                            ofType:@"png"
                                                       inDirectory:ImageFolder];
        NSString *jpgPath = [[NSBundle mainBundle] pathForResource:filename
                                                            ofType:@"jpg"
                                                       inDirectory:ImageFolder];
        //load
        NSInteger pngTime = [self loadImageForOneSec:pngPath] * 1000;
        NSInteger jpgTime = [self loadImageForOneSec:jpgPath] * 1000;
        //updated UI on main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            //find table cell and update
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.detailTextLabel.text = [NSString stringWithFormat:@"PNG: %03ims JPG: %03ims", pngTime, jpgTime];
        });
    });
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //dequeue cell
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier:@"Cell"];
    }
    //set up cell
    NSString *imageName = self.items[indexPath.row];
    cell.textLabel.text = imageName;
    cell.detailTextLabel.text = @"Loading...";
    //load image
    [self loadImageAtIndex:indexPath.row];
    return cell;
}

@end

 

PNG和JPEG壓縮演算法作用於兩種不同的圖片類型:JPEG對於噪點大的圖片效果很好;但是PNG更適合於扁平顏色,鋒利的線條或者一些漸變色的圖片。為了讓測評的基準更加公平,我們用一些不同的圖片來做實驗:一張照片和一張彩虹色的漸變。JPEG版本的圖片都用預設的Photoshop60%“高質量”設置編碼。結果見圖片14.5。

14.4 總結

總結

在這章中,我們研究了和圖片載入解壓相關的性能問題,並延展了一系列解決方案。

在第15章“圖層性能”中,我們將討論和圖層渲染和組合相關的性能問題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • https://sqlserver.code.blog/2019/12/10/different-ag-groups-have-the-exactly-same-group_id-value-if-the-group-names-are-same-and-the-cluster_type-exter ...
  • Redis Cluster 自動化安裝,擴容和縮容 之前寫過一篇基於python的redis集群自動化安裝的實現,基於純命令的集群實現還是相當繁瑣的,因此官方提供了redis-trib.rb這個工具雖然官方的的redis-trib.rb提供了集群創建、 檢查、 修複、均衡等命令行工具,之所個人接受不 ...
  • Linux使用MySQL Yum存儲庫上安裝MySQL 5.7,適用於Oracle Linux,Red Hat Enterprise Linux和CentOS系統。 1、添加MySQL Yum存儲庫 將MySQL Yum存儲庫添加到系統的存儲庫列表中。這是一次性操作,可以通過安裝MySQL提供的RP ...
  • 前言 Hello我又來了,快年底了,作為一個有抱負的碼農,我想給自己攢一個年終總結。自上上篇寫了手動搭建Redis集群和MySQL主從同步(非Docker)和上篇寫了動手實現MySQL讀寫分離and故障轉移之後,索性這次把資料庫中最核心的也是最難搞懂的內容,也就是索引,分享給大家。 這篇博客我會談談 ...
  • ###第一周:R基礎 rm(list = ls()) #ctr+L###矩陣相乘,函數diag()a=matrix(1:12,nrow=3,ncol=4)b=matrix(1:12,nrow=4,ncol=3)a%*%ba=matrix(1:16,nrow=4,ncol=4)diag(a)#返回對角 ...
  • ORACLE資料庫中,我們會使用一些SQL語句找出存在隱式轉換的問題SQL,其中網上流傳的一個SQL語句如下,查詢V$SQL_PLAN的欄位FILTER_PREDICATES中是否存在INTERNAL_FUNCTION: SELECT SQL_ID, PLAN_HASH_VALUEFROM V$SQ... ...
  • 在安裝neo4j之前,需要安裝Java JRE,並配置Java開發環境,然後安裝neo4j服務。 一、CentOS下安裝 1.下載Neo4j 去官網下載最新的neo4j,選擇社區版。地址:https://neo4j.com/download/other-releases/#releases 將本地下 ...
  • Frida介面功能介紹 Frida是個so級別的hook框架,它可以幫助開發、安全人員對指定的進程的so模塊進行分析。它主要提供了功能簡單的Python介面和功能豐富的JS介面,使得hook函數和修改so可以編程化,介面中包含了主控端與目標進程的交互介面。 目標進程的交互介面分為: JS介面 功能包 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...