iOS核心动画高级技巧 - 7

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来绘图的一个一般缘由就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:app

  • 任意多边形(不只仅是一个矩形)框架

  • 斜线或曲线异步

  • 文本

  • 渐变

举个例子,清单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章“图层性能”中,咱们将讨论和图层渲染和组合相关的性能问题。

相关文章
相关标签/搜索