Alamofire之Request(二)和队列执行顺序分析

上一篇文章 Alamofire之Request(一) 简单介绍了 Alamofire - Request 的整体流程. 但是有些细节是被过掉了的. 这里就详细补充一下.

上篇文章末尾我们提到了

delegate[task] = request

即在 SessionDelegate 中建立了 taskrequest 的绑定关系. 可以很方便的通过 task 获取到 request. 那么为什么要这么做呢 ?

SessionDelegate扮演的角色

看下这张图:


SessionDelegate类

图中得知:

SessionDelegate 是面向 SessionManager 的协议集合,其内部实现了所有和 URLSession 有关的 Delegate那么它就有该任务交由谁去执行的决定权.

其实真正情况如下:

  • 真正处理任务的是 task 对应 requestdelegate. DownloadTaskDelegateDataTaskDelegateUploadTaskDelegate 等来具体处理回调。
  • 有部分情况 SessionDelegate 会查看用户有没有指定, 如果用户有指定, 则还需调用用户指定的闭包.

来看个🌰:

extension SessionDelegate: URLSessionDownloadDelegate {
    open func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL)
    {
        if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
            downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
        }
    }
}

这个下载 session 的回调中就根据 downloadTaskDidFinishDownloadingToURL 这个开放出去的属性有没有被用户指定来指定执行权.

SessionManager.default.delegate.downloadTaskDidFinishDownloadingToURL = {
///...
}

如果没有指定, 就下发到 DownloadTaskDelegate 具体去执行文件操作.

func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didFinishDownloadingTo location: URL)
{
    temporaryURL = location

    let result = destination(location, response)
    let destinationURL = result.destinationURL
    let options = result.options

    self.destinationURL = destinationURL

    do {
        if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
            try FileManager.default.removeItem(at: destinationURL)
        }

        if options.contains(.createIntermediateDirectories) {
            let directory = destinationURL.deletingLastPathComponent()
            try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
        }

        try FileManager.default.moveItem(at: location, to: destinationURL)
    } catch {
        self.error = error
    }
}

SessionDelegate 通过的任务分发功能。让处理具体事务的 delegate 去处理对应的事务,避免了其内部逻辑混乱。同时管理触发用户发送的指令. 实现业务逻辑层下沉. 这也是我们做 SDK 时非常重要的架构思想

Alamofire中队列执行保障顺序与TimeLine

Alamofire 任务执行队列

回到我们执行 request 的代码中

open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
    var originalRequest: URLRequest?

    do {
        originalRequest = try urlRequest.asURLRequest()
        let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

        let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
        let request = DataRequest(session: session, requestTask: .data(originalTask, task))

        delegate[task] = request

        if startRequestsImmediately { request.resume() }

        return request
    } catch {
        return request(originalRequest, failedWith: error)
    }
}

之前上一篇博客讲述 request.resume() 后因为篇幅原因并没有继续往下讲.
那我们继续, 点进去查看方法 (找 Requestresume 方法).

open func resume() {
    guard let task = task else { delegate.queue.isSuspended = false ; return }

    if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }

    task.resume()

    NotificationCenter.default.post(
        name: Notification.Name.Task.DidResume,
        object: self,
        userInfo: [Notification.Key.Task: task]
    )
}

先来看第一句, 当没有任务, 则队列暂停挂起. ❓


别着急 , 我们先来看看这个 queue 是什么.

open class TaskDelegate: NSObject {

    public let queue: OperationQueue

    init(task: URLSessionTask?) {
        _task = task

        self.queue = {
            let operationQueue = OperationQueue()

            operationQueue.maxConcurrentOperationCount = 1
            operationQueue.isSuspended = true
            operationQueue.qualityOfService = .utility

            return operationQueue
        }()
    }
}

这个 queue 作为 TaskDelegate 的一个属性, 在初始化时就设置为挂起状态, 并且最大并发操作数为1.

TaskDelegate 这个角色不熟悉的可以回顾一下这个流程:

  • DataRequestSessionManager 发起 request 时创建.
let request = DataRequest(session: session, requestTask: >.data(originalTask, task))
  • 调用了父类的 init 方法, 创建了相应的 DataTaskDelegate / DownloadTaskDelegate 等等 TaskDelegate 的子类, 这个类就包含了 queue, 也就是任务的具体执行队列.

这个 queue 搞清楚了,有啥用? 接下来呢? 那么我们这里就引入一下 Alamofire 中的 TimeLine 时间轴. 然后一起讲述控制队列顺序.

Alamofire -- TimeLine

先回顾一下我们写的代码:

request("https://www.baidu.com", method: .get, parameters: ["username":"lb", "password":"123456"]).response { (response) in
    print("response === \(response)")
}

