前一篇文章大致说了下Swift版本的原生请求,以及使用Alamofire请求数据是如何的方便,这篇文章开始,慢慢的深入接入Alamofire的源码,仔细看看一个在GitHub上如此高星的框架源码多么的优秀!代码里使用的Alamofire版本为4.8.2的稳定release版本,本篇主要介绍的内容如下:
- SessionManager初始化
- SessionManager下载
- SessionManager后台下载
- SessionManager流程分析
直接进入request的源码开干
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
)
}
可以看到method参数是一个HTTPMethod的枚举,默认值必须是用的最多的get方法,自行进入一看源码,
清楚!String类型关联的枚举,里面是各种HTTP的请求方法。
SessionManager.default
看着就像是一个单利的用法,验证一下就继续深入源码:
/// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use
/// directly for any ad hoc requests.
public static let `default`: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
果不其然,的确是个单利。这个跟AFNetworking里面的manager不一样,AFNetworking里的manager是一个工厂模式,并非一个单利。
SessionManager的初始化方法只传了一个configuration参数,深入看一下SessionManager的初始化方法:
public init(
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
delegate: SessionDelegate = SessionDelegate(),
serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
{
self.delegate = delegate
self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
}
可见初始化方法里的参数全有默认值,也就是说可以直接SessionManager()初始化出来一个实例的。那么在单利里面传的一个configuration时候把httpAdditionalHeaders
赋了值,直接看源码:
/// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers.
public static let defaultHTTPHeaders: HTTPHeaders = {
// Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5"
// Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in
let quality = 1.0 - (Double(index) * 0.1)
return "\(languageCode);q=\(quality)"
}.joined(separator: ", ")
// User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3
// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0`
let userAgent: String = {
if let info = Bundle.main.infoDictionary {
let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown"
let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown"
let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown"
let osNameVersion: String = {
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
let osName: String = {
#if os(iOS)
return "iOS"
#elseif os(watchOS)
return "watchOS"
#elseif os(tvOS)
return "tvOS"
#elseif os(macOS)
return "OS X"
#elseif os(Linux)
return "Linux"
#else
return "Unknown"
#endif
}()
return "\(osName) \(versionString)"
}()
let alamofireVersion: String = {
guard
let afInfo = Bundle(for: SessionManager.self).infoDictionary,
let build = afInfo["CFBundleShortVersionString"]
else { return "Unknown" }
return "Alamofire/\(build)"
}()
return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
}
return "Alamofire"
}()
return [
"Accept-Encoding": acceptEncoding,
"Accept-Language": acceptLanguage,
"User-Agent": userAgent
]
}()
设置了一些请求头信息,看下来觉得userAgent
是个非常有意义的值,里面包含了项目、设备、Alamofire版本等信息,可以方便后端查找有异常的数据,能更好的定位异常。直接用Charles抓个包,将原生请求和Alamofire的请求对比一下:


在SessionManager的初始化方法里看到有一个
delegate
参数,这个东西就是用来实现代理移交的,SessionManager作为一个管理者,不可能直接管太多的细节,所以会把一些工作移交给对应的人来出来。同时看到有一个commonInit
方法,直接干进去看源码:
private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
session.serverTrustPolicyManager = serverTrustPolicyManager
delegate.sessionManager = self
delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
guard let strongSelf = self else { return }
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
}
}
delegate.sessionManager = self
这是什么骚操作,是不是感觉产生了循环引用,会导致内存泄漏,哈哈,这点大可放心,这么牛逼的一个开源库,这种低级错误不存在的,这个赋值主要是想标记一下,有了问题反馈给谁,可以这么理解:

