应用程序生命周期中的 Alamofire 请求阻塞

Posted

技术标签:

【中文标题】应用程序生命周期中的 Alamofire 请求阻塞【英文标题】:Alamofire request blocking during application lifecycle 【发布时间】:2017-04-10 15:17:34 【问题描述】:

我在使用 OperationOperationQueue 时遇到了 Alamofire 问题。

我有一个名为NetworkingQueueOperationQueue,我将一些操作(包装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 将主队列用于其完成处理程序(除非您覆盖 responseJSONqueue 参数),但如果您在主线程上等待,则永远不会发生这种情况。 (顺便说一句,如果你可以重构,这样你就不会明确地“等待”操作,这不仅可以避免死锁风险,而且总体上是一种更好的模式。)

    我还注意到,您正在将 Alamofire 请求与后台会话结合使用,这些请求包含在操作中。后台会话与操作和完成处理程序关闭模式是对立的。在您的应用程序被抛弃后后台会话继续进行,您必须完全依赖您在应用程序启动时首次配置 SessionManager 时设置的 SessionDelegate 闭包。当应用重新启动时,您的操作和完成处理程序闭包早已不复存在。

    底线,您真的需要后台会话(即在您的应用终止后继续上传和下载)吗?如果是这样,您可能希望失去这个完成处理程序和基于操作的方法。如果您不需要在应用程序终止后继续执行此操作,请不要使用后台会话。配置 Alamofire 以正确处理后台会话是一项非常重要的练习,因此只有在绝对需要时才这样做。请记住不要将后台会话与 Alamofire(和 URLSession)自动为您执行的简单异步处理混为一谈。


你问:

我的应用程序是一个广播播放器,我需要在播放音乐和背景时获取当前正在播放的节目。这就是我需要后台 Session 的原因。

如果您希望在应用未运行时继续下载,则需要后台会话。但是,如果您的应用在后台运行并播放音乐,您可能不需要 后台会话。但是,如果用户选择下载特定的媒体资产,您可能需要后台会话,以便在用户离开应用时继续下载,无论应用是否正在播放音乐。

事实上,当应用程序处于前台时,我也将后台会话用于我所做的所有网络。我应该避免这种情况吗?

没关系。它有点慢,IIRC,但没关系。

问题不在于您使用的是后台会话,而在于您做错了。 Alamofire 基于操作的包装对后台会话没有意义。对于在后台进行的会话,您受限于如何使用URLSession,即:

应用未运行时不能使用数据任务;仅上传和下载任务。

您不能依赖完成处理程序闭包(因为后台会话的全部目的是在您的应用终止时保持它们运行,然后在它们完成后再次启动您的应用;但是如果应用被终止,您的闭包是都消失了)。

您只能将基于委托的 API 用于后台会话,而不是完成处理程序。

您必须实现应用程序委托方法来捕获系统提供的完成处理程序,您在处理完后台会话委托调用后调用该完成处理程序。当您的 URLSession 告诉您它已处理完所有后台委托方法时,您必须调用它。

所有这些都是沉重的负担,恕我直言。鉴于系统让您的应用程序在背景音乐中保持活力,您可能会考虑使用标准的URLSessionConfiguration。如果您要使用后台会话,您可能需要重构所有这些基于完成处理程序的代码。

我正在使用的等待来自另一个队列,并且从未在主队列中使用(我知道这是一个线程反模式,我会处理它)。

很好。使用“等待”仍然会产生严重的代码异味,但如果您 100% 确信这里没有死锁,您可以摆脱它。但这是您真正应该检查的事情(例如,在“等待”之后放置一些日志记录语句,并确保您已经越过了那条线,如果您还没有确认这一点)。

实际上,当我进行两次联网操作时使用它,第二次取决于第二次的结果。我在第一次操作后等待以避免 KVO 观察。我应该避免这种情况吗?

就个人而言,我会失去 KVO 观察,而只是在操作之间建立 addDependency。此外,如果您摆脱了 KVO 观察,您可以摆脱双重 KVO 通知过程。但我不认为这个 KVO 的东西是问题的根源,所以也许你推迟了。

【讨论】:

以上是关于应用程序生命周期中的 Alamofire 请求阻塞的主要内容,如果未能解决你的问题,请参考以下文章

react生命周期详解

第18题-线程生命周期

第18题-线程生命周期

React生命周期

JavaSE---线程的生命周期

线程的生命周期