Alamofire-Request补充

一、回顾

在前面源码探索中,SessionManager管理RequestSessionDelegate的创建,并通过task绑定RequestSessionDelegate对象;Request负责请求的参数的配置,以及task不同任务的创建,创建连接外部(发送请求对象)和TaskDelegate的方法,通过闭包参数,获取TaskDelegate代理事件的内容;TaskDelegate代理事件是由SessionDelegate通过task移交的。总结图:

SessionManager.png

以上处理的目的是对任务做分层处理,使结构清晰。

二、RequestAdapter-适配器

Request文件下还存在一个协议RequestAdapter。在Manager中创建调用。如下:

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)
    }
}

联系上下文,adapter并没有被初始化,怎么回事呢?下面看一下是如何定义的:

/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
public protocol RequestAdapter {
    /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result.
    ///
    /// - parameter urlRequest: The URL request to adapt.
    ///
    /// - throws: An `Error` if the adaptation encounters an error.
    ///
    /// - returns: The adapted `URLRequest`.
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}

一个协议内部定义了一个方法,上面定义可以以某种方式检查并适应URLRequest,实际是告诉我们,根据需要遵循该协议并实现该方法。一脸懵逼,实现它干嘛呢?其实也不难猜测,既然给我们该类型,肯定是方便我们设置参数,如token、device、vision等等这些公共参数,其实可以设置的,那下面就来试试。

1、添加公共参数

class MyAdapter: RequestAdapter{
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
             var request = urlRequest
        request.setValue("hibotoken", forHTTPHeaderField: "token")
        request.setValue("device", forHTTPHeaderField: "iOS")
        request.setValue("vision", forHTTPHeaderField: "1.0.0")
        return request
    }
}

下面设置adapter并发送一个请求:

let urlStr = "http://onapp.yahibo.top/public/?s=api/test/list"
let url = URL.init(string: urlStr)!
Alamofire.SessionManager.default.adapter = MyAdapter()
Alamofire.request(url,method: .post,parameters: ["page":"1","size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
}
  • SessionManager中定义了adapter对象,这里就对其赋值一个实现了adapt方法的子类对象
    这里在请求前在adapt中设置了请求头,那么就运行一下,通过抓包看看公共参数是否添加成功:
args.png

添加成功,开发中的参数以后就可以单独使用该方法进行管理了。

2、重定向

class redireatAdapter: RequestAdapter{
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        let newURLRequest = URLRequest.init(url: URL.init(string: "http://onapp.yahibo.top/public/?s=api/test")!)
        return newURLRequest
    }
}

直接修改原请求地址,重定向至其他地址。

为什么会添加公共参数,或重定向?

代码追踪,追踪到最终使用位置如下:

func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
    do {
        let urlRequest = try self.urlRequest.adapt(using: adapter)
        return queue.sync { session.dataTask(with: urlRequest) }
    } catch {
        throw AdaptError(error: error)
    }
}
func adapt(using adapter: RequestAdapter?) throws -> URLRequest {
    guard let adapter = adapter else { return self }
    return try adapter.adapt(self)
}

这里调用了该方法,这里判断了adapter是否存在,不存在直接使用前面创建并设置好参数的URLRequest对象,如果存在则adapter调用adapt方法,将当前URLRequest对象传出去加工处理。

三、validate-自定义验证

开发中经常会根据不同的状态码来处理,比如开发中需要将某一结果定义为错误请求,在error中来做处理,那么在该框架中我们可以使用validate来重新验证,并定义请求结果。代码如下:

let urlStr = "http://onapp.yahibo.top/public/?s=api/test/list2"
let url = URL.init(string: urlStr)!
Alamofire.request(url,method: .post,parameters: ["page":"1","size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
}.validate{ (request, response, data) -> Request.ValidationResult in
    print(response)
    guard let _ = data else {
        return .failure(NSError(domain: "没有数据啊", code: 0, userInfo: nil))
    }
    guard response.statusCode == 200 else {
        return .failure(NSError(domain: "是不是哪弄错了", code: response.statusCode, userInfo: nil))
    }
    return .success
}
  • 通过链式方法调用validate验证方法,根据具体需求添加验证逻辑
  • 返回数据为空,定义为错误信息
  • statusCode != 200认为是错误请求
    通过以上试用,我们对Alamofire又有了更多的了解,无论是监听请求进度还是这种验证均以链式调用为主,方便快捷。

四、RequestRetrier-重新请求

很多情况下,如果网络请求失败,我们是有重新请求的需求,那么该框架也提供了这样的方法,请求失败都会调用代理方法:urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)。而框架就在该代理方法中做了如下处理:

