iOS图像优化技巧
1、如何处理大尺寸图片?
2、如何处理瀑布流图片占用大量内存的问题?
3、如何处理多张图片上传和下载问题?
1、处理大尺寸图片
那么,什么时候对图像进行渲染优化才有意义呢?
当它明显大于 UIImageView
显示尺寸的时候
想要完整渲染这张宽高为 12,000 px 的图片,需要高达 20 MB 的空间。对于当今的硬件来说,你可能不会在意这么少兆字节的占用。但那只是它压缩后的尺寸。要展示它,UIImageView
首先需要把 JPEG 数据解码成位图(bitmap),如果要在一个 UIImageView
上按原样设置这张全尺寸图片,你的应用内存占用将会激增到几百兆,对用户明显没有什么好处(毕竟,屏幕能显示的像素有限)。但只要在设置 UIImageView
的 image
属性之前,将图像渲染的尺寸调整成 UIImageView
的大小,你用到的内存就会少一个数量级:
内存消耗 (MB) | |
---|---|
无下采样 | 220.2 |
下采样 | 23.7 |
这个技巧就是众所周知的下采样(downsampling),在这些情况下,它可以有效地优化你应用的性能表现。如果你想了解更多关于下采样的知识或者其它图形图像的最佳实践,请参照 来自 WWDC 2018 的精彩课程。
而现在,很少有应用程序会尝试一次性加载这么大的图像了,但是也跟我从设计师那里拿到的图片资源不会差太多。(认真的吗?一张颜色渐变的 PNG 图片要 3 MB?) 考虑到这一点,让我们来看看有什么不同的方法,可以让你用来对图像进行优化或者下采样。
不用说,这里所有从 URL 加载的示例图像都是针对本地文件。记住,在应用的主线程同步使用网络请求图像绝不是什么好主意。
图像渲染优化技巧
优化图像渲染的方法有很多种,每种都有不同的功能和性能特性。我们在本文看到的这些例子,架构层次跨度上从底层的 Core Graphics、vImage、Image I/O 到上层的 Core Image 和 UIKit 都有。
- 绘制到 UIGraphicsImageRenderer 上
- 绘制到 Core Graphics Context 上
- 使用 Image I/O 创建缩略图像
- 使用 Core Image 进行 Lanczos 重采样
- 使用 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 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ |
技巧 #1: 绘制到 UIGraphicsImageRenderer 上
图像渲染优化的最上层 API 位于 UIKit 框架中。给定一个 UIImage
,你可以绘制到 UIGraphicsImageRenderer
的上下文(context)中以渲染缩小版本的图像:
1 | import UIKit |
UIGraphicsImageRenderer
是一项相对较新的技术,在 iOS 10 中被引入,用以取代旧版本的 UIGraphicsBeginImageContextWithOptions
/ UIGraphicsEndImageContext
API。你通过指定以 point
计量的 size
创建了一个 UIGraphicsImageRenderer
。**image
方法带有一个闭包参数,返回的是一个经过闭包处理后的位图。最终,原始图像便会在缩小到指定的范围内绘制。**
技巧 #2:绘制到 Core Graphics Context 中
Core Graphics / Quartz 2D 提供了一系列底层 API 让我们可以进行更多高级的配置。
给定一个 CGImage
作为暂时的位图上下文,使用 draw(_:in:)
方法来绘制缩放后的图像:
1 | import UIKit |
这个 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 | import ImageIO |
给定一个 CGImageSource
和一系列配置选项,CGImageSourceCreateThumbnailAtIndex(_:_:_:)
函数创建了一个图像的缩略图。优化尺寸大小的操作是通过 kCGImageSourceThumbnailMaxPixelSize
完成的,它根据图像原始宽高比指定的最大尺寸来缩放图像。通过设定 kCGImageSourceCreateThumbnailFromImageIfAbsent
或 kCGImageSourceCreateThumbnailFromImageAlways
选项,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种类型,比较简单,在 UIScrollView
的代理中就可以监听到。
1 | - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView; |
而第2种类型和第3种类型,就没有方法让我们可以直接监听到了。但是只要是滑动了,就一定会触发 UIScrollView
的下面代理,然后通过 UIScrollView
部分属性的改变,我们就可以监听到滚动停止了,后面会详细介绍方法。
1 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; |
监听UIScrollView停止滚动
通过翻阅文档,我们可以看到 UIScrollView
有三个属性: tracking、dragging、decelerating。
1 | // returns YES if user has touched. may not yet have started dragging |
在滚动和滚动结束时,这三个属性的值都不相同。我们利用这三个属性,完成对 UIScrollView
停止滚动的监听。
停止类型1:scrollViewDidEndDecelerating
1 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; |
停止类型2:scrollViewDidEndDragging & scrollViewDidEndDecelerating
1 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; |
停止类型3:scrollViewDidEndDragging
1 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; |
通过上面的代码,可以发现,我们只需要对 UIScrollView
的这三个属性进行相应的组合,就可以监听到 UIScrollView
停止滚动的事件了。
1 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { |
1 | -(void)scrollViewDidScroll:(UIScrollView *)sender |
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进行布局