KM的博客.

iOS图像优化技巧

字数统计: 2.8k阅读时长: 11 min
2018/12/01

iOS图像优化技巧

  • 1、如何处理大尺寸图片?

  • 2、如何处理瀑布流图片占用大量内存的问题?

  • 3、如何处理多张图片上传和下载问题?

1、处理大尺寸图片

那么,什么时候对图像进行渲染优化才有意义呢?

当它明显大于 UIImageView 显示尺寸的时候

想要完整渲染这张宽高为 12,000 px 的图片,需要高达 20 MB 的空间。对于当今的硬件来说,你可能不会在意这么少兆字节的占用。但那只是它压缩后的尺寸。要展示它,UIImageView 首先需要把 JPEG 数据解码成位图(bitmap),如果要在一个 UIImageView 上按原样设置这张全尺寸图片,你的应用内存占用将会激增到几百兆,对用户明显没有什么好处(毕竟,屏幕能显示的像素有限)。但只要在设置 UIImageViewimage 属性之前,将图像渲染的尺寸调整成 UIImageView 的大小,你用到的内存就会少一个数量级:

内存消耗 (MB)
无下采样 220.2
下采样 23.7

这个技巧就是众所周知的下采样(downsampling),在这些情况下,它可以有效地优化你应用的性能表现。如果你想了解更多关于下采样的知识或者其它图形图像的最佳实践,请参照 来自 WWDC 2018 的精彩课程

而现在,很少有应用程序会尝试一次性加载这么大的图像了,但是也跟我从设计师那里拿到的图片资源不会差多。(认真的吗?一张颜色渐变的 PNG 图片要 3 MB?) 考虑到这一点,让我们来看看有什么不同的方法,可以让你用来对图像进行优化或者下采样。

不用说,这里所有从 URL 加载的示例图像都是针对本地文件。记住,在应用的主线程同步使用网络请求图像绝不是什么好主意。

图像渲染优化技巧

优化图像渲染的方法有很多种,每种都有不同的功能和性能特性。我们在本文看到的这些例子,架构层次跨度上从底层的 Core Graphics、vImage、Image I/O 到上层的 Core Image 和 UIKit 都有。

  1. 绘制到 UIGraphicsImageRenderer 上
  2. 绘制到 Core Graphics Context 上
  3. 使用 Image I/O 创建缩略图像
  4. 使用 Core Image 进行 Lanczos 重采样
  5. 使用 vImage 优化图片渲染

下面的这些数字是多次迭代加载、优化、渲染之前那张 超大地球图片 的平均时间:

耗时 (seconds)
技巧 #1: UIKit 0.1420
技巧 #2: Core Graphics 1 0.1722
技巧 #3: Image I/O 0.1616
技巧 #4: Core Image 2 2.4983
技巧 #5: vImage 2.3126

1
设置不同的 CGInterpolationQuality 值出来的结果是一致的,在性能上的差异可以忽略不计。

2
若在 CIContext 创建时设置 kCIContextUseSoftwareRenderer 的值为 true,会导致耗时相比基础结果慢一个数量级。

使用CGContextDrawImage()

异步解码图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1、获取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"img"].CGImage;

// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}

// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);

// 2、获取context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);

//3、 draw
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);

// get CGImage
cgImage = CGBitmapContextCreateImage(context);

// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];

// release
CGContextRelease(context);
CGImageRelease(cgImage);

// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});

技巧 #1: 绘制到 UIGraphicsImageRenderer 上

图像渲染优化的最上层 API 位于 UIKit 框架中。给定一个 UIImage,你可以绘制到 UIGraphicsImageRenderer 的上下文(context)中以渲染缩小版本的图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit

// 技巧 #1
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
guard let image = UIImage(contentsOfFile: url.path) else {
return nil
}

let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { (context) in
image.draw(in: CGRect(origin: .zero, size: size))
}
}