打印结果:

Timeline: { "Request Start Time": 588241191.681, "Initial Response Time": 588241192.039, "Request Completed Time": 588241192.089, "Serialization Completed Time": 588241192.090, "Latency": 0.357 secs, "Request Duration": 0.407 secs, "Serialization Duration": 0.001 secs, "Total Duration": 0.409 secs }

那么我们顺着这打印顺序来找一下这些时间.

  • 发起时间

刚刚在 Requestresume 方法中,我们也看到了这句代码

if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }

startTime 记录了当前的绝对时间. 也就是我们的网络请求的发起时间.

  • 开始收到数据时间
    查找方法: SessionDelegate -> URLSessionDataDelegate 的代理方法中 didReceive data 方法,然后找到具体实现者 DataTaskDelegate 方法如下:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

    if let dataTaskDidReceiveData = dataTaskDidReceiveData {
        dataTaskDidReceiveData(session, dataTask, data)
    } else {
        if let dataStream = dataStream {
            dataStream(data)
        } else {
            mutableData.append(data)
        }

        let bytesReceived = Int64(data.count)
        totalBytesReceived += bytesReceived
        let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown

        progress.totalUnitCount = totalBytesExpected
        progress.completedUnitCount = totalBytesReceived

        if let progressHandler = progressHandler {
            progressHandler.queue.async { progressHandler.closure(self.progress) }
        }
    }
}

记录了 initialResponseTime开始接收到数据时间.

  • 网络请求结束时间
    再来看下 DataRequest 创建方法, -> 父类 init 方法
init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
    self.session = session

    switch requestTask {
    case .data(let originalTask, let task):
        taskDelegate = DataTaskDelegate(task: task)
        self.originalTask = originalTask
    case .download(let originalTask, let task):
        taskDelegate = DownloadTaskDelegate(task: task)
        self.originalTask = originalTask
    case .upload(let originalTask, let task):
        taskDelegate = UploadTaskDelegate(task: task)
        self.originalTask = originalTask
    case .stream(let originalTask, let task):
        taskDelegate = TaskDelegate(task: task)
        self.originalTask = originalTask
    }

    delegate.error = error
    delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
}

很好奇为什么是初始化 Request 拿的是请求结束时间?

初始化 DataRequest 时,向 queue 队列中添加一个任务,获取当前的时间戳赋值给 endTime,这就是网络请求结束时间。
但是因为当前队列默认为挂起状态,所以不会执行里面的任务。那么这个任务在何时执行呢?
在网络请求完成回调 didCompleteWithError 方法时会恢复 queue 队列。

方法查找过程 : SessionDelegate : didCompleteWithError -> 里面分发给了 TaskDelegateurlSession(session, task:, didCompleteWithError:) 方法:

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let taskDidCompleteWithError = taskDidCompleteWithError {
        taskDidCompleteWithError(session, task, error)
    } else {
        if let error = error {
            if self.error == nil { self.error = error }

            if
                let downloadDelegate = self as? DownloadTaskDelegate,
                let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
            {
                downloadDelegate.resumeData = resumeData
            }
        }

        queue.isSuspended = false
    }
}

看到这里聪明的你是不是明白了什么? 是的, 不管外界如何链式编程. 我只要控制队列的挂起和取消就可以保证按该有的顺序执行相应的任务.

  • 通过计算得出的 请求周期 ,延迟, 等部分时间
public init(
    requestStartTime: CFAbsoluteTime = 0.0,
    initialResponseTime: CFAbsoluteTime = 0.0,
    requestCompletedTime: CFAbsoluteTime = 0.0,
    serializationCompletedTime: CFAbsoluteTime = 0.0)
{
    self.requestStartTime = requestStartTime
    self.initialResponseTime = initialResponseTime
    self.requestCompletedTime = requestCompletedTime
    self.serializationCompletedTime = serializationCompletedTime

    self.latency = initialResponseTime - requestStartTime
    self.requestDuration = requestCompletedTime - requestStartTime
    self.serializationDuration = serializationCompletedTime - requestCompletedTime
    self.totalDuration = serializationCompletedTime - requestStartTime
}

由计算得出的请求周期 ,延迟, 等时间, 因此它也是帮助我们测量网络请求速度的一个很方便的工具.

最后附上一张 Alamofire 架构图:

至此, Request 完整流程 以及 SessionManger - SessionDelegate - Request 以及其子类 - TaskDelegate 以及其子类的层级关系图是不是更清晰完整了. 整个架构层分工清晰, 业务逻辑层下沉, 又保证了用户指令的优先级. 这SDK的架构设计思想 啧啧.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容