简单易懂的Alamofire使用及源码分析

Alamofire应该是用Swift做iOS开发里最常用到的三方框架了。我们在开发过程中常会遇到的网络请求,如:向服务端请求json数据,上传图片,下载文件等,都可以直接调用Alamofire或者二次封装它再进行调用。这里假定你已经使用过Alamofire,那么本篇文章会对Alamofire的部分源码选择性的介绍。当然,这里并不会贴上源码里的大段代码,而是通过一个例子来进入到Alamofire中的各个模块,让读者更好理解。

我们先来看看一个Alamofire的调用例子:
let url = "http://suggest.taobao.com/sug"     //淘宝的一个搜索api
let parameters: [String: Any] = [             //对`袜子`进行搜索
    "code" : "utf-8",
    "q" : "袜子"
]

`Alamofire`.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: nil)
           .validate(statusCode: [200])
           .responseData(queue: DispatchQueue.global(), completionHandler: { (responseData) in
                switch responseData.result {
                case .success(let data):
                  guard let jsonString = String(data: data, encoding: .utf8) else { return }
                  print(jsonString)
                case .failure(let error):
                  print(error)
            }
})
// `Alamofire`两边的单引号是为了markdown语法的显示更清晰,请忽略掉。

这个方法是Alamofire的基本用法。
我们通过Alamofire的request开始调用,
1.传入url
2.使用get方法
3.传入parameters,并在encoding中定义了参数的编码方式
4.因为是一个公共api,这里并不需要headers,headers传nil
5.在validate中传入需要验证的statusCode的数组
6.在responseData方法里传入全局队列和回调的处理闭包completionHandler
7.在闭包里对返回的数据里进行判断,若返回成功,打印jsonString,若失败则打印错误。

我们从前往后一点点来看。

(1)request方法做了什么事情?

我们点击request进入Alamofire.swift从最上面看起(这里建议同步点开Alamofire里的源文件,便于对比)

  • URLConvertible协议 的定义。我们在例子里传入的url是String类型,也可以将它转成URL类型再传入,结果都正确。
    就是因为String和URL都遵循URLConvertible协议,通过实现协议里的
func asURL() throws -> URL

方法,从String,URL或URLComponents类型里转换为URL,若失败则抛出定位为AFError的错误

  • 往下看,是协议URLRequestConvertible的定义,它是负责URLRequest的转换,和上面类似,就不多介绍了。

  • 再望下看,就是Alamofire对外公开的常用接口了。包括request,download,upload等方法的定义。而它们实际上都是调用了

SessionManager.default.request(...) -> Request  //传入参数省略

SessionManager是Alamofire中最外层用于发起网络请求的单例类,它会在init方法中定义实际的请求会话URLSession。而请求实际的调用者是URLSession。

结论是通过request方法,传入所需参数,发起请求,并返回Request的子类

如:DataRequest,DownloadRequest等,而DataRequest又继承于Request.

(2)返回给我们Request的子类有什么用呢?

我们进入 Request.swift 来看看Request类型的实现。

  • 定义了RequestTask,为请求任务分类
  • 持有了task(请求任务),session(请求对话),request(请求),response(响应)等
  • 定义了我们会常涉及到的任务管理方法,包含resume(激活),suspend(暂停),cancel(取消),这里我们来看一下cancel的实现
/// 取消请求.
open func cancel() {
    guard let task = task else { return }
    task.cancel()               //这里的task是request持有的URLSessionTask,实际上是调用它的取消方法
    NotificationCenter.default.post(          //当取消任务时,发送DidCancel的通知
        name: Notification.Name.Task.DidCancel,
        object: self,
        userInfo: [Notification.Key.Task: task]
    )
}

如果一个页面里的请求未结束就关闭页面,我们想要结束这次请求时,
可以在Alamofire请求的末尾调用cancel()方法来取消这次请求,它实际上是调用真正的网络请求URLSessionTask的cancel()方法。而Request的其他实现以及它的子类DataRequest,DownloadRequest这里也不详细介绍了。这里也可以顺便看看 Notification.swift ,了解一下Alamofire封装了哪些通知

结论是:返回给我们的Request,可以让我们控制请求的暂停,恢复,取消等。
(3)我们在例子中向encoding传入URLEncoding.default有什么用?

我们进入 ParameterEncoding.swift 看看它的实现
一来就看到了枚举类型HTTPMethod的实现,在这个文件下定义.get,.post,.put方法等类型,证明ParameterEncoding(即参数编码)和具体的请求方式是有联系的。
我们看到ParameterEncoding是一个协议,并且只有一个方法

func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest

//此方法是为了完成对传入的parameters的编码工作

我们常会用到的URLEncoding,JSONEncoding,以及不常用到的PropertyListEncoding都遵循这个协议。
这里截取URLEncoding中对该方法实现中会调用的一个方法

private func query(_ parameters: [String: Any]) -> String {
    var components: [(String, String)] = []
    for key in parameters.keys.sorted(by: <) {
        let value = parameters[key]!
        components += queryComponents(fromKey: key, value: value)
    }
    return components.map { "\($0)=\($1)" }.joined(separator: "&")
}

//本方法以及它的调用处可以看到URLEncoding方式是将parameters通过添加 & ,= 的方式拼接到url身后
而如果JSONEncoding方式是将parameters转化为二进制放入httpBody里。
因此也很好理解,为什么get方法的安全性不如post方法,因为get将请求参数暴露在外,而post在内部。