UIGraphicsImageRenderer 是一项相对较新的技术,在 iOS 10 中被引入,用以取代旧版本的 UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext API。你通过指定以 point 计量的 size 创建了一个 UIGraphicsImageRenderer。**image 方法带有一个闭包参数,返回的是一个经过闭包处理后的位图。最终,原始图像便会在缩小到指定的范围内绘制。**

技巧 #2:绘制到 Core Graphics Context 中

Core Graphics / Quartz 2D 提供了一系列底层 API 让我们可以进行更多高级的配置。

给定一个 CGImage 作为暂时的位图上下文,使用 draw(_:in:) 方法来绘制缩放后的图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import UIKit
import CoreGraphics

// 技巧 #2
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
return nil
}

let context = CGContext(data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: image.bitsPerComponent,
bytesPerRow: image.bytesPerRow,
space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: image.bitmapInfo.rawValue)
context?.interpolationQuality = .high
context?.draw(image, in: CGRect(origin: .zero, size: size))

guard let scaledImage = context?.makeImage() else { return nil }

return UIImage(cgImage: scaledImage)
}

这个 CGContext 初始化方法接收了几个参数来构造一个上下文,包括了必要的宽高参数,还有在给出的色域范围内每个颜色通道所需要的内存大小。在这个例子中,这些参数都是通过 CGImage 这个对象获取的。下一步,设置 interpolationQuality 属性为 .high 指示上下文在保证一定的精度上填充像素。draw(_:in:) 方法则是在给定的宽高和位置绘制图像,可以让图片在特定的边距下裁剪,也可以适用于一些像是人脸识别之类的图像特性。最后 makeImage() 从上下文获取信息并且渲染到一个 CGImage 值上(之后会用来构造 UIImage 对象)。

技巧 #3:使用 Image I/O 创建缩略图像

处理大分辨率图片时,往往容易出现OOM,原因是-[UIImage drawInRect:]在绘制时,先解码图片,再生成原始分辨率大小的bitmap,这是很耗内存的。

解决方法是使用更低层的ImageIO接口,避免中间bitmap产生:

Image I/O 是一个强大(却鲜有人知)的图像处理框架。

它可以读写许多不同图像格式,访问图像的元数据,还有执行常规的图像处理操作。这个框架通过先进的缓存机制,提供了平台上最快的图片编码器和解码器,甚至可以增量加载图片。

这个重要的 CGImageSourceCreateThumbnailAtIndex 提供了一个带有许多不同配置选项的 API,比起在 Core Graphics 中等价的处理操作要简洁得多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ImageIO

// 技巧 #3
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
]

guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
else {
return nil
}

return UIImage(cgImage: image)
}

给定一个 CGImageSource 和一系列配置选项,CGImageSourceCreateThumbnailAtIndex(_:_:_:) 函数创建了一个图像的缩略图。优化尺寸大小的操作是通过 kCGImageSourceThumbnailMaxPixelSize 完成的,它根据图像原始宽高比指定的最大尺寸来缩放图像。通过设定 kCGImageSourceCreateThumbnailFromImageIfAbsentkCGImageSourceCreateThumbnailFromImageAlways 选项,Image I/O 可以自动缓存优化后的结果以便后续调用。

总结

  • UIKit, Core Graphics, 和 Image I/O 都能很好地用于大部分图片的优化操作。
  • 如果(在 iOS 平台,至少)要选择一个的话,UIGraphicsImageRenderer 是你最佳的选择。
  • Core Image 在图像优化渲染操作方面性能表现优越。实际上,根据 Apple 官方 Core Image 编程规范中的性能最佳实践单元,你应该使用 Core Graphics 或 Image I/O 对图像进行裁剪和下采样,而不是用 Core Image。
  • 除非你已经在使用 **vImage**,否则在大多数情况下用到底层的 Accelerate API 所需的额外工作可能是不合理的。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

2、同时处理大量图片

UIScrollView滚动停止监测

通过调查发现 UIScrollView 停止滚动的类型分为三种:

  1. 快速滚动,自然停止
  2. 快速滚动,手指按压突然停止
  3. 慢速上下滑动停止