SessionManager就是一个管理者,delegate有多种,如果说不同的delegate之间直接交流,会很乱,所以需要有一个管理者或者说中间者来统一调度管理。
commonInit方法里最后的一个方法看名字就很熟悉,是不是很像上一篇文章的后台下载,没错这个就是用来处理后台下载的。
下面来一个实实在在的用Alamofire下载文件的示例:
SessionManager.default.download(videoUrlStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(response.suggestedFilename ?? "video0818.mp4")
return (fileUrl!,[.createIntermediateDirectories,.removePreviousFile])
}
.downloadProgress { (progress) in
print("进度:\(progress)")
}
.response { (downloadResponse) in
print("回调信息:\(downloadResponse)")
}
漂亮的链式语法,看着就是舒服!return (fileUrl!,[.createIntermediateDirectories,.removePreviousFile])
返回的是个目标地址和下载的一些可选项的元组,比较简单,我这里传的是移除目标路径里已经有的文件和创建需要的目录,也就是说下载完后将下载完的文件从tmp移动到返回的目标路径的时候,如果目标路径里是存在文件的会直接被移除掉。那么问题来了,文件比较大,下载到一半的时候用户进入后台了,这样写会继续下载吗 ?如果你还记得SessionManager的defalut的单利对象里面的let configuration = URLSessionConfiguration.default
,那你就肯定知道不支持后台下载的。。。那就自己摸索一下先,一顿操作猛如虎之后代码如下:
let configuration=URLSessionConfiguration.background(withIdentifier: "com.BoxJing.backgroundDownload")
let manager=SessionManager(configuration: configuration)
manager.download(videoUrlStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(response.suggestedFilename ?? "video0818.mp4")
return (fileUrl!,[.createIntermediateDirectories,.removePreviousFile])
}
.downloadProgress { (progress) in
print("进度:\(progress)")
}
.response { (downloadResponse) in
print("回调信息:\(downloadResponse)")
}
会发现下面的结果(-999):
019-08-18 16:15:12.750608+0800 AlamofireDemo[3064:98404] Task <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1> load failed with error Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=http://cache.fotoplace.cc/181114/13404389/3832bf181a42f3b372156659dac6b731.mp4, NSErrorFailingURLKey=http://cache.fotoplace.cc/181114/13404389/3832bf181a42f3b372156659dac6b731.mp4, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"BackgroundDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>",
"LocalDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>, NSLocalizedDescription=cancelled} [-999]
回调信息:DefaultDownloadResponse(request: Optional(http://cache.fotoplace.cc/181114/13404389/3832bf181a42f3b372156659dac6b731.mp4), response: nil, temporaryURL: nil, destinationURL: nil, resumeData: nil, error: Optional(Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=http://cache.fotoplace.cc/181114/13404389/3832bf181a42f3b372156659dac6b731.mp4, NSErrorFailingURLKey=http://cache.fotoplace.cc/181114/13404389/3832bf181a42f3b372156659dac6b731.mp4, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"BackgroundDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>",
"LocalDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDownloadTask <BC8DA9F9-9779-4181-99FA-3BF8C2EEBE32>.<1>, NSLocalizedDescription=cancelled}), timeline: Timeline: { "Request Start Time": 587808912.744, "Initial Response Time": 587808912.754, "Request Completed Time": 587808912.754, "Serialization Completed Time": 587808912.756, "Latency": 0.010 secs, "Request Duration": 0.010 secs, "Serialization Duration": 0.002 secs, "Total Duration": 0.012 secs }, _metrics: nil)
这就尴尬了!明明就是设置一个background模式,就下载不了了???如何解决?(想自己去探究的可以直接去Alamofire的issue里翻一翻会有结果的,每个开源项目的issue是最好的解决自己问题的地方)。
不多扯,自行去issue里继续找去吧,原来需要将manager持有一下保存住。来个属性保存一SessionManager对象,var manager = SessionManager()
代码完善一下:
let configuration=URLSessionConfiguration.background(withIdentifier: "com.BoxJing.backgroundDownload")
manager = SessionManager(configuration: configuration)
manager.download(videoUrlStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(response.suggestedFilename ?? "video0818.mp4")
return (fileUrl!,[.createIntermediateDirectories,.removePreviousFile])
}
.downloadProgress { (progress) in
print("进度:\(progress)")
}
.response { (downloadResponse) in
print("回调信息:\(downloadResponse)")
}
的确是可以后台下载了,这样还没完!为什么呢?记不记得前一篇文章里提到一个苹果文档里说的,如果有后台下载的任务,任务完成后一定要通知系统!要想通知系统,那肯定要有session的完成后的代理方法urlSessionDidFinishEvents
,直接在工程里搜一下,相信Alamofire里面肯定会有对应的实现。

#if !os(macOS)
/// Tells the delegate that all messages enqueued for a session have been delivered.
///
/// - parameter session: The session that no longer has any outstanding requests.
open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
sessionDidFinishEventsForBackgroundURLSession?(session)
}
#endif
在SessionDelegate类中追踪到了sessionDidFinishEventsForBackgroundURLSession
,熟悉不熟悉,就是出现在前面commonInit
方法里面的!再来看一下:
delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
guard let strongSelf = self else { return }
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
}
很明显会执行manager的一个backgroundCompletionHandler
闭包,那就直接干出来一个试试会执行不会
manager.backgroundCompletionHandler = {
print("下载完成了!!!")
}
在后台下载完成后的确执行了我们写的回调闭包!那我们理论上就是应该在backgroundCompletionHandler
闭包里告诉系统,任务完成了。一顿操作猛如虎:
AppDelegate中:
var backgroundSessionCompletionHandler:(() -> Void)?
//
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
print("Delagete 任务完成")
self.backgroundSessionCompletionHandler=completionHandler
}
backgroundCompletionHandler闭包中:
manager.backgroundCompletionHandler = {
print("下载完成了!!!")
DispatchQueue.main.async {
guard let appDele = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle=appDele.backgroundSessionCompletionHandler else {return}
backgroundHandle()
}
}

完美!但是,看着真的是很不爽!有木有啊,
manager.backgroundCompletionHandler
方法里面的代码跟屎一样的烦人!如何铲掉这坨屎嘞?过程就不再赘述了,可以自行去issue里找一找,这里直接给出思想:单利。直接干代码:
struct BoxBackgroundManger {
static let shared = BoxBackgroundManger()
let manager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.BoxJing.backgroundDownload")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.sharedContainerIdentifier = "group.com.BoxJing.backgroundDownload"
return SessionManager(configuration: configuration)
}()
}
//下载
BoxBackgroundManger.shared.manager.download(videoUrlStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(response.suggestedFilename ?? "video0818.mp4")
return (fileUrl!,[.createIntermediateDirectories,.removePreviousFile])
}
.downloadProgress { (progress) in
print("进度:\(progress)")
}
.response { (downloadResponse) in
print("回调信息:\(downloadResponse)")
}
AppDelegate中:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
print("Delagete 任务完成")
// self.backgroundSessionCompletionHandler=completionHandler
BoxBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}
直接看结果:

非常的完美!!!有木有!到此为止,Alamofire的后台下载完美结束!
网友评论