if let retrier = retrier, let error = error {
    retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in
        guard shouldRetry else { completeTask(session, task, error) ; return }
        DispatchQueue.utility.after(timeDelay) { [weak self] in
            guard let strongSelf = self else { return }
            let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false
            if retrySucceeded, let task = request.task {
                strongSelf[task] = request
                return
            } else {
                completeTask(session, task, error)
            }
        }
    }
} else {
    completeTask(session, task, error)
}
  • 当请求错误,先判断retrier是否被定义如果定义则调用should方法
  • 这里retrier是一个继承自RequestRetrier协议的类对象

RequestRetrier

/// A type that determines whether a request should be retried after being executed by the specified session manager
/// and encountering an error.
public protocol RequestRetrier {
    /// Determines whether the `Request` should be retried by calling the `completion` closure.
    ///
    /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
    /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
    /// cleaned up after.
    ///
    /// - parameter manager:    The session manager the request was executed on.
    /// - parameter request:    The request that failed due to the encountered error.
    /// - parameter error:      The error encountered when executing the request.
    /// - parameter completion: The completion closure to be executed when retry decision has been determined.
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
}
  • RequestAdapter一样,需要定义类并实现方法

创建子类并继承协议,实现协议方法如下:

class MyRetrier: RequestRetrier{
    var count: Int = 0
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        if count<3 {
            completion(true,2)
             count += 1
        }else{
            completion(false,2)
        }
    }
}
  • 设置重新请求次数,为3次
  • 调用内部实现的闭包,向内传值,告诉内部重新请求还是,终止请求
  • completion有两个参数shouldRetry为是否请求,timeDelay为延时请求的延时时间,这里设置为2秒
  • 延时请求避免,无效请求

下面就可以设置一个错误连接发送请求尝试一下:

let urlStr = "http://onapp.yahibo.top/public/?s=api/test/list2"
let url = URL.init(string: urlStr)!
Alamofire.SessionManager.default.retrier = MyRetrier()
Alamofire.request(url,method: .post,parameters: ["page":"1","size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
    }.validate{ (request, response, data) -> Request.ValidationResult in
        print(response)
        guard let _ = data else {
            return .failure(NSError(domain: "没有数据啊", code: 10086, userInfo: nil))
        }
        if response.statusCode == 404 {
            return .failure(NSError(domain: "密码错误", code: response.statusCode, userInfo: nil))
        }
        return .success
}
  • RequestAdapter使用方法一致,需要配置SessionManager的retrier属性

五、Response-响应结果

Alamofire对请求到的数据进行了处理再返回给我们,以上请求我们都调用了responseJSON方法来获取最终数据,下面看一下responseJSON内部做了哪些处理:

public func responseJSON(
    queue: DispatchQueue? = nil,
    options: JSONSerialization.ReadingOptions = .allowFragments,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self
{
    return response(
        queue: queue,
        responseSerializer: DataRequest.jsonResponseSerializer(options: options),
        completionHandler: completionHandler
    )
}

联系上文可知responseJSONDataRequest的一个扩展方法,继承自Request类,因此可以进行链式调用。该方法内部继续调用了response方法。方法如下:

public func response<T: DataResponseSerializerProtocol>(
    queue: DispatchQueue? = nil,
    responseSerializer: T,
    completionHandler: @escaping (DataResponse<T.SerializedObject>) -> Void)
    -> Self
{
    delegate.queue.addOperation {
        let result = responseSerializer.serializeResponse(
            self.request,
            self.response,
            self.delegate.data,
            self.delegate.error
        )
        var dataResponse = DataResponse<T.SerializedObject>(
            request: self.request,
            response: self.response,
            data: self.delegate.data,
            result: result,
            timeline: self.timeline
        )
        dataResponse.add(self.delegate.metrics)
        (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
    }
    return self
}
  • 方法内部对请求结果进行了序列化处理
  • 将序列化的结果封装至DataResponse对象中

以上其实并没有看到我们熟悉的序列化,再继续搜索,找到如下代码:

public static func serializeResponseJSON(
        options: JSONSerialization.ReadingOptions,
        response: HTTPURLResponse?,
        data: Data?,
        error: Error?)
        -> Result<Any>
    {
        guard error == nil else { return .failure(error!) }
        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) }
        guard let validData = data, validData.count > 0 else {
            return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
        }
        do {
            let json = try JSONSerialization.jsonObject(with: validData, options: options)
            return .success(json)
        } catch {
            return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
        }
    }
  • 序列化结果封装至Result对象中
  • Result对象最终封装至DataResponse对象中来管理

从上面代码能够发现response对象管理了请求过程中所有参数:

var dataResponse = DataResponse<T.SerializedObject>(
    request: self.request,
    response: self.response,
    data: self.delegate.data,
    result: result,
    timeline: self.timeline
)

因此在请求结果中,我们能够很方便的拿到所有我们需要的信息。

六、Timeline-时间轴

为什么有时间轴,在网络请求中,我们需要准确的知道请求耗时,以便于前端或后台做优化处理。下面就看一下Alamofire的时间轴是如何设计的。

首先我们能够看到,任务是在队列中执行的:

func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
    do {
        let urlRequest = try self.urlRequest.adapt(using: adapter)
        return queue.sync { session.dataTask(with: urlRequest) }
    } catch {
        throw AdaptError(error: error)
    }
}

