版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.10.10 星期六 |
前言
在我们开发中总有从远程下载图片,OC中有个很成熟的三方框架,大家都知道的
SDWebImage
,同样Swift中也有类似的三方框架Nuke
,接下来几篇我们就一起看一下这个框架。
1. Nuke框架详细解析(一) —— 基本概览(一)
开始
首先看下主要内容:
在本
Nuke
教程中,您将学习如何使用Swift Package Manager
集成Nuke
以及如何使用它来加载带有和不带有Combine
的远程图像。内容主要来自翻译。
下面看下写作环境
Swift 5, iOS 13, Xcode 11
下面就是正文啦
每年,最新的iPhone
相机越来越好。但是你知道谁拥有最好的相机吗?是NASA!
他们拍摄的最佳主题是什么?再一次,除非您为NASA工作,否则不幸的是答案不是您。SPACE
!但是,您紧随其后。
拥有一个可以显示NASA
美丽照片的应用程序,这样您可以随时在手机上查看它们,这不是很好吗?唯一的问题是这些照片中的某些照片非常大-大约60MB
。最重要的是,他们没有可用的缩略图。
您已经不必编写与OperationQueue
相关的大量样板代码了。如果有更好的办法。
好消息!还有一种更好的方式,可以使用称为Nuke的第三方库的形式。
在此Nuke教程中,您将学习如何:
- 使用
Swift Package Manager
将Nuke
集成到您的项目中。 - 加载远程图像,同时保持应用的响应速度。
- 快速调整照片大小,以控制内存使用量。
- 集成
Nuke
的Combine
扩展,即ImagePublisher
。 - 在项目中使用
Combine
可连续加载不同的图像尺寸。
准备好在舒适的iPhone
上探索太空了吗?是时候开始了!
打开启动程序项目,然后构建并运行。 您可以在闲暇时浏览一下该应用。
Far Out Photos
应用可从NASA
网站获取照片,并将其显示在基于UICollectionView
的画廊中。 轻触其中一张美丽的照片将其全屏显示。
不幸的是,此应用存在一个主要问题。 速度慢且反应迟钝。 这是因为所有对图像的网络请求都是在主线程上完成的,因此它们会干扰您的滚动能力。 此外,由于没有缓存,因此每次加载图像时都会重新获取它们。
您将使用Nuke
修复此应用并使其平稳运行。
Setting up Nuke
您可以选择几种方法将Nuke
集成到项目中,但是在本教程中,您将使用Swift Package Manager
或SwiftPM
。 这是Apple
在Xcode 11
中引入的在项目中安装软件包的一种简便方法。
要了解有关SwiftPM
的更多信息,请查看iOS的Swift Package Manager for iOS教程。
1. Installing with Swift Package Manager
要添加包,请转到File ▸ Swift Packages ▸ Add Package Dependency
。
这将打开一个对话框,以选择要安装的软件包。 在文本字段中输入URL https://github.com/kean/Nuke.git
,然后单击Next
。
选择rule
的Version
,然后从下拉菜单中选择Up to Next Major
,然后在框中输入9.1.0
。 这指定该版本的最低版本为9.1.0
,最高为(但不包括)10.0.0
。 然后选择Next
,等待Xcode验证软件包。
确保已选择target
中的Far Out Photos
,然后单击Finish
。
安装软件包后,您的Project
导航器应如下所示。
完成此操作后,就该进入Nuke
了。
注意:由于Nuke仍在积极开发中,因此您可能会看到不同的补丁程序编号(版本中的第三个号码)。
Using Nuke: Basic Mode
首先要解决的是滚动浏览图库时应用程序的糟糕响应性。
首先,打开PhotoGalleryViewController.swift
。 将以下内容添加到文件的顶部:
import Nuke
接下来,找到collectionView(_:cellForItemAt :)
。 在这种方法中,您应该看到以下代码:
if let imageData = try? Data(contentsOf: photoURLs[indexPath.row]),
let image = UIImage(data: imageData) {
cell.imageView.image = image
} else {
cell.imageView.image = nil
}
此代码无法有效地从提供的URL中获取照片,并将其分配给collection view cell
中的image view
,从而在整个过程中阻塞了主线程。
删除这七行,并用以下内容替换它们:
// 1
let url = photoURLs[indexPath.row]
// 2
Nuke.loadImage(with: url, into: cell.imageView)
在此代码中,您:
- 1) 根据单元格的
index path
获取正确照片的URL
。 - 2) 使用
Nuke
将图片从URL直接加载到单元格的image view
中。Nuke
正在做所有繁重的任务。 它将图像加载到后台线程上,并将其分配给image view
。
就这些! 比预期容易得多吧?
构建并运行。 您应该注意到一项重大改进。 现在,您可以平滑滚动图库视图!
1. Setting Loading Options
这是一个很好的开始,但是仍然缺少一些非常重要的东西。 该应用程序没有向用户提供有关图像即将到来的视觉反馈。 取而代之的是,如果用户滚动得足够快,他们只会看到几乎纯黑的屏幕。
如果只有Nuke
可以用某种方式代替加载图像显示图像,则可以使用placeholder
。
今天是您的幸运日。 是的!
Nuke
有一个名为ImageLoadingOptions
的结构,它允许您更改Nuke
呈现图像的方式,包括设置占位符图像。
返回collectionView(_:cellForItemAt :)
,将您刚才编写的代码替换为以下代码:
let url = photoURLs[indexPath.row]
// 1
let options = ImageLoadingOptions(
placeholder: UIImage(named: "dark-moon"),
transition: .fadeIn(duration: 0.5)
)
// 2
Nuke.loadImage(with: url, options: options, into: cell.imageView)
在这里:
- 1) 创建一个
ImageLoadingOptions
结构。 您将placeholder image
设置为名为dark-moon
的图像,并配置从placeholder image
到从网络获取的图像的过渡。 在这种情况下,您可以将过渡设置为0.5
秒内的淡入。 - 2) 与以前一样,使用
Nuke
将图像从URL
直接加载到单元格的image view
中,但这一次使用刚刚配置的选项。
现在,构建并运行该应用程序。 您应该看到placeholder image
逐渐消失,以显示加载时的真实图像。
很棒的工作! 仅需几行代码,您就已经大大改善了该应用程序。
2. Monitoring Memory Usage
到目前为止,该应用程序如何为您服务? 您是否经历过任何崩溃?
如果您一直在设备上运行此项目,则很有可能遇到一两次崩溃。 为什么? 因为这个程序是一个记忆猪。
3. Using Instruments
若要查看它的严重程度,请再次运行该应用程序,然后执行以下操作:
- 1) 在“导航器”面板中选择
Debug navigator
。 - 2) 然后在调试仪表列表下选择内存
Memory
。 - 3) 单击
Profile in Instruments
。 - 4) 最后,在出现的对话框中单击
Restart
。
在运行profiler
的情况下,将库滚动到底部,尤其要注意标记为VM: CG raster data
的行的Persistent Bytes
列。 保留了超过1 GB
的数据!
问题的根源在于,即使下载的图像在屏幕上看起来很小,它们仍然是全尺寸图像,并完全存储在内存中。 这不好。
不幸的是,NASA
并未为其图像提供缩略图大小。
该怎么办? 也许Nuke
可以帮忙吗?
确实可以!
Advanced Nuking
Nuke
具有许多功能,可用于优化内存并改善应用程序的用户体验和加载时间。
1. Loading with Requests
到目前为止,您已经向loadImage
传递了一个URL
。 但是Nuke
也具有接受ImageRequest
的方法的变体。
ImageRequest
可以定义一组在下载图像后要应用的图像处理器processors
。 在这里,您将创建一个调整大小的处理器,并将其附加到请求中。
在photoURLs
实例变量的定义之后的PhotoGalleryViewController.swift
中,添加以下两个计算出的属性:
// 1
var pixelSize: CGFloat {
return cellSize * UIScreen.main.scale
}
// 2
var resizedImageProcessors: [ImageProcessing] {
let imageSize = CGSize(width: pixelSize, height: pixelSize)
return [ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill)]
}
这是您的新代码的作用:
- 1)
pixelSize
是单元格的大小(以像素为单位)。 一些iPhone
的分辨率为2倍(每点2像素),而其他iPhone的分辨率为3倍(每点3像素)。 您想让图像看起来清晰而不在高分辨率屏幕上出现像素化。 此乘数也称为设备的比例(scale)
。 - 2)
resizedImageProcessors
是一个Nuke
配置,用于定义要对图像执行的操作。 目前,您只想调整图像的大小以适合您的单元格并将宽高比填充用作内容模式。
返回collectionView(_:cellForItemAt :)
,将对Nuke.loadImage(with:options:into :)
的调用替换为以下内容:
// 1
let request = ImageRequest(
url: url,
processors: resizedImageProcessors)
// 2
Nuke.loadImage(with: request, options: options, into: cell.imageView)
使用此代码,您:
- 1) 为所需的图像
URL
创建一个ImageRequest
,并使用您先前定义的图像处理器在下载后对图像应用调整大小。 - 2) 让
Nuke
使用您先前设置的选项根据此请求加载图像,并将其显示在单元格的image view
中。
现在,再次构建并运行,并以与以前相同的方式打开内存分析器。
哇! VM: CG raster data
现在不到300MB
! 这个数字要合理得多!
2. Optimizing Code
当前,对于每个collection view cell
,您都在重新创建相同的ImageLoadingOptions
。 那不是超级有效。
解决此问题的一种方法是为您使用的选项创建一个常量类属性,然后每次将其传递给Nuke
的loadImage(with:options:into :)
。
Nuke
有更好的方法可以做到这一点。 在Nuke
中,如果未提供其他选项,则可以将ImageLoadingOptions
定义为默认值。
在PhotoGalleryViewController.swift
中,将以下代码添加到viewDidLoad()
的底部
// 1
let contentModes = ImageLoadingOptions.ContentModes(
success: .scaleAspectFill,
failure: .scaleAspectFit,
placeholder: .scaleAspectFit)
ImageLoadingOptions.shared.contentModes = contentModes
// 2
ImageLoadingOptions.shared.placeholder = UIImage(named: "dark-moon")
// 3
ImageLoadingOptions.shared.failureImage = UIImage(named: "annoyed")
// 4
ImageLoadingOptions.shared.transition = .fadeIn(duration: 0.5)
在此代码中,您:
- 1) 为每种图像加载结果类型定义默认的
contentMode
:success, failure and the placeholder
。 - 2) 设置默认的占位符图像。
- 3) 设置默认图片以在出现错误时显示。
- 4) 定义从占位符到另一个图像的默认过渡。
完成后,返回到collectionView(_:cellForItemAt :)
。 在这里,您需要做两件事。
首先,删除以下代码行:
let options = ImageLoadingOptions(
placeholder: UIImage(named: "dark-moon"),
transition: .fadeIn(duration: 0.5)
)
您不再需要这些,因为您定义了默认选项。 然后,您需要将对loadImage(with:options:into :)
的调用更改为如下所示:
Nuke.loadImage(with: request, into: cell.imageView)
如果您现在构建并运行代码,可能不会有太大的区别,但是您确实在改进代码的同时潜入了一项新功能。
关闭您的Wi-Fi
并再次运行该应用程序。 您应该开始看到每个加载失败的图像都会出现一个愤怒而沮丧的小外星人。
除了添加失败的图像之外,您还应该知道代码更小,更干净,这让您感到满意!
3. Using ImagePipeline to Load Images
好的,您需要解决另一个问题。
当前,当您在图库中点击图像时,要在详细视图中显示的图像将在主线程上获取。 如您所知,这阻止了UI
响应输入。
如果您的互联网速度足够快,那么几张图像可能不会引起任何问题。 但是,滚动到画廊的最底部。 检出鹰状星云的图像,它是中间图像,从底部开始的第三行:
完整图片大约60 MB
! 如果单击它,您将注意到UI冻结。
要解决此问题,您将使用-等待-Nuke
。 但是,您将不会使用loadImage(with:into :)
。 相反,您将使用其他方法来理解使用Nuke
的不同方法。
打开PhotoViewController.swift
。 在文件顶部导入Nuke
。
import Nuke
在viewDidLoad()
中找到以下代码
if let imageData = try? Data(contentsOf: imageURL),
let image = UIImage(data: imageData) {
imageView.image = image
}
这与您之前看到的相同的不成熟的图像加载。 将其替换为以下代码:
// 1
imageView.image = ImageLoadingOptions.shared.placeholder
imageView.contentMode = .scaleAspectFit
// 2
ImagePipeline.shared.loadImage(
// 3
with: imageURL) { [weak self] response in // 4
guard let self = self else {
return
}
// 5
switch response {
// 6
case .failure:
self.imageView.image = ImageLoadingOptions.shared.failureImage
self.imageView.contentMode = .scaleAspectFit
// 7
case let .success(imageResponse):
self.imageView.image = imageResponse.image
self.imageView.contentMode = .scaleAspectFill
}
}
在此代码中,您:
- 1) 设置占位符图像和内容模式。
- 2) 在
ImagePipeline
单例上调用loadImage(with :)
。 - 3) 传递适当的照片URL。
- 4) 提供完成处理程序
(completion handler)
。 处理程序具有一个枚举类型Result
的参数。 - 5)
response
只能有两个值:.success
的关联值类型为ImageResponse
,或.failure
的关联值类型为Error
。 因此,switch
语句将最好地检查两个可能的值。 - 6) 在失败的情况下,将图像设置为适当的失败图像。
- 7) 为了获得成功,请将图像设置为下载的照片。
是时候了。 构建并运行,然后再次点击Eagle Nebula
照片。
不再冻结UI! 做得好。
4. Caching Images
Nuke
具有一项很酷的功能,可让您主动缓存图像。 这是什么意思? 好吧,这意味着它将忽略可能在HTTP headers
中找到的Cache-Control
指令。
但是,为什么要这样做呢? 有时,您知道数据不会更改。 在网络上找到的图片通常是这种情况。 并非总是如此,但经常如此。
如果您的应用使用的是您不应更改的图像,则最好将Nuke
设置为使用积极的图像缓存,而不必冒险重新加载相同的图像。
在PhotoGalleryViewController.swift
中,返回到``viewDidLoad()`并将此代码添加到末尾:
// 1
DataLoader.sharedUrlCache.diskCapacity = 0
let pipeline = ImagePipeline {
// 2
let dataCache = try? DataCache(name: "com.raywenderlich.Far-Out-Photos.datacache")
// 3
dataCache?.sizeLimit = 200 * 1024 * 1024
// 4
$0.dataCache = dataCache
}
// 5
ImagePipeline.shared = pipeline
在这里,您:
- 1) 通过将默认容量设置为零来禁用默认磁盘缓存。 您不想意外地将图像缓存两次。 您将创建自己的缓存。
- 2) 创建一个新的数据缓存。
- 3) 将缓存大小限制设置为
200 MB
(因为每千字节有1,024字节,每兆字节有1,024千字节)。 - 4) 配置
ImagePipeline
以使用新创建的DataCache
。 - 5) 将此图像管道设置为未指定任何管道时使用的默认管道。
就这些!
如果您现在构建并运行代码,则不会有什么不同。 但是,在后台,您的应用程序将所有图像缓存到磁盘,并使用缓存加载其可以加载的所有内容。
注意:如果您要手动清除缓存,这很简单,但并直接。 由于事物在
ImagePipeline
中的内部存储方式,您需要进行一些类型转换:if let dataCache = ImagePipeline.shared .configuration.dataCache as? DataCache { dataCache.removeAll() }
Combining with Combine
Nuke
通过其扩展extensions支持与其他框架的集成。 在下一部分中,您将学习如何将Nuke
与Combine
框架结合使用。
Combine is a framework是Apple与iOS 13一起发布的框架,可通过发布者-订阅者结构提供声明性API。 它极大地简化了异步事件的处理,并允许您组合事件处理操作。
1. Setting up ImagePublisher
您需要安装一个名为ImagePublisher
的Nuke
扩展。 使用Swift Package Manager
以与之前对Nuke相同的方式进行安装。
从URL
添加一个新包:https://github.com/kean/ImagePublisher.git
。
注意:在编写本教程时,可用的版本是
0.2.1
。
在开始使用新的扩展程序之前,请注意应用程序中当前发生的情况。
首先,图库会下载所有图像,并将图像的调整大小版本存储在其缓存中。其次,当您打开任何照片时,整个图像开始下载,并且在这种情况下,您会看到一个占位符图像。
到目前为止,这起了很大的作用,但这并不是您想要提供给用户的最佳体验。尽管已存储了调整大小后的图像,但您正在显示通用的占位符图像。理想情况下,您要显示该照片,直到下载完整图像。
最好的方法是请求调整大小的图像,如果图像已经被缓存,则几乎可以立即获得。然后,在完成该请求之后,立即获取完整的请求。换句话说:您想链接两个请求。
但是首先,使用ImagePublisher
根据基本要求测试您的技能。
2. Using ImagePublisher
在PhotoViewController.swift
中,在文件顶部添加以下导入:
import Combine
import ImagePublisher
然后在类的顶部添加以下属性。
//1
var cancellable: AnyCancellable?
//2
var resizedImageProcessors: [ImageProcessing] = []
- 1) 简单来说,
cancellable
是可取消操作的句柄(顾名思义,cancellable)。 您将使用它来引用您要创建的用于下载图像的请求操作。 - 2)
resizedImageProcessors
就像您为重新调整大小的图像生成请求时在PhotoGalleryViewController
中使用的一样。 您需要PhotoGalleryViewController
,因为它要打开一个新的PhotoViewController
以提供相同的处理器,以便您可以发出相同的请求。 如果请求不同,则将下载一张新照片,但您希望在图库中显示同一张照片,以便您尽可能从缓存中获取它。
在类结束时,添加新方法
func loadImage(url: URL) {
// 1
let resizedImageRequest = ImageRequest(
url: url,
processors: resizedImageProcessors)
// 2
let resizedImagePublisher = ImagePipeline.shared
.imagePublisher(with: resizedImageRequest)
// 3
cancellable = resizedImagePublisher
.sink(
// 4
receiveCompletion: { [weak self] response in
guard let self = self else { return }
switch response {
case .failure:
self.imageView.image = ImageLoadingOptions.shared.failureImage
self.imageView.contentMode = .scaleAspectFit
case .finished:
break
}
},
// 5
receiveValue: {
self.imageView.image = $0.image
self.imageView.contentMode = .scaleAspectFill
}
)
}
这是这样做的:
- 1) 使用先前定义的处理器,使用提供的图像URL创建一个新请求。
- 2) 创建一个新的发布者
(publisher)
以处理此请求。发布者实际上是完成或失败的值流。对于此发布者,它将处理请求,然后返回图像或失败。 - 3) 执行此发布者,或在
Combine lingo
中执行以下操作:在具有两个闭包的此发布者上,使用sink(receiveCompletion:receiveValue:)
创建一个subscriber
。 - 4) 第一个闭包提供了发布者的结果,无论发布者设法正常完成操作还是失败。如果发生失败,您将使用
aspect fit
内容模式显示故障图像。如果它正常完成,那么您在这里什么也不做,因为您将在第二个闭包中收到该值。 - 5) 显示您用
aspect fill
正常收到的值。要知道,如果第一个闭包响应失败,您将不会收到任何值。
在viewDidLoad()
中,删除以下所有负责加载图像的代码:
ImagePipeline.shared.loadImage(
with: imageURL) { [weak self] response in // 4
guard let self = self else {
return
}
switch response {
case .failure:
self.imageView.image = ImageLoadingOptions.shared.failureImage
self.imageView.contentMode = .scaleAspectFit
case let .success(imageResponse):
self.imageView.image = imageResponse.image
self.imageView.contentMode = .scaleAspectFill
}
}
并将其替换为对新添加的方法的调用:
loadImage(url: imageURL)
现在转到PhotoGalleryViewController.swift
并找到collectionView(_:didSelectItemAt :)
。 在将视图控制器推入导航堆栈之前,添加以下行:
photoViewController.resizedImageProcessors = resizedImageProcessors
在模拟器上,长按应用程序图标完全卸载该应用程序,然后选择Delete App
。 这会完全删除缓存的图像以及与该应用程序相关的所有其他内容,因此您可以重新开始。
构建并运行。 点按任何已完成加载的图像,然后看它会立即显示而不显示加载占位符。 但是图像不会像以前那样清晰。 这是因为您正在从图库中加载相同尺寸的图像,而Nuke
则将来自缓存的第二个请求提供给您。
您尚未完成。 您也想加载完整尺寸的图像。
3. Chaining Requests
到目前为止,您已经定义了一个发布者,但是您想使用两个发布者(每个图像请求一个发布者)并将两个链接起来以依次执行它们。
在loadImage(url :)
中的PhotoViewController.swift
中,在定义了resizedImagePublisher
之后,立即添加此行。
let originalImagePublisher = ImagePipeline.shared.imagePublisher(with: url)
这将直接使用图像URL
创建发布者,而无需任何图像处理器。
接下来,替换:
cancellable = resizedImagePublisher
.sink(
使用
cancellable = resizedImagePublisher.append(originalImagePublisher)
.sink(
在发布者上添加.append(:)
会创建一个新的发布者,它将resizeImagePublisher
和originalImagePublisher
结合在一起。 因此,您可以以相同的方式对待它,并且在内部它将使每个发布者都能正常工作并完成工作,然后再转到下一个发布者。
构建并运行。 点按图库中的任何图像,然后看到它以图像开头,然后在您眼前显示更清晰的图像。
注意:如果看不到过渡效果,请在
PhotoGalleryViewController.resizedImageProcessors
中减小图库中要求的图像尺寸,以降低第一张图像的质量。
4. Cleaning the Subscriber
早先您了解到,在subscriber
中,一旦publisher
完成或失败,您将得到一次通知,而发布者的结果将被通知,并且只有当发布者完成时,您才会获得带有值的通知。
这意味着您正在两个不同的地方进行响应,尽管您所做的只是显示不同的图像和不同的内容模式。
为什么不让发布者只提供要使用的图像和内容模式,那么您要做的就是仅显示这些值。更准确地说:即使发布失败,也要使发布者提供价值。
您正在使用的发布者返回的对象具有图片,但与内容模式无关。那是这个程序的独特之处。
因此,您要做的第一件事就是告诉发布者提供不同的值类型,而不是ImageResponse
,后者是ImagePublisher
中定义的类型。您要使用元组(Image,UIView.ContentMode)
回到PhotoViewController
。在调用.sink(receiveCompletion:receiveValue :)
之前,添加以下内容:
.map {
($0.image, UIView.ContentMode.scaleAspectFill)
}
map(_ :)
为您进行转换。 您正在使用从发布者处收到的原始值,并将此值转换为元组。 并且由于有一个值,这意味着发布者成功了,所有NASA
图像都应将scaleAspectFill
作为其内容模式。
其次,如果发生会导致发布者失败的错误,您想中断发布者。 相反,让发布者将失败的图像和aspect fit
放到元组中。
定义originalImagePublisher
之后,添加:
guard let failedImage = ImageLoadingOptions.shared.failureImage else {
return
}
然后,在map(_ :)
的右花括号之后以及对.sink(receiveCompletion:receiveValue :)
的调用之前,添加:
.catch { _ in
Just((failedImage, .scaleAspectFit))
}
发布者在失败时调用.catch(_ :)
。 对Just(_ :)
的调用创建了一个发布者,该发布者总是发送给定的值,然后完成。 在这种情况下,这是您在全局ImageLoadingOptions
中指定的默认失败图像。 catch(_ :)
表示将“捕获”来自图像发布者的任何错误,然后将失败替换为故障图像。
最后,将.sink(receiveCompletion:receiveValue :)
的所有代码替换为:
.sink {
self.imageView.image = $0
self.imageView.contentMode = $1
}
您的subscriber
将始终获得值,因为您设法将发布者转变为永不失败的发布者。 现在,它提供的结果类型为您的应用程序提供了更多便利。 因此,自然不需要检查其是否成功的代码,而您所需要做的就是“仅”接收值。
构建并运行该应用程序。 它会像以前一样工作,但是现在您有了一些更简单的Combine
代码!
您还可以在Nuke
中探索更多功能。 例如,Nuke
支持动画GIF。 如果您的应用需要该级别的控制,则还可以更深入地了解缓存管理主题。
此外,一些插件可以扩展Nuke
的功能,例如对SwiftUI , RxSwift, WebP和Alamofire的支持。
而且,您随时可以从 Combine: Getting Started中了解有关Combine
的更多信息。
后记
本篇主要讲述了Swift中的三方框架
Nuke
的简单使用示例,感兴趣的给个赞或者关注~~~