引用
文章来源:
作者:黄文臣
前言:
是一个由swift编写的优雅的网络开发框架。
大部分用swift编写的ios app的网络模块都是基于alamofire的。作为swift社区最活跃的几个项目之一,有许多开发者在不断的对其进行完善,所以学习这种优秀的开源代码对深入理解swift的特性很有帮助。
本文很长,大到整个框架的设计,小到某些基础功能的使用都会涉及。
url loading system
ios的网络开发(url loading system)的类层次如下:
从图中可以看出,整个框架包括url loading相关的核心类和五种辅助类。其中,五种辅助类划分如下
- configuration 配置信息,比如cookie的存储策略,tls版本等等。
- authentication and credentials 授权和证书
- protocol support 用做proxy来拦截或特殊处理某些url
- cookie storage 管理cookie
- cache management 管理缓存
alamofire就是建立在nsurlsession上的封装。
nsurlsession是在2013年推出的新api,并且apple在2015年废弃了nsurlconnection。如果你的app还在用以nsurlconnection建立的网络层(比如afnetworking 2.x),那么你真的应该考虑升级到nsurlsession(比如afnetworking 3.x),废弃的api也许还能正常工作,但是apple已对其不再维护,当然也就不支持http 2.0等新特性。
关于nsurlsesson的基础使用,我之前有过几篇博客,可以在这个链接找到:
那么,用nsurlsession来进行http/https请求的时候,实际的过程如何呢?
- 建立nsurlsessiontask,并且resume.
- 检查cache策略,如果有需要从本地cache中直接返回数据
- 通过dns进行域名查找
- 建立tcp连接
- 如果是https,进行tls握手(如有资源需要认证访问,可能需要客户端提供证书,用户名密码等信息)
- 请求开始,收到http的response
- 接收http的data
tips: 理解http/https的请求过程很重要,因为往往你需要统计api请求在哪个阶段出了问题,然后对症下药,提高用户体验。
整体架构
alamofie的整体功能图如下:
其中:
- 左侧是暴露给外部的接口,右侧是内部实现相关
- 这三个模块比较独立:和 是基于alamofire开发的独立的库,分别用来做图片和网络状态小菊花,networkreachabilitymanager也是先对独立的用来检测蜂窝移动,wifi等网络变化的。
我们先从一个api调用切入,来分析各个模块的作用:
alamofire.request(/**/).validate(/**/).responsejson {/**/}
初始化sessionmanager的单例default
//整理后代码
self.delegate = sessiondelegate()
self.session = urlsession(configuration: configuration, delegate: delegate, delegatequeue: nil)
在初始化sessionmanager代码里,提供了一个默认的sessiondelegate,并且初始化了一个urlsession,这个urlsession的delegate是sessiondelegate。
通过这个初始化,我们知道urlsession的几个代理事件都是传递给sessionmanager的sessiondelegate了。
执行全局方法alamofire.request
方法体中调用sessionmanager.default单例的实例方法来创建datarequest。这一步做了如下动作:
- 根据传入的url,parameters等参数创建urlrequest
- 根据urlrequest和sessionmanager的属性session(urlsession),adapter(请求适配器),queue(gcd queue)创建urlsessiondatatask
- 根据基类request提供的方法,创建子类datarequest实例,并且为子类datarequest初始化一个datataskdelegate。
每一个datarequest对应一个datataskdelegate,每一个taskdelegate有一个operationqueue,这个queue在初始化的时候是挂起状态的,并且是一个串行队列(maxconcurrentoperationcount = 1)。
open class request{
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
//省略
}
delegate.error = error
delegate.queue.addoperation { self.endtime = cfabsolutetimegetcurrent() } //加入统计请求结束的operation
}
}
执行datatask.validate
内容很简单,就是把传入的闭包保存起来,等待后续执行,并且返回self
执行datatask.responsejson
在这个方法里,创建一个nsoperation加入到datataskdelegate的queue中,这个queue在创建之初是刮挂起状态的,所以提交的任务不会执行。
urlsession收到数据
首先sessiondelegate代理方法被调用:
open func urlsession(_ session: urlsession, datatask: urlsessiondatatask, didreceive data: data) {
if let datataskdidreceivedata = datataskdidreceivedata {//有自定义实现
datataskdidreceivedata(session, datatask, data)
} else if let delegate = self[datatask]?.delegate as? datataskdelegate {//走默认实现
delegate.urlsession(session, datatask: datatask, didreceive: data)
}
}
在这个代理方法里,根据存储的字典 urlsessiontask -> taskdelegate 找到这个task的datataskdelegate,调用其方法
func urlsession(_ session: urlsession, datatask: urlsessiondatatask, didreceive data: data) {
//整理后代码
mutabledata.append(data) //存储数据到内存
progresshandler.queue.async { progresshandler.closure(self.progress) } //回调progresshandler
}
urlsession完成task
首先调用sessiondelegate中的urlsession的代理方法
open func urlsession(_ session: urlsession, task: urlsessiontask, didcompletewitherror error: error?) {
//整理后代码
//执行response的validation
request.validations.foreach { $0() }
//唤起queue,来执行提交的任务
strongself[task]?.delegate.queue.issuspended = false
strongself[task] = nil
}
由于queue被唤起,所以之前提交的完成callback会被执行。
执行网络请求完成的callback
//序列化请求结果,这里的responseserializer为dataresponseserializerprotocol协议类型
let result = responseserializer.serializeresponse(
self.request,
self.response,
self.delegate.data,
self.delegate.error
)
//建立response对象
var dataresponse = dataresponse(
request: self.request,
response: self.response,
data: self.delegate.data,
result: result,
timeline: self.timeline
)
//增加统计相关信息
dataresponse.add(self.delegate.metrics)
//执行传入的必报,也就是responsejson函数传入的闭包
(queue ?? dispatchqueue.main).async { completionhandler(dataresponse) }
api设计
衡量一个框架好坏最重要的因素就是是否容易使用。
那么,如何定义容易使用呢?
根据二八原则,对于一个框架的使用百分之八十的时候都是很基础的功能使用,当这些基础的功能使用是容易的,我们认为这个框架是容易使用的。
我们来对比一下,同样get一个url,然后把数据解析成json。使用nsurlsession层次的api如下
guard let url = else {
return;
}
let datatask = urlsession.shared.datatask(with: url) { (data, response, error) in
guard let data = data else{
return;
}
do{
let json = try jsonserialization.jsonobject(with: data, options: .allowfragments)
print(json)
}catch let error{
print(error)
}
};
datatask.resume()
使用alamofire
alamofire.request("https://raw.githubusercontent.com/leomobiledeveloper/react-native-files/master/person.json").responsejson { (response) in
if let json = response.result.value {
print("json: \(json)")
}
}
tips: 这里的alamofire.request指的是module(模块) alamofire的一个全局方法request调用。
可以看到,使用系统的api,我们不得不先创建url,然后建立datatask,并且resume。接着在callback里去解析json。由于swift是一种强类型的语言,我们不得不进行大量的逻辑判断和try-catch。
而alamofire把这些步骤简化成了一个静态的方法调用,并且用链式的方式来处理异步的response解析。由于是链式的,你可以用链式的方式实现很多逻辑,比如验证返回值:
alamofire.request("https://httpbin.org/get")
.validate(statuscode: 200..<300) //返回值验证
.responsedata { response in //解析返回的数据
switch response.result {
case .success:
print("validation successful")
case .failure(let error):
print(error)
}
}
用链式的方式进行异步处理是一个很好的实践,延伸阅读可以参考:,。
链式的异步处理有很多优点:
- 优雅的处理大量的callback
- 代码更容易理解,更容易维护
- 不需要在每一步都进行错误检查
80%情况下的api调用
alamofire是采用静态方法的方式来提供80%情况下的api,这些全局方法可以在找到,以request为例:
@discardableresult //关键词告诉编译器,即使返回值不被持有,也别报警告
public func request(
_ url: urlconvertible,
method: httpmethod = .get,
parameters: parameters? = nil,
encoding: parameterencoding = urlencoding.default,
headers: httpheaders? = nil)
-> datarequest
{
return sessionmanager.default.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}
我们来分析下这个简单却又精炼的方法,方法的几个参数
- url 请求的url,协议urlconvertible类型(alamofire用extension的方式为url,string,urlcomponents实现了这个协议)
- method 请求的http方法,默认为get
- parameters 请求的参数,默认为nil
- encoding,,参数编码类型,默认urlencoding.default,也就是根据http方法的类型决定参数是query或者body里
- headers, http header
返回值是一个datarequest实例,这个实例就是异步调用链的头部。
tips: 用默认参数来实现默认配置是一个很好的实践。
如何实现链式调用
open class request {
var validations: [() -> void] = []
public func validate(_ validation: @escaping validation) -> self {
let validationexecution: () -> void = {/**/}
validations.append(validationexecution)
return self
}
}
从代码中,我们可以比较清楚的看出链式调用的原理:
函数的参数是闭包类型,方法体把这个闭包类型输入存储起来,并且返回self。在合适的时候执行闭包即可实现异步的链式调用。
模块功能
软件设计有一个非常重要的原则就是:单一功能原则。
alaofire的文件划分如下:
我们来分析alamofire的各个模块负责的功能:
- sessionmanager 整个alamofire框架的核心枢纽,封装了urlsession。负责提供外部调用的api,处理请求适配器,请求的重拾。
- sessiondelegate sessionmanager的代理,封装了urlsessiondelegate。负责对task的回调事件提供默认实现(转发给taskdelegate进行实际处理),并且以闭包的方式暴露给外部,让外部可以自定义实现。
- taskdelegate 对urlsessiontask的回调进行实际的处理,并且执行task的完成回调用
- request 是urlsessiontask的封装,是暴露给上层的请求任务
parameterencoding 对参数进行encoding(json,query等)
- response 代表返回数据序列化后的结果
responseserializer 对返回的数据进行序列化(json,property list等)
- servertrustpolicymanager/servertrustpolicy 对tls等过程中发生的认证进行处理
- timeline 纯粹的用来进行网络请求过程的数据统计
线程
alamofire的线程处理都是采用gcd和nsoperation,并没有使用底层的thread。
sessionmanager
每一个sessionmanager有一个常量属性
let queue = dispatchqueue(label: "org.alamofire.session-manager." uuid().uuidstring)
这个queue用来做task的初始化工作,也做了比如文件创建等
//task初始化
return task = queue.sync { session.downloadtask(with: urlrequest) }
//创建目录
try filemanager.createdirectory(at: directoryurl, withintermediatedirectories: true, attributes: nil)
urlsession
session是这样被初始化的:
self.session = urlsession(configuration: configuration, delegate: delegate, delegatequeue: nil)
delegatequeue是urlsession的各种回调函数被调用的串行队列,这里传入nil,表示由系统自动为我们创建回调队列。
globalqueue
关于全局队列,有如下使用
//重试
dispatchqueue.utility.after{}
//初始化上传的multipartformdata
dispatchqueue.global(qos: .utility).async
taskdelegate
每一个task有一个taskdelegate,每一个taskdelegate有一个常量属性queue
self.queue = {
let operationqueue = operationqueue()
operationqueue.maxconcurrentoperationcount = 1
operationqueue.issuspended = true
operationqueue.qualityofservice = .utility
return operationqueue
}()
这个queue有一点黑科技,在创建的时候是挂起的,然后不断的往里塞任务:比如responsejson等。然后等task完成的时候,再唤起queue,执行这些任务。
还是举一个例子,我们来看看队列之前的切换:
alamofire.request(/**/).validate(/**/).responsejson {/**/}
- 主队列调用request方法
- sync到sessionmanager的queue上创建urlsessiondatatask
- 主队列调用validate方法和responsejson保存相关闭包
- urlsession中由系统自动创建的queue收到delegate事件回调
- 收到urlsessiontask完成的回调,taskdelegate的queue被唤起
- 异步到主队列执行responsejson中传入的闭包
当然,上述的队列使用不包括以参数方式传递进入的,比如responsejson,就可以指定这个闭包执行的队列
public func responsejson(
queue: dispatchqueue? = nil,
options: jsonserialization.readingoptions = .allowfragments,
completionhandler: @escaping (dataresponse) -> void)
-> self{}
错误处理
数据结构
alamofire的错误处理是采用了带关联值枚举,在swift开发中,枚举是最常见的用来处理错误的。
在关联值枚举中,alamofire还定义了内部类型,来对错误类型进行二次分类。代码如下:
public enum aferror: error {
public enum parameterencodingfailurereason {/*省略*/}
public enum multipartencodingfailurereason {/*省略*/}
public enum responsevalidationfailurereason {/*省略*/}
public enum responseserializationfailurereason {/*省略*/}
//枚举的可能值
case invalid
case parameterencodingfailed(reason: parameterencodingfailurereason)
case multipartencodingfailed(reason: multipartencodingfailurereason)
case responsevalidationfailed(reason: responsevalidationfailurereason)
case responseserializationfailed(reason: responseserializationfailurereason)
}
我们来分析为什么要这样定义这些错误类型,一个典型的网络库的请求数据流如下:
其中,在调用urlsession相关的api之前,我们要先创建urlrequest,然后交给urlsession去做实际的http请求,然后拿到http请求的二进制数据,根据需要转换成字符串/json等交给上层。
所以,alomofire的错误处理思想是:
根据错误发生的位置进行一级分类,再用嵌套类型对错误进行二次分类。
除了错误定义之外,开发者抓到错误能有友善的描述信息也是很重要的,这就是。swift提供localizederror
extension aferror: localizederror {
public var errordescription: string? {
/*省略*/
}
}
extension aferror.parameterencodingfailurereason {
var localizeddescription: string {
/*省略*/
}
}
swift错误处理延伸阅读:
继承
nrulsessiontask是由继承来实现的,继承关系如下
urlsessiontask — task的基类
- urlsessiondatatask - 拉取url的内容nsdata
urlsessionuploadtask — 上传数据到url,并且返回是nsdata
- urlsessiondownloadtask - 下载url的内容到文件
- urlsessionstreamtask — 建立tcp/ip连接
仿照这种关系,alamofire的request也是类似的继承关系:
request — task的基类
- datarequest - 拉取url的内容nsdata
uploadrequest — 上传数据到url,并且返回是nsdata
- downloadrequest - 下载url的内容到文件
- streamrequest — 建立tcp/ip连接
其实原因和很简单:
父类提供基础的属性和方法来给子类复用。
在request中,除了继承,还使用了聚类的方式:由父类提供接口,初始化子类
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
/**/
}
}
协议
swift是面向协议编程的语言。
alamofire的很多设计都是以协议为中心的,
以parameterencoding协议:
定义如下:
public protocol parameterencoding {
func encode(_ urlrequest: urlrequestconvertible, with parameters: parameters?) throws -> urlrequest
}
接口依赖于这个协议类型
public func request(
_ url: urlconvertible,
method: httpmethod = .get,
parameters: parameters? = nil,
encoding: parameterencoding = urlencoding.default,
headers: httpheaders? = nil)
-> datarequest
{
/*略*/
}
这样在传入的时候,只要是这个协议类型都可以,不管是struct,enum或者class。
alamofire实现了三种encoding方式:
public struct urlencoding: parameterencoding {}
public struct jsonencoding: parameterencoding {}
public struct propertylistencoding: parameterencoding {}
扩展性
由于提供的接口是协议类型的,于是你可以方便直接把一个实例当作url,并且自定义encodeing方法
enum api:urlconvertible{
case login
public func as throws -> url {
//return login url
}
}
class customencoding: parameterencoding{/*/*}
然后,你就可以这么调用了
alamofire.request(api.login, method: .post, encoding: cusomencoding())
可以看到,使用协议提供的接口是抽象的接口,与具体的class/enum/struct无关,也就易于扩展
代理
代理是cocoatouch一个很优秀的设计模式,它提供了一种盲通信的方式把相关的任务划分到不同的类中。
在alamofire中,最主要的就是这两对代理关系:
由于delegate的存在,
- sessionmanager只需要关注urlsession的封装即可,session层面的事件回调交给由sessiondelegate处理
- request只需要关注urlsessiontask的封装,task层面的任务交给requestdelegate处理。
这样,保证了各个模块之间的功能单一,不会互相耦合。
类型安全
swift本身是一种类型安全的语言,这意味着如果编译器发现类型不对,你的代码将编译不通过。
urlrequest有一个属性是httpmethod
var httpmethod: string? { get set }
它的类型是string类型,这意味着你可以随意的赋值,编译器缺不会提示你你的输入可能又问题。
request.httpmethod = "1234"
考虑到httpmethod无非也就是那几种,很适合用enum来做,alamofire对其进行了封装
public enum httpmethod: string {
case options = "options"
case get = "get"
case head = "head"
case post = "post"
case put = "put"
case patch = "patch"
case delete = "delete"
case trace = "trace"
case connect = "connect"
}
然后,上层的方法提供的接口是枚举类型:
public func request(
_ url: urlconvertible,
method: httpmethod = .get, //这里
parameters: parameters? = nil,
encoding: parameterencoding = urlencoding.default,
headers: httpheaders? = nil)
-> datarequest
{
/*略*/
}
这样,编译器就能够进行合理的检查,也不容易出错了。
版本与平台适配
alamofire适配的平台有ios/osx/tvos/watchos,适配的最低ios版本是ios 8。 那么,就出现了一个问题
- 有些平台没有对应的api
- 有些api是高版本的系统才有的
举个例子:
func streamtask(with service: netservice) -> urlsessionstreamtask
alamofire采用如下方式进行适配:
@avialable - 用来标记适配系统版本(for编译器)
比如,这个函数被标记为ios 9.0后可用,如果直接在target ios 8的调用,则会报错。可以在if #available{}中调用
@discardableresult
@available(ios 9.0, macos 10.11, tvos 9.0, *)
public func stream(withhostname hostname: string, port: int) -> streamrequest {
return sessionmanager.default.stream(withhostname: hostname, port: port)
}
#if ... #endif - 用作条件编译(for编译器)
例如:在watchos上不编译
#if !os(watchos)
@discardableresult
@available(ios 9.0, macos 10.11, tvos 9.0, *)
public func stream(withhostname hostname: string, port: int) -> streamrequest {
return sessionmanager.default.stream(withhostname: hostname, port: port)
}
#endif
#available - 满足平台和系统要求才调用(for 编译器,运行时)
extension response {
mutating func add(_ metrics: anyobject?) {
#if !os(watchos)
guard #available(ios 10.0, macos 10.12, tvos 10.0, *) else { return }
guard let metrics = metrics as? urlsessiontaskmetrics else { return }
_metrics = metrics
#endif
}
}
总结
alamofire是一个优雅的swift开源库,它的代码真的很优雅,强烈建议对swift感兴趣并且想深入学习的同学用几天的空余时间去研究下。看的时候多问自己几个问题:
- 为什么这里要用protocol而不用继承?
- 为什么这里要用struct而不用class?
- …..
总之,多问为什么,然后找到答案,就会很有收获。
本文同步放到我的上,如有问题欢迎issue。