应用程序生命周期中的 Alamofire 请求阻塞
Posted
技术标签:
【中文标题】应用程序生命周期中的 Alamofire 请求阻塞【英文标题】:Alamofire request blocking during application lifecycle 【发布时间】:2017-04-10 15:17:34 【问题描述】:我在使用 Operation
和 OperationQueue
时遇到了 Alamofire 问题。
我有一个名为NetworkingQueue
的OperationQueue
,我将一些操作(包装AlamofireRequest)推入其中,一切正常,但在应用程序运行期间,有一段时间没有发送所有Alamofire 请求。我的队列越来越大,没有请求到最后。
我没有计划随时重现它。
有没有人能帮帮我?
这是一个代码示例
BackgroundAlamoSession
let configuration = URLSessionConfiguration.background(withIdentifier: "[...].background")
self.networkingSessionManager = Alamofire.SessionManager(configuration: configuration)
AbstractOperation.swift
import UIKit
import XCGLogger
class AbstractOperation:Operation
private let _LOGGER:XCGLogger = XCGLogger.default
enum State:String
case Ready = "ready"
case Executing = "executing"
case Finished = "finished"
var keyPath: String
get
return "is" + self.rawValue.capitalized
override var isAsynchronous:Bool
get
return true
var state = State.Ready
willSet
willChangeValue(forKey: self.state.rawValue)
willChangeValue(forKey: self.state.keyPath)
willChangeValue(forKey: newValue.rawValue)
willChangeValue(forKey: newValue.keyPath)
didSet
didChangeValue(forKey: oldValue.rawValue)
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: self.state.rawValue)
didChangeValue(forKey: self.state.keyPath)
override var isExecuting: Bool
return state == .Executing
override var isFinished:Bool
return state == .Finished
一个具体的操作实现
import UIKit
import XCGLogger
import SwiftyJSON
class FetchObject: AbstractOperation
public let _LOGGER:XCGLogger = XCGLogger.default
private let _objectId:Int
private let _force:Bool
public var object:ObjectModel?
init(_ objectId:Int, force:Bool)
self._objectId = objectId
self._force = force
convenience init(_ objectId:Int)
self.init(objectId, force:false)
override var desc:String
get
return "FetchObject(\(self._objectId))"
public override func start()
self.state = .Executing
_LOGGER.verbose("Fetch object operation start")
if !self._force
let objectInCache:objectModel? = Application.main.collections.availableObjectModels[self._objectId]
if let objectInCache = objectInCache
_LOGGER.verbose("object with id \(self._objectId) founded on cache")
self.object = objectInCache
self._LOGGER.verbose("Fetch object operation end : success")
self.state = .Finished
return
if !self.isCancelled
let url = "[...]\(self._objectId)"
_LOGGER.verbose("Requesting object with id \(self._objectId) on server")
Application.main.networkingSessionManager.request(url, method : .get)
.validate()
.responseJSON(
completionHandler: response in
switch response.result
case .success:
guard let raw:Any = response.result.value else
self._LOGGER.error("Error while fetching json programm : Empty response")
self._LOGGER.verbose("Fetch object operation end : error")
self.state = .Finished
return
let data:JSON = JSON(raw)
self._LOGGER.verbose("Received object from server \(data["bId"])")
self.object = ObjectModel(objectId:data["oId"].intValue,data:data)
Application.main.collections.availableobjectModels[self.object!.objectId] = self.object
self._LOGGER.verbose("Fetch object operation end : success")
self.state = .Finished
break
case .failure(let error):
self._LOGGER.error("Error while fetching json program \(error)")
self._LOGGER.verbose("Fetch object operation end : error")
self.state = .Finished
break
)
else
self._LOGGER.verbose("Fetch object operation end : cancel")
self.state = .Finished
网络队列
class MyQueue
public static let networkQueue:SaootiQueue = SaootiQueue(name:"NetworkQueue", concurent:true)
我如何在另一个操作中使用它并等待结果
let getObjectOperation:FetchObject = FetchObject(30)
SaootiQueue.networkQueue.addOperations([getObjectOperation], waitUntilFinished: true)
我如何使用它主要操作使用KVO
let getObjectOperation:FetchObject = FetchObject(30)
operation.addObserver(self, forKeyPath: #keyPath(Operation.isFinished), options: [.new], context: nil)
operation.addObserver(self, forKeyPath: #keyPath(Operation.isCancelled), options: [.new], context: nil)
queue.addOperation(operation)
//[...]
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
if let operation = object as? FetchObject
operation.removeObserver(self, forKeyPath: #keyPath(Operation.isFinished))
operation.removeObserver(self, forKeyPath: #keyPath(Operation.isCancelled))
if keyPath == #keyPath(Operation.isFinished)
//Do something
一些澄清:
我的应用程序是一个广播播放器,我需要在播放音乐和背景时获取当前正在播放的节目。这就是为什么我需要后台会话。
事实上,当应用程序处于前台时,我也将后台会话用于我所做的所有网络。我应该避免这种情况吗?
我正在使用的等待来自另一个队列,并且从未在主队列中使用(我知道这是一个线程反模式,我会处理它)。
实际上,当我进行两次联网操作时使用它,而第二次取决于第二次的结果。我在第一次操作后等待以避免 KVO 观察。我应该避免这种情况吗?
补充编辑:
当我说“我的队列越来越大,没有请求走到最后”时,这意味着在应用程序生命周期中的某个时刻,暂时是随机的(我每次都找不到重现它的方法),Alamofire 请求没有到达响应方法。
因为Operation
包装器没有结束并且队列正在增长。
顺便说一句,我正在努力将 Alamofire 请求转换为 URLRequest
以获得线索,并且我在使用主队列时发现了一些问题。由于 Alamofire 使用主队列进行响应方法,我必须对原因进行排序,我会看看是否发现潜在的死锁
我会及时通知您。谢谢
【问题讨论】:
感谢您的评论。我添加了一些示例代码。我希望它能提供更多信息,如果有什么问题在检测它。 什么是operation.first
?我没有看到任何 first
属性...
提供代码时,我尝试删除所有业务代码以专注于问题,这很糟糕。首先是业务代码的一部分。事实上,所有操作都是重载以提出不同的请求。第一个参数在重载类上。我从示例代码中删除它
如果您 100% 确定这是到达 request
的情况,但您没有看到 responseJSON
被调用,我所知道的唯一可能导致的情况是僵局。但这会导致 UI 锁定,并且主线程上的任何内容都会冻结。或者,如果您设置了一些异常大的超时参数,它可能似乎没有运行闭包。或者,如果您的操作中有一些路径没有将 state
设置为完成或取消,您的队列将备份。很难说没有MCVE。
【参考方案1】:
有一些小问题,但这个操作实现看起来基本正确。当然,您应该使您的状态管理线程安全,并且您可以进行其他风格改进,但我认为这对您的问题并不重要。
看起来令人担忧的是addOperations(_:waitUntilFinished:)
。您在哪个队列等候?如果您从主队列执行此操作,您将死锁(即看起来 Alamofire 请求永远不会完成)。 Alamofire 将主队列用于其完成处理程序(除非您覆盖 responseJSON
的 queue
参数),但如果您在主线程上等待,则永远不会发生这种情况。 (顺便说一句,如果你可以重构,这样你就不会明确地“等待”操作,这不仅可以避免死锁风险,而且总体上是一种更好的模式。)
我还注意到,您正在将 Alamofire 请求与后台会话结合使用,这些请求包含在操作中。后台会话与操作和完成处理程序关闭模式是对立的。在您的应用程序被抛弃后后台会话继续进行,您必须完全依赖您在应用程序启动时首次配置 SessionManager
时设置的 SessionDelegate
闭包。当应用重新启动时,您的操作和完成处理程序闭包早已不复存在。
底线,您真的需要后台会话(即在您的应用终止后继续上传和下载)吗?如果是这样,您可能希望失去这个完成处理程序和基于操作的方法。如果您不需要在应用程序终止后继续执行此操作,请不要使用后台会话。配置 Alamofire 以正确处理后台会话是一项非常重要的练习,因此只有在绝对需要时才这样做。请记住不要将后台会话与 Alamofire(和 URLSession
)自动为您执行的简单异步处理混为一谈。
你问:
我的应用程序是一个广播播放器,我需要在播放音乐和背景时获取当前正在播放的节目。这就是我需要后台 Session 的原因。
如果您希望在应用未运行时继续下载,则需要后台会话。但是,如果您的应用在后台运行并播放音乐,您可能不需要 后台会话。但是,如果用户选择下载特定的媒体资产,您可能需要后台会话,以便在用户离开应用时继续下载,无论应用是否正在播放音乐。
事实上,当应用程序处于前台时,我也将后台会话用于我所做的所有网络。我应该避免这种情况吗?
没关系。它有点慢,IIRC,但没关系。
问题不在于您使用的是后台会话,而在于您做错了。 Alamofire 基于操作的包装对后台会话没有意义。对于在后台进行的会话,您受限于如何使用URLSession
,即:
应用未运行时不能使用数据任务;仅上传和下载任务。
您不能依赖完成处理程序闭包(因为后台会话的全部目的是在您的应用终止时保持它们运行,然后在它们完成后再次启动您的应用;但是如果应用被终止,您的闭包是都消失了)。
您只能将基于委托的 API 用于后台会话,而不是完成处理程序。
您必须实现应用程序委托方法来捕获系统提供的完成处理程序,您在处理完后台会话委托调用后调用该完成处理程序。当您的 URLSession
告诉您它已处理完所有后台委托方法时,您必须调用它。
所有这些都是沉重的负担,恕我直言。鉴于系统让您的应用程序在背景音乐中保持活力,您可能会考虑使用标准的URLSessionConfiguration
。如果您要使用后台会话,您可能需要重构所有这些基于完成处理程序的代码。
我正在使用的等待来自另一个队列,并且从未在主队列中使用(我知道这是一个线程反模式,我会处理它)。
很好。使用“等待”仍然会产生严重的代码异味,但如果您 100% 确信这里没有死锁,您可以摆脱它。但这是您真正应该检查的事情(例如,在“等待”之后放置一些日志记录语句,并确保您已经越过了那条线,如果您还没有确认这一点)。
实际上,当我进行两次联网操作时使用它,第二次取决于第二次的结果。我在第一次操作后等待以避免 KVO 观察。我应该避免这种情况吗?
就个人而言,我会失去 KVO 观察,而只是在操作之间建立 addDependency
。此外,如果您摆脱了 KVO 观察,您可以摆脱双重 KVO 通知过程。但我不认为这个 KVO 的东西是问题的根源,所以也许你推迟了。
【讨论】:
以上是关于应用程序生命周期中的 Alamofire 请求阻塞的主要内容,如果未能解决你的问题,请参考以下文章