第1种类型,比较简单,在 UIScrollView 的代理中就可以监听到。

1
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;

而第2种类型和第3种类型,就没有方法让我们可以直接监听到了。但是只要是滑动了,就一定会触发 UIScrollView 的下面代理,然后通过 UIScrollView 部分属性的改变,我们就可以监听到滚动停止了,后面会详细介绍方法。

1
2
3
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

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

监听UIScrollView停止滚动

通过翻阅文档,我们可以看到 UIScrollView 有三个属性: tracking、dragging、decelerating。

1
2
3
4
5
6
7
8
9
// returns YES if user has touched. may not yet have started dragging
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;

// returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging
@property(nonatomic,readonly,getter=isDragging) BOOL dragging;


// returns YES if user isn't dragging (touch up) but scroll view is still moving
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;

在滚动和滚动结束时,这三个属性的值都不相同。我们利用这三个属性,完成对 UIScrollView 停止滚动的监听。

停止类型1:scrollViewDidEndDecelerating

1
2
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
tracking:0,dragging:0,decelerating:0

停止类型2:scrollViewDidEndDragging & scrollViewDidEndDecelerating

1
2
3
4
5
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
tracking:1,dragging:0,decelerating:1

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
tracking:0,dragging:0,decelerating:0

停止类型3:scrollViewDidEndDragging

1
2
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
tracking:1,dragging:0,decelerating:0

通过上面的代码,可以发现,我们只需要对 UIScrollView 的这三个属性进行相应的组合,就可以监听到 UIScrollView 停止滚动的事件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self stoppedScrolling];
}
}

- (void)stoppedScrolling {
// ...
NSLog(@"停止滚动了!!!");
}

1
2
3
4
5
6
7
8
9
10
-(void)scrollViewDidScroll:(UIScrollView *)sender
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:nil afterDelay:0.1];
}

-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}

3、处理多图上传下载问题?

UI刷新问题

layout的相关方法:

  • layoutSubviews
  • layoutIfNeeded
  • setNeedsLayout
  • setNeedsDisplay
  • drawRect
  • sizeThatFits
  • sizeToFit

layoutSubviews

这个方法,默认没有做任何事情,需要子类进行重写 。 系统在很多时候会去调用这个方法:

1.初始化不会触发layoutSubviews,但是如果设置了不为CGRectZero的frame的时候就会触发。
2.addSubview会触发layoutSubviews
3.设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
4.滚动一个UIScrollView会触发layoutSubviews
5.旋转Screen会触发父UIView上的layoutSubviews事件
6.改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.layoutSubviews,

当我们在某个类的内部调整子视图位置时,需要调用。

反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写

setNeedsLayout

  • 标记为需要重新布局,不立即刷新,但layoutSubviews一定会被调用
  • 配合layoutIfNeeded立即更新

layoutIfNeeded

  • 如果有需要刷新的标记,立即调用layoutSubviews进行布局
CATALOG
  1. 1. iOS图像优化技巧
  2. 2. 1、处理大尺寸图片
    1. 2.1. 图像渲染优化技巧
    2. 2.2. 使用CGContextDrawImage()
    3. 2.3. 异步解码图片
      1. 2.3.1. 技巧 #1: 绘制到 UIGraphicsImageRenderer 上
      2. 2.3.2. 技巧 #2:绘制到 Core Graphics Context 中
      3. 2.3.3. 技巧 #3:使用 Image I/O 创建缩略图像
    4. 2.4. 总结
  3. 3. 2、同时处理大量图片
    1. 3.0.1. UIScrollView滚动停止监测
    2. 3.0.2. 监听UIScrollView停止滚动
  • 4. 3、处理多图上传下载问题?
  • 5. UI刷新问题
    1. 5.0.0.1. layout的相关方法:
    2. 5.0.0.2. layoutSubviews
    3. 5.0.0.3. setNeedsLayout
    4. 5.0.0.4. layoutIfNeeded