队列是在SessionManager中创建,Manager真是什么都管啊。代码如下:

let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
  • 设置标识绑定了当前设备的UUID
  • 该队列是管理发起的任务,和时间轴没有关系

紧接着初始化TaskDelegate对象。如下:

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)
    }
}

通过.data(originalTask, task)传入任务task,来初始化TaskDelegate对象如下:

self.queue = {
    let operationQueue = OperationQueue()
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.isSuspended = true
    operationQueue.qualityOfService = .utility
    return operationQueue
}()
  • 设置最大并发量为1,让任务顺序执行
  • 初始化的队列默认为挂起状态,因为任务还没有开启

1、startTime-记录发起请求时间

任务的创建与执行在Request中进行,代码如下:

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]
    )
}
  • resumeSessionManager中调用执行
  • 判断任务是否存在如果存在继续执行,因为有任务会被挂起,这里重新启动
  • task不存在,说明任务已结束,队列启动执行其他任务
  • 启动任务前记录请求初始时间,因为有挂起情况,这里对startTime做了判空操作

2、endTimer-记录请求结束时间

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() }
}
  • 创建并分类任务代理,以便任务下发
  • 记录任务结束时间

上面代码做了一个初始化,为什么说是结束时间呢,因为队列为同步队列,上次请求任务结束后才会执行。即请求完成后,代码如下:

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
    }
}
  • queue.isSuspended = false恢复队列,恢复后上面提到的记录时间任务即可加入到队列中执行

3、initialResponseTime-初始化响应时间

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
}
  • 初始化数据响应时间,不同任务对应的都有初始化方法,如下载任务,上传任务

4、TimeLine-时间轴设置

在响应初始化中,初始化时间轴:

extension DataRequest {
    @discardableResult
    public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Self {
        delegate.queue.addOperation {
            (queue ?? DispatchQueue.main).async {
                var dataResponse = DefaultDataResponse(
                    request: self.request,
                    response: self.response,
                    data: self.delegate.data,
                    error: self.delegate.error,
                    timeline: self.timeline
                )
                dataResponse.add(self.delegate.metrics)
                completionHandler(dataResponse)
            }
        }
        return self
    }
}
  • 时间轴是要面向开发的,因此在响应初始化时,被封装至Response

初始化时间轴,对前面的时间记录做统一管理:

extension Request {
    var timeline: Timeline {
        let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent()
        let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent()
        let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime
        return Timeline(
            requestStartTime: requestStartTime,
            initialResponseTime: initialResponseTime,
            requestCompletedTime: requestCompletedTime,
            serializationCompletedTime: CFAbsoluteTimeGetCurrent()
        )
    }
}

时间轴初始化,计算请求间隔,序列化时间间隔:

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
}

时间轴TimeLine记录了请求过程中的操作时间点,并计算了每部操作的时间间隔,在请求结束后封装至Response中。这里通过队列来同步请求中的操作,以保证startTime、endTime的准确性,其他时间记录是在请求代理回调中设置。

TimeLine:

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

推荐阅读更多精彩内容