版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.12.30 星期日 |
前言
OC对有一个很棒的网络图片加载控件SDWebImage,那么Swift中呢,如果要加载远程网络图片呢?这里接下来几篇就给大家介绍一个Swift中用于加载网络图片的框架 —— Nuke。感兴趣的可以看下面几篇文章。
1. Nuke框架详细解析(一) —— 基本概览(一)
开始
首先看一下写作环境
Swift 4.2, iOS 12, Xcode 10
在这个Nuke教程中,您将学习如何使用CocoaPods将Nuke集成到项目中并加载远程图像,同时保持应用程序的响应能力。
每年,最新的iPhone相机变得越来越好。但是你知道谁拥有最好的相机吗?
NASA!
他们拍摄的最佳主题是什么?再说一遍,除非你为美国宇航局工作,否则不幸的是答案不是你。
SPACE!
有一个显示NASA漂亮照片的应用程序会很好,所以你可以随时在手机上查看它们。唯一的问题是这些空间照片中的一些是巨大的 - 像60MB巨大。最重要的是,它们没有可用的缩略图。
您已经害怕必须编写与OperationQueues
相关的大量样板代码。如果有更好的方式呢。
好消息!有一种名为Nuke
的第三方库形式的更好方法。
在这个Nuke教程中,您将学习如何:
- 使用CocoaPods将Nuke集成到您的项目中。
- 加载远程图像,同时保持应用程序的响应能力。
- 动态调整照片大小以控制内存使用。
准备好在iPhone上舒适的探索空间了吗?
打开入门项目并进行一些探索。
Far Out Photos
应用程序从NASA的网站上抓取照片并将其显示在基于UICollectionView
的图库中。 点击其中一张漂亮的照片就可以全屏显示。
不幸的是,这个应用程序的编写方式存在一个主要问题。 它缓慢且反应迟钝。 这是因为所有网络图像请求都在主线程上完成 - 您知道,UI应该刷新的主线程。 此外,没有缓存。
你将使用Nuke
修复这个应用程序,让它像Europa
表面一样平稳运行。 或者甚至可能像冥王星上的心脏一样平滑。
Setting Up Nuke
您可以将Nuke集成到项目中,但是对于这个Nuke教程,您将使用CocoaPods。 使用CocoaPods的一种非常简单的方法是下载app。
下载完成后,启动CocoaPods应用程序。 你应该看到这样的东西:
接下来,按Command-N。 在弹出的Open
面板中,导航到包含您下载的教程材料的目录,然后从starter目录中选择Far Out Photos.xcodeproj
。
CocoaPods应用程序将在项目目录中自动创建Podfile
,然后使用内置编辑器打开它。
在end
关键字之前添加以下行:
pod 'Nuke', '~> 7.0'
你的Podfile
应该如下所示:
project 'Far Out Photos.xcodeproj'
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'Far Out Photos' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for Far Out Photos
pod 'Nuke', '~> 7.0'
end
此代码将指示CocoaPods使用最新版本的Nuke,直到下一个主要版本(在本例中为8.0)。
现在,按右上角的Install
按钮,等待CocoaPods做它的事情。
当你看到一条消息Pod installation [is] complete!
时,你就会知道它已经完成了!
如果您仍然打开Far Out Photos.xcodeproj
,则需要关闭它,然后从现在开启Far Out Photos.xcworkspace
。 CocoaPods使用Xcode工作区将第三方项目添加到您自己的项目中。
完成后,是时候深入了解Nuke。
Basic Nuke Usage
你要解决的第一件事是当滚动浏览图库时应用程序的可怕响应。
这个Nuke教程中的大部分工作都将集中在PhotoGalleryViewController.swift
上。 继续打开它,并将以下内容添加到文件的顶部:
import Nuke
如果您打算使用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中提取照片,并将其分配给集合视图单元格中的图像,从而阻止进程中的主线程。
删除这七行并用以下内容替换它们:
// 1
let url = photoURLs[indexPath.row]
// 2
Nuke.loadImage(with: url, into: cell.imageView)
在此代码中,您:
- 1) 根据单元格的索引路径获取正确照片的URL。
- 2) 使用
Nuke
将URL中的图像直接加载到单元格的图像视图中。
这很容易!
构建并运行应用程序,您应该注意到一个重大改进。 您现在可以平滑地滚动图库视图。
1. Image Loading Options
这是一个很好的开始,但有一些非常重要的东西仍然缺失。 该应用程序不会向用户提供图像即将到来的视觉反馈。 相反,如果用户滚动得足够快,他们只会看到一个几乎纯黑色的屏幕。
要是Nuke有某种方式来显示图像来代替加载图像那就好了 - 如果你愿意,可以使用占位符。
今天是你的幸运日!
Nuke有一个名为ImageLoadingOptions
的结构,它允许您更改图像的显示方式,包括设置占位符图像。
回到collectionView(_:cellForItemAt :)
函数,用以下代码替换刚写的代码:
// This line doesn't change
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
结构,设置占位符图像并配置从占位符图像到从网络获取的图像的转换。 在这种情况下,您将过渡设置为0.5秒以上的淡入。 - 2) 使用Nuke使用刚刚配置的选项将URL中的图像直接加载到单元格的图像视图中。
现在,当您构建并运行应用程序时,您应该看到占位符图像淡入淡出以显示加载时的真实图像。
太棒了! 只需几行代码,您就已经大大改进了应用程序!
2. What About Memory?
到目前为止,该应用程序运行的怎么样? 你遇到过任何crash吗?
如果您已经在设备上运行此项目,那么您很可能遇到一两次崩溃。 为什么? 因为这个应用程序是一个memory hog
。
要查看它有多糟糕,请再次运行该应用,然后执行以下操作:
- 1) 在
Navigator
面板中选择Debug navigator
。 - 2) 在调试量表列表下选择
Memory
。 - 3) 单击
Profile in Instruments
- 4) 最后,在出现的对话框中单击
Restart
。
运行profiler
后,将图库滚动到底部,特别注意标记为VM: CG raster data
的行的Persistent Bytes
列。 它保存了超过1千兆字节的数据!
问题的根源在于,即使下载的图像在屏幕上看起来很小,它们仍然是全尺寸图像并完全存储在存储器中。 这不好。
不幸的是,NASA没有为其图像提供缩略图大小。
该怎么办?
也许Nuke可以帮忙吗?
是。 Nuke可以提供帮助!
到目前为止,您已经将loadImage
传递给了URL。 这些方法还可以选择接受ImageRequest
。
ImageRequest
可以定义目标图像大小,Nuke会在将图像分配给图像视图之前自动调整下载图像的大小。
返回到collectionView(_:cellForItemAt :)
函数,用以下代码替换对Nuke.loadImage(_:_:_ :)
的调用:
// 1
let request = ImageRequest(
url: url,
targetSize: CGSize(width: pixelSize, height: pixelSize),
contentMode: .aspectFill)
// 2
Nuke.loadImage(with: request, options: options, into: cell.imageView)
使用此代码,您:
- 1) 为所需的图像URL创建
ImageRequest
,目标大小为pixelSize x pixelSize
,内容模式设置为填充整个区域,同时保持原始高宽比。 - 2) 让Nuke使用您先前在单元格图像视图中设置的选项,根据此图像请求加载图像。
什么是pixelSize
?
Nuke根据像素而非点来调整图像大小。 在类的顶部,就在cellSize
属性的定义之下,添加以下计算属性:
var pixelSize: CGFloat {
get {
return cellSize * UIScreen.main.scale
}
}
此计算属性采用以点为单位的cellSize
属性,并将其乘以设备的scale
,从而为您提供像素。
现在,再次构建并运行应用程序,并以与之前相同的方式打开内存分析器。
哇!VM: CG raster data
现在低于30MB! 您不太可能遇到超过30MB的内存使用量崩溃。
Advanced Nuking
1. Optimizing Code
目前,对于每个collection view cell
,您将重新创建相同的ImageLoadingOptions
。 这不是高效的一种做法。
修复此问题的一种方法是为您正在使用的选项创建一个常量类属性,并每次将其传递给Nuke的loadImage(_:_:_)
函数。
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
:成功,失败和占位符。 - 2) 设置默认占位符图像。
- 3) 设置默认图像以在出现错误时显示。
- 4) 定义从占位符到另一个图像的默认转换。
完成后,返回到collectionView(_:cellForItemAt :)
函数。 在这里,你需要做两件事。
首先,删除以下行:
let options = ImageLoadingOptions(
placeholder: UIImage(named: "dark-moon"),
transition: .fadeIn(duration: 0.5)
)
您将不再需要它,因为您定义了默认选项。 然后,您需要将对loadImage(_:_:_ :)
的调用更改为如下所示:
Nuke.loadImage(with: request, into: cell.imageView)
如果您现在构建并运行代码,您可能不会看到太多差异,但您确实在改进代码的同时隐藏了一项新功能。
关闭Wi-Fi并再次运行应用程序。 您应该开始看到每个无法加载的图像出现愤怒和沮丧的小外星人。
除了添加故障图像外,您应该感觉内容知道您的代码更小,更清洁,更高效!
2. Loading images without a view
好的,你需要解决另一个问题。
目前,当您点击图库中的图像时,图像将在主线程上获取。 正如您所知,这会阻止UI刷新。
如果您的互联网足够快,您可能不会注意到一些图像的任何问题。 但是,滚动到图库的最底部。 看看Eagle Nebula
的图像,这是中间的图像,从底部第三行:
全尺寸图片大约60 MB! 如果你点击它,你肯定会注意到你的UI卡死了。
要解决这个问题,你将要使用 - 等待它 - Nuke。 但是,您不会使用loadImage(_:_ :)
。 要理解为什么这个方法不起作用,请看一下collectionView(_:didSelectItemAt :)
中的代码。
guard let photoViewController = PhotoViewController.instantiate() else {
return
}
...
navigationController?.pushViewController(photoViewController, animated: true)
detail view controller
被实例化,然后被推送到导航堆栈。 如果您尝试将PhotoViewController
的image view
属性传递给loadImage(_:_:_ :)
,应用程序将崩溃。 调用时image view
属性不存在。
为了解决这个问题,Nuke可以在不需要图像视图的情况下请求图像。 相反,您可以定义图像加载时的行为方式,以及完成后会发生什么。
在PhotoGalleryViewController.swift
的底部,找到collectionView(_:didSelectItemAt :)
并删除guard
语句后的所有内容。
现在,在其位置添加以下代码:
// 1
photoViewController.image = ImageLoadingOptions.shared.placeholder
photoViewController.contentMode = .scaleAspectFit
// 2
ImagePipeline.shared.loadImage(
// 3
with: photoURLs[indexPath.row],
// 4
progress: nil) { response, err in // 5
if err != nil {
// 6
photoViewController.image = ImageLoadingOptions.shared.failureImage
photoViewController.contentMode = .scaleAspectFit
} else {
// 7
photoViewController.image = response?.image
photoViewController.contentMode = .scaleAspectFill
}
}
// 8
navigationController?.pushViewController(photoViewController, animated: true)
在此代码中,您:
- 1) 设置占位符图像和
content mode
。 - 2) 在
ImagePipeline
单例上调用loadImage(_:_:_ :)
。 - 3) 传递相应的照片网址。
- 4) 将进度处理程序设置为nil。
- 5) 创建完成处理程序。
- 6) 如果出现错误,请将图像设置为相应的失败图像。
- 7) 如果一切都成功,将图像设置为下载的照片。
- 8) 最后,将
photoViewController
推送到导航堆栈。
在构建和运行应用程序之前,您需要处理这些新添加的代码引入的一些错误。 PhotoViewController
没有contentMode
属性。
打开PhotoViewController.swift
并找到image
属性的声明。 删除它并用以下代码替换它:
var image: UIImage? {
didSet {
imageView?.image = image
}
}
var contentMode: UIView.ContentMode = .scaleAspectFill {
didSet {
imageView?.contentMode = contentMode
}
}
此代码允许您在加载所有视图之前更改创建后的PhotoViewController
的UIImage
和UIView.ContentMode
。
最后,在同一个文件中,将以下行添加到viewDidLoad(_ :)
的末尾:
imageView?.contentMode = contentMode
这确保了如果在加载视图之前设置了contentMode
,它仍将被使用。
是时候了。 构建并运行应用程序,再次点击Eagle Nebula
照片。
现在就没有UI卡死的现象了。
3. Caching Images
Nuke有一个很酷的功能,可以让你积极地缓存图像。 这是什么意思? 嗯,这意味着它将忽略可能在HTTP头中找到的Cache-Control
指令。
但是你为什么要这样做呢? 有时,您知道数据不会改变。 对于在网络上找到的图像,通常就是这种情况。 不总是,但经常。
如果您的应用正在使用您知道不应该更改的图像,则可以将Nuke设置为使用积极的图像缓存,而不是冒着重新加载相同图像的风险。
在PhotoGalleryViewController.swift
中,返回viewDidLoad(_ :)
并将此代码添加到结尾:
// 1
DataLoader.sharedUrlCache.diskCapacity = 0
let pipeline = ImagePipeline {
// 2
let dataCache = try! DataCache(name: "com.razeware.Far-Out-Photos.datacache")
// 3
dataCache.sizeLimit = 200 * 1024 * 1024
// 4
$0.dataCache = dataCache
}
// 5
ImagePipeline.shared = pipeline
在这里,您:
- 1) 通过将其容量设置为零来禁用默认磁盘缓存。 您不希望意外地将图像缓存两次!
- 2) 创建一个新的数据缓存。
- 3) 将缓存大小限制设置为200 MB(因为每千字节有1024个字节,每兆字节有1024千字节)。
- 4) 配置
ImagePipeline
以使用新创建的DataCache
。 - 5) 将此
ImagePipeline
设置为未指定时使用的默认管道。
这就是所有的操作!
如果您现在构建并运行代码,您将看不出有什么区别。 然而,在幕后,您的应用程序将所有图像缓存到磁盘并使用缓存加载任何可能的内容。
注意:如果要手动清除缓存,则很简单,但并不那么直接。 由于内部存储在
ImagePipeline
中的方式,您需要进行一些类型转换。if let dataCache = ImagePipeline.shared .configuration.dataCache as? DataCache { dataCache.removeAll() }
恭喜你做到这一点! 您已经学到了很多关于如何使用Nuke来改善应用程序性能的知识!
您还可以探索Nuke的更多功能。 例如,Nuke支持逐行图像加载和动画GIF。 如果您的应用需要该级别的控制,您还可以深入了解缓存管理主题。
此外,有几个插件可以扩展Nuke的功能,例如支持RxSwift,WebP和 Alamofire。
后记
本篇主要讲述了基于Nuke使用的一个简单示例,感兴趣的给个赞或者关注~~~