iOS Core Animation Advanced Techniques-图像IO
上十三章节:
这篇随笔主要介绍有关图像IO。
加载与潜伏:
-
- 图片消耗很大一部分内存,不太可能把需要显示的图片都保留在内存中
- 所以需要在应用运行的时候周期性地加载和卸载图片。
- 设法在程序生命周期中不易察觉的时候加载图片,例如启动,或者在屏幕切换的过程中。
- 有些时候,提前加载所有的东西并不明智。
- 线程加载:
- 对于大图来说,不应放到主线程中加载。
- 错误范例://在主线程中-collectionView:cellForItemAtIndexPath:方法中同步加载图片
- #import "ViewController.h"
- @interface ViewController() <UICollectionViewDataSource>
- @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
- /*
- 当传送器滚动的时候,图片也在实时加载,于是(预期中的)卡动就发生了。
- 通过时间分析工具:
- UIImage的+imageWithContentsOfFile:方法消耗大部分时间
- 很明显,图片加载造成了瓶颈。
- */
- 解决方案:
- 在子线程中加载图片,这并不能够降低实际的加载时间,只不过主线程能够有时间做一些别的事情,比如响应用户输入,以及滑动动画。
- 示范例子:
- /*
- 由于视图在UICollectionView会被循环利用,
- 我们加载图片的时候不能确定是否被不同的索引重新复用。
- 为了避免图片加载到错误的视图中,我们在加载前把单元格打上索引的标签,然后在设置图片的时候检测标签是否发生了改变。
- */
- - (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) {//因为加载需要时间,要在对应cell显示对应的image,所以需要对比索引
- imageView.image = image;
- }
- });
- });
- return cell;
- }
- 延迟解压:
- 一旦图片文件被加载就必须要进行解码。
- 解码过程是一个相当复杂的任务,需要消耗非常长的时间。
- 解码后的图片将同样使用相当大的内存。
- 当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。
- 这就会导致在准备绘制图片的时候影响性能,因为需要在绘制之前才进行解压(通常是消耗时间的问题所在)。
- 最简单的方法就是使用UIImage的+imageNamed:方法避免延时加载。这个方法会在加载图片之后立刻进行解压
- 但是:
- +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来创建图片,强制图片立刻解压,然后在图片的生命周期保留解压后的版本。
- 这样的好处在于绘制图片可以把它放到后台线程(例如加载本身)中执行,而不会阻塞UI。
- */
- 强制解压后,可以通过以下两种方式提前渲染图片:
- 1.将图片的一个像素绘制成一个像素大小的CGContext。这样仍然会解压整张图片,但是绘制本身并没有消耗任何时间。可以在任何时间点绘制出来。
- 2.将整张图片绘制到CGContext中,丢弃原始的图片,并且用一个从上下文内容中新的图片来代替。
- 如果不使用+imageNamed:,那么把整张图片绘制到CGContext可能是最佳的方式了。
- 示范例子:
- - (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];//把图片绘制到CGContext上下文中
- image = UIGraphicsGetImageFromCurrentImageContext();//取出绘制后的图片并赋值给之前的图片变量上,那么之前的图片对象就被释放掉
- UIGraphicsEndImageContext();
- //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;
- }
- /*
- 有一个问题:
- 由于图片加载是在子线程异步读取数据的,
- 所以每次显示需要一定的时间才能把图片显示出来。
- */
- CATiledLayer:
- CATiledLayer专用图层可以用来异步加载和显示大型图片,而不阻塞用户输入。
- 可以使用CATiledLayer在UICollectionView中为每个表格创建分离的CATiledLayer实例加载传动器图片,每个表格仅使用一个图层。
- 这样使用CATiledLayer有几个潜在的弊端:
- 1.CATiledLayer的队列和缓存算法没有暴露出来,所以我们只能祈祷它能匹配我们的需求
- 2.CATiledLayer需要我们每次重绘图片到CGContext中,即使它已经解压缩,而且和我们单元格尺寸一样(因此可以直接用作图层内容,而不需要重绘)。
- 示范例子:
- //使用CATiledLayer的图片传送器
- #import "ViewController.h"
- #import <QuartzCore/QuartzCore.h>
- @interface ViewController() <UICollectionViewDataSource>
- @property (nonatomic, copy) NSArray *imagePaths;
- @property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
- @end
- @implementation ViewController
- - (void)viewDidLoad
- {
- //set up data
- self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:@"Vacation Photos"];
- [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 the tiled layer
- CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject];
- if (!tileLayer) {
- tileLayer = [CATiledLayer layer];
- tileLayer.frame = cell.bounds;
- tileLayer.contentsScale = [UIScreen mainScreen].scale;
- tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
- tileLayer.delegate = self;
- [tileLayer setValue:@(indexPath.row) forKey:@"index"];
- [cell.contentView.layer addSublayer:tileLayer];
- }
- //tag the layer with the correct index and reload
- tileLayer.contents = nil;
- [tileLayer setValue:@(indexPath.row) forKey:@"index"];
- [tileLayer setNeedsDisplay];
- return cell;
- }
- - (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
- {
- //get image index
- NSInteger index = [[layer valueForKey:@"index"] integerValue];
- //load tile image
- NSString *imagePath = self.imagePaths[index];
- UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
- //calculate image rect
- CGFloat aspectRatio = tileImage.size.height / tileImage.size.width;
- CGRect imageRect = CGRectZero;
- imageRect.size.width = layer.bounds.size.width;
- imageRect.size.height = layer.bounds.size.height * aspectRatio;
- imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2;
- //draw tile
- UIGraphicsPushContext(ctx);
- [tileImage drawInRect:imageRect];
- UIGraphicsPopContext();
- }
- @end
- /*
- 在-drawLayer:inContext:方法中,我们需要知道图层属于哪一个indexPath以加载正确的图片。这里我们利用了CALayer的KVC来存储和检索任意的值,将图层和索引打标签。
- 有一个问题在于图片加载到屏幕上后有一个明显的淡入。
- 可以调整CATiledLayer的fadeDuration属性来调整淡入的速度,或者直接将整个渐变移除。
- 上面的图片要过一会才显示的问题还是会存在,下面将通过缓存机制优化该问题
- */
分辨率交换:
- 如果需要快速加载和显示移动大图,简单的办法就是欺骗人眼,在移动传送器的时候显示一个小图(或者低分辨率),然后当停止的时候再换成大图。
- 这意味着我们需要对每张图片存储两份不同分辨率的副本,但是幸运的是,由于需要同时支持Retina和非Retina设备,本来这就是普遍要做到的。
- 为了做到图片交换,我们需要利用UIScrollView的一些实现UIScrollViewDelegate协议的委托方法(和其他类似于UITableView和UICollectionView基于滚动视图的控件一样):
- - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- 可以使用这几个方法来检测传送器是否停止滚动,然后加载高分辨率的图片。
缓存:
- 就是将昂贵计算后的结果(或者是从闪存或者网络加载的文件)存储到内存中,以便后续使用,这样访问起来很快。
- 如果有很多张图片要显示,提前把它们全部都加载进去是不切实际的,但是,这并不意味着,你在遇到加载问题后,当其移出屏幕时就立刻将其销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了
- 缓存本质上是一个权衡过程 :
- 为了提升性能而消耗了内存,但是由于内存是一个非常宝贵的资源,所以不能把所有东西都做缓存。
- 大多情况下,iOS都为我们做好了图片的缓存:
- +imageNamed:方法的好处:
- 1.可以立刻解压图片而不用等到绘制的时候
- 2.它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。
- //在nib文件中引用的图片同样也是这个机制
- +imageNamed:方法的好处:
- 有些时候你还是要实现自己的缓存机制,原因如下:
- 1.[UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片
- 2.如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。
- 3.缓存机制并不是公开的,所以你不能很好地控制它。例如:
- 没法做到检测图片是否在加载之前就做了缓存
- 不能够设置缓存大小
- 当图片没用的时候也不能把它从缓存中移除。
- 自定义缓存:
- 写自己的图片缓存涉及哪些方面:
- 1.选择一个合适的缓存键:
- 缓存键用来做图片的唯一标识。
- 2.提前缓存:
- 提前加载的逻辑是应用内在就有的,比如一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现
- 3.缓存失效:
- 如果图片文件发生了变化,怎样才能通知到缓存更新呢?一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。
- 4.缓存回收:
- 当内存不够的时候,如何判断哪些缓存需要清空呢?对缓存回收的问题,苹果提供了一个叫做NSCache通用的解决方案
- 1.选择一个合适的缓存键:
- 写自己的图片缓存涉及哪些方面:
NSCache:
- NSCache和NSDictionary类似:
- 通过-setObject:forKey:和-object:forKey:方法分别来插入,检索。
- 和字典不同的是:
- NSCache在系统低内存的时候自动丢弃存储的对象。自动
- 可以使用:
- -setTotalCostLimit:
- 方法来指定全体缓存的尺寸
- -setCountLimit:
- 方法设置缓存大小
- -setObject:forKey:cost:
- 来对每个存储的对象指定消耗的值来提供一些暗示何时丢弃对象
- 对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。
- -setTotalCostLimit:
- 示范例子:
- //使用图片缓存和提前加载的实现来扩展之前的传送器案例
- #import "ViewController.h"
- @interface ViewController() <UICollectionViewDataSource>
- @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
- /*
- 通过预加载和缓存机制
- 上面因子线程读取图片要过一会才显示出来的问题,经过缓存机制有效的改善了。
- */
文件格式:
- JPEG对于噪点大的图片效果很好
- PNG更适合于扁平颜色,锋利的线条或者一些渐变色的图片
- 相对于不友好的PNG图片,相同像素的JPEG图片总是比PNG加载更快
- 一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的。