因此,我们将调用例子里的url变为

let url = "http://suggest.taobao.com/sug?code=utf-8&q=%E8%A2%9C%E5%AD%90"
//%E8%A2%9C%E5%AD%90由对`袜子`urlencode转化而来。

并且向parameter传nil,依然会得到同样的返回。因为现在这个url就是之前请求时,Alamofire转化之后的url。当然转化的过程不仅仅止于此,这里只说了其中的代表部分

结论是encoding: 是对参数进行编码,如果传入URLEncoding,会将参数拼接进url;如果传入JSONEncoding,则将参数转为二进制放入httpbody(Propertylist是以plist方式编码,我也很少用,就不介绍了)
(4)例子中传入的headers做了些什么?
public typealias HTTPHeaders = [String: String]

由HTTPHeaders的别名我们得知它的类型是[String: String]。
我们在Alamofire.swift里顺藤摸瓜找到了我们传的headers实际上是怎么处理的

extension URLRequest {
    public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
        let url = try url.asURL()

        self.init(url: url)

        httpMethod = method.rawValue

        if let headers = headers {
            for (headerField, headerValue) in headers {
                setValue(headerValue, forHTTPHeaderField: headerField)
            }
        }
    }
}

在URLRequest的扩展里,添加了它的init方法。在init方法里,遍历了headers里的key和value,
并调用setValue: forHTTPHeaderField:方法将这些key,value放入URLRequest的请求头里

结论是:传入的headers是会将其中的key,value赋给URLRequest的实例,也就是我们常说的请求头的设置
(5)例子中validate做了些什么?

我们打开 Validation.swift 发现,validate是对Request及其子类的扩展中增加的方法,传入一个Request并进行认证,再返回本身的类型,我们这里先看对statusCode的认证

public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {}

那么到底是如何认证的呢?这里我们再进入它的内部实现,看到:

fileprivate func validate<S: Sequence>(
    statusCode acceptableStatusCodes: S,
    response: HTTPURLResponse)
    -> ValidationResult
    where S.Iterator.Element == Int
{
    if acceptableStatusCodes.contains(response.statusCode) {        //若validate传入code包含response响应的code,则认证成功
        return .success
    } else {                  //若不包括,则返回失败,抛出AFError
        let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
        return .failure(AFError.responseValidationFailed(reason: reason))
    }
}

//acceptableStatusCodes为我们实际在validate中传入的statusCode数组,在例子中为[200].

通过acceptableStatusCodes.contains(response.statusCode)判断请求返回的code是否是我们想要的。
在这里我们可以做个测试,将例子中的[200]变为[201],就会打印出抛出的错误。因为http的正确返回码为200,我们传入的数组不包含200,就会抛出错误。当然我们也可以在例子中不调用validate,这样就不会有这些验证。
这里只介绍了statusCode的认证,实际上validate里还有对contentType等的认证。

结论是:validate会对http响应码,或者响应内容类型等进行认证,若认证成功不影响请求,若认证失败,会抛出错误。
(6)queue: DispatchQueue.global()做了些什么?

我们在一步步点进去在 ResponseSerialization.swift 的reponse方法中发现

(queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
//若queue为空则使用主队列处理返回数据

这行代码是传入queue的作用。

结论是:我们会把请求返回的处理放入queue:所传入的队列中,若我们没有向queue传入队列,那么会默认把处理放入主队列中。例子中我选择将处理放入全局队列中。

这里可以顺便看一下 DispatchQueue+Alamofire.swift 文件,里面是对DispatchQueue增加属性,获取不同优先级的队列,并增加了一个名为after的延迟调用方法。属性和方法都没有添加public关键字,那么它们只想在Alamofire内部调用,而不想暴露给我们。

(7)请求中的错误是如何分类的?

Alamofire里的错误都已经被定义到AFError中,我们打开 AFError.swift ,可以看到

enum AFError {       //这里为了看得方便,将AFError重新整理了下,源码中的内容要更多,但类型是一样的
  case invalidURL //无效的URL 
  case parameterEncodingFailed //请求参数编码失败 
  case multipartEncodingFailed //多部分编码失败 
  case responseValidationFailed //响应验证失败 
  case responseSerializationFailed //响应序列化失败 
}

而AFError的类型中,除了invalidURL的参数是遵循协议URLConvertible,其余类型的参数又由枚举类型组成。
这样就将很多种的错误类型,包括在了这5种类型中。
往下看,还能看到很多内容,例如对localizedDescription的实现,在请求抛出错误时,我们如果打印error.localizedDescription,看到的错误信息就是在这里定义的。

(8)还有哪些文件需要了解?
  • MultipartFormData.swift中会看到我们在使用上传功能时的编码方式
  • NetworkReachabilityManager.swift是Alamofire基于SCNetworkReachability封装的,用于监听当前手机的网络状态的工具,我们可以直接用
NetworkReachabilityManager().networkReachabilityStatus

来获取当前手机的网络状态
-ResponseSerialization.swift里定义了对请求返回的请求方式

以上就是我们根据一个简单的网络请求,而定位到Alamofire的内部实现的过程。因为内容很多,并且为了博客的可读性,这里只挑了一些基础且常用的进行介绍。我们如果想深入了解Alamofire,并且学习它的设计思路,还是需要到源码中一点点分析。

觉得有帮助的话,点个赞吧😊~

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

推荐阅读更多精彩内容