iOS - 异步请求和 UI 交互
iOS - Asynchronous requests and UI interactions
我有一个页面使用 Alamofire 从 Google 图像搜索 API 检索图像并将它们显示在 collectionView 中。一切正常,但当请求仍在加载时,我根本无法与 UI 交互,无论是单击按钮还是滚动 collectionView。这是正常行为吗?我希望用户至少能够在请求花费很长时间时按下“后退”按钮,或者在处理新请求时上下滚动 collectionView。
Alamofire.request(.GET, googleImageApiUrl, parameters: [:]).responseJSON { (request, response, JSON, error) in
if let results = JSON as? NSDictionary {
if let images = results["responseData"]!["results"] as? NSArray {
for image in images {
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
self.imageData.append(data)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
}
}
}
}
总而言之,一旦发起请求,在 collectionView 重新加载其数据之前,用户无法以任何方式与页面交互。我想如果请求是异步的,那么我仍然可以与 UI 进行交互。这是 Alamofire 限制,一般限制,还是我做错了什么?
Alamofire 异步网络。不用担心。不过,问题可能出在这里:
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
如果正如我所怀疑的那样,URL 是一个远程 URL,那么您现在正在重复 同步 网络。 NSData(contentsOfURL:)
不 适合在主线程上通过网络获取内容。如果您要在网络上获取更多数据,请使用适当的网络代码 - 甚至可以使用更多的 Alamofire...
尝试下面的方法可能会解决您的问题,它可以将请求发送到具有高优先级的后台队列,使其尽可能快而无需锁定 UI
Alamofire.request(.GET, googleImageApiUrl, parameters: [:]).responseJSON { (request, response, JSON, error) in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
if let results = JSON as? NSDictionary {
if let images = results["responseData"]!["results"] as? NSArray {
for image in images {
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
self.imageData.append(data)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
}
}
}
}
}
我相信您现在知道,主队列的内部 dispatch_async
是多余的,因为您已经在主线程上了。而且,正如其他人所指出的那样,您绝对不想在此块内执行 NSData(contentsOfURL:...)
操作,而是也异步请求这些图像。正如 Matt 指出的那样 (+1),Alamofire 触手可及,所以请使用它。
但是这里有一个更深层次的问题:你在前面加载图像!你不应该那样做。您也不应该将它们加载到数组中。你应该改为:
延迟加载:您应该让每个单元格仅在需要单元格时才异步请求其自己的图像,而不是尝试提前执行此操作。这意味着让 cellForItemAtIndexPath
请求这些单元格的单个图像,而不是让 viewDidLoad
加载它们。
缓存: 而不是将图像保存在数组中(在这种情况下,您很容易 运行 进入内存问题),您应该保存他们在 NSCache
或类似的东西中。这样,如果您在遇到内存压力时必须清除它们,您可以安全地这样做,但保持应用程序 运行ning 顺利。
除非你的图像都 (a) 非常小; (b) 数量不多,您需要一个模型来支持您的集合视图,假设您可能无法同时将所有图像保存在内存中。
优先显示可见单元格的图像: 如果重复使用某个单元格,您应该取消之前对该单元格的请求。否则,如果您快速滚动浏览集合视图,对可见单元格的请求将积压在对早已滚出视图的单元格的请求之后。在慢速网络连接上,这成为一个关键的改进。
防止超时:确保防止请求超时。如果你注意了前面的一点,这个问题会有所减少,但特别是在集合视图上,它仍然是一个真正的风险。您可以增加网络请求的超时时间,或者更好的方法是将请求包装在异步 NSOperation
对象中,然后使用 NSOperationQueue
来管理请求,其中的 maxConcurrentOperationCount
是合理的,例如 4或 5.
预热: 之前建议懒加载。实际上,如果你想变得非常花哨,你可以将可见单元格的延迟加载与可见单元格附近的单元格的“预热”(或预取或急切获取)结合起来,以便在用户滚动时它们准备就绪。
这是一个相当复杂的概念,因此这是您此时最不应该担心的事情。但是,如果您对这个概念感兴趣,请参阅 WWDC 2014 视频 Introducing the Photos Frameworks,了解复杂的预热机制可能是什么样子的示例。 (请注意,我并不是建议您使用 Photos.framework,但这是一个聪明的抓取系统的示例。)
在我上面的许多观察中隐含的是,我们需要针对真实场景优化我们的应用程序。我们使用高速 wifi 连接进行大部分测试,有时会忘记在现实世界中,用户正在处理时断时续的蜂窝连接。因此,我建议 运行 带有网络调节器的应用 link 模拟非常弱的蜂窝连接。只有当你这样做时,上面关于延迟加载、优先显示单元格和超时的重要性才会得到明显的体现。
顺便说一句,如果您不想在其中一些问题中迷失方向,您可以考虑使用专为异步图像检索设计的 UIImageView
类别,例如 AlamofireImage.它最大限度地减少了您必须编写的代码量。
我有一个页面使用 Alamofire 从 Google 图像搜索 API 检索图像并将它们显示在 collectionView 中。一切正常,但当请求仍在加载时,我根本无法与 UI 交互,无论是单击按钮还是滚动 collectionView。这是正常行为吗?我希望用户至少能够在请求花费很长时间时按下“后退”按钮,或者在处理新请求时上下滚动 collectionView。
Alamofire.request(.GET, googleImageApiUrl, parameters: [:]).responseJSON { (request, response, JSON, error) in
if let results = JSON as? NSDictionary {
if let images = results["responseData"]!["results"] as? NSArray {
for image in images {
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
self.imageData.append(data)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
}
}
}
}
总而言之,一旦发起请求,在 collectionView 重新加载其数据之前,用户无法以任何方式与页面交互。我想如果请求是异步的,那么我仍然可以与 UI 进行交互。这是 Alamofire 限制,一般限制,还是我做错了什么?
Alamofire 异步网络。不用担心。不过,问题可能出在这里:
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
如果正如我所怀疑的那样,URL 是一个远程 URL,那么您现在正在重复 同步 网络。 NSData(contentsOfURL:)
不 适合在主线程上通过网络获取内容。如果您要在网络上获取更多数据,请使用适当的网络代码 - 甚至可以使用更多的 Alamofire...
尝试下面的方法可能会解决您的问题,它可以将请求发送到具有高优先级的后台队列,使其尽可能快而无需锁定 UI
Alamofire.request(.GET, googleImageApiUrl, parameters: [:]).responseJSON { (request, response, JSON, error) in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
if let results = JSON as? NSDictionary {
if let images = results["responseData"]!["results"] as? NSArray {
for image in images {
let url = NSURL(string: image["unescapedUrl"] as! String)!
if let data = NSData(contentsOfURL: url) {
self.imageData.append(data)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
}
}
}
}
}
我相信您现在知道,主队列的内部 dispatch_async
是多余的,因为您已经在主线程上了。而且,正如其他人所指出的那样,您绝对不想在此块内执行 NSData(contentsOfURL:...)
操作,而是也异步请求这些图像。正如 Matt 指出的那样 (+1),Alamofire 触手可及,所以请使用它。
但是这里有一个更深层次的问题:你在前面加载图像!你不应该那样做。您也不应该将它们加载到数组中。你应该改为:
延迟加载:您应该让每个单元格仅在需要单元格时才异步请求其自己的图像,而不是尝试提前执行此操作。这意味着让
cellForItemAtIndexPath
请求这些单元格的单个图像,而不是让viewDidLoad
加载它们。缓存: 而不是将图像保存在数组中(在这种情况下,您很容易 运行 进入内存问题),您应该保存他们在
NSCache
或类似的东西中。这样,如果您在遇到内存压力时必须清除它们,您可以安全地这样做,但保持应用程序 运行ning 顺利。除非你的图像都 (a) 非常小; (b) 数量不多,您需要一个模型来支持您的集合视图,假设您可能无法同时将所有图像保存在内存中。
优先显示可见单元格的图像: 如果重复使用某个单元格,您应该取消之前对该单元格的请求。否则,如果您快速滚动浏览集合视图,对可见单元格的请求将积压在对早已滚出视图的单元格的请求之后。在慢速网络连接上,这成为一个关键的改进。
防止超时:确保防止请求超时。如果你注意了前面的一点,这个问题会有所减少,但特别是在集合视图上,它仍然是一个真正的风险。您可以增加网络请求的超时时间,或者更好的方法是将请求包装在异步
NSOperation
对象中,然后使用NSOperationQueue
来管理请求,其中的maxConcurrentOperationCount
是合理的,例如 4或 5.预热: 之前建议懒加载。实际上,如果你想变得非常花哨,你可以将可见单元格的延迟加载与可见单元格附近的单元格的“预热”(或预取或急切获取)结合起来,以便在用户滚动时它们准备就绪。
这是一个相当复杂的概念,因此这是您此时最不应该担心的事情。但是,如果您对这个概念感兴趣,请参阅 WWDC 2014 视频 Introducing the Photos Frameworks,了解复杂的预热机制可能是什么样子的示例。 (请注意,我并不是建议您使用 Photos.framework,但这是一个聪明的抓取系统的示例。)
在我上面的许多观察中隐含的是,我们需要针对真实场景优化我们的应用程序。我们使用高速 wifi 连接进行大部分测试,有时会忘记在现实世界中,用户正在处理时断时续的蜂窝连接。因此,我建议 运行 带有网络调节器的应用 link 模拟非常弱的蜂窝连接。只有当你这样做时,上面关于延迟加载、优先显示单元格和超时的重要性才会得到明显的体现。
顺便说一句,如果您不想在其中一些问题中迷失方向,您可以考虑使用专为异步图像检索设计的 UIImageView
类别,例如 AlamofireImage.它最大限度地减少了您必须编写的代码量。