Alamofire之SessionManager

2,983 阅读5分钟

一、SessionManager流程分析

在使用Alamofire发起网络请求时,我们一般会使用 request 方法请求一个网络地址。如下所示:

let urlBD = "https://www.baidu.com"

Alamofire.request(urlBD).response { (response) in
    print(response)
}

在上面👆的代码中,我们请求了百度的地址。

我们来查看一下 request 的内部实现是什么样子的呢?

从源码中可以看到 request 有多个参数,除了必传的 url 外都有默认值。然后返回了一个 SessionManager.default.request

小伙伴们应该知道,在iOS进行网络请求的时候,是调用的 URLSession.dataTask,如下:

let session = URLSession.init(configuration: .default)

let task = session.dataTask(with: URL(string: urlBD)!) { (data, response, error) in
    
    print(response)
}

task.resume()

那么为什么在Alamofire中不直接使用 URLSession,而要使用 SessionManager 做一层封装呢?

SessionManagerdefault方法中,使用默认的 URLSessionConfiguration,并添加了Alamofire自身的一些参数到配置中。然后初始化 SessionManager

init 函数中,除了参数 configuration外,还有 delegate 参数。并且这个参数的默认值 SessionDelegate() 被传给 URLSession 作为其代理。通过查看源码,SessionDelegate 已经实现了 URLSessionDelegateURLSessionTaskDelegateURLSessionDataDelegateURLSessionDownloadDelegateURLSessionStreamDelegate的代理方法。

小伙伴都知道,在直接使用 URLSession 做网络请求时,一般都会将其代理设置为当前的 ViewController,而这里将其代理移交给 SessionDelegate()的目的,就是为了便于对请求作统一处理,不用让开发者在每次发起网络请求时,都去处理其回调。

我们继续查看 commonInit 函数的源码:

commonInit 函数中,delegate.sessionManager 被设置为自身 self,而 self 其实是持有 delegate 的。那么这里会不会造成循环引用呢?答案是不会。因为 delegatesessionManagerweak 属性。

这里将 sessionManagerdelegate 是为了 1、在 delegate 处理回调的时候可以将消息回传给 sessionManager。 2、在 delegate 处理回调时,将不属于自身的业务交给 sessionManager 统一调度处理。 3、减少 delegate 与其他业务的依赖。

小伙伴们还要注意这里的 sessionDidFinishEventsForBackgroundURLSession闭包

在处理后台请求时,需要用到。

以上就是 SessionManager 的源码分析。 SessionManager 初始化完成后,就直接调用 request 方法请求数据了。此处先不赘述。

二、URLSession后台下载

小伙伴们在项目中可能会遇到后台下载的情况,可能很多小伙伴会觉得这个比较难,但其实 URLSession 的后台处理还是很简单的,下面我们来简单实现一下。

// 初始化一个background的模式的configuration
let configuration = URLSessionConfiguration.background(withIdentifier: "BOBackground")

// 通过configuration初始化网络下载会话
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

// 创建下载URL,下载QQ的安装包
let downloadUrl = URL(string: "https://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg")!

// session创建downloadTask任务
let task = session.downloadTask(with: downloadUrl)

// 启动下载任务
task.resume()

上面的代码就是一个简单的下载QQ安装包的下载任务。

  • 因为需要使用后台下载,所以需要初始化一个 background 模式的 configuration
  • 为了显示下载进度以及将下载文件移动到指定的路径,所以需要设置代理。
  • 下载任务 downloadTask 创建后,默认是挂起状态,所以需要调用 resume() 启动下载任务。

再实现代理方法。

extension ViewController: URLSessionDownloadDelegate {
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        
        // 下载完成 - 开始沙盒迁移
        print("下载完成 - \(location)")
        let locationPath = location.path
        //拷贝到用户目录
        let documnets = NSHomeDirectory() + "/Documents/" + "QQ" + ".dmg"
        print("移动地址:\(documnets)")
        //创建文件管理器
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        print(" 当前下载: \(bytesWritten)\n 已下载: \(totalBytesWritten)\n 预计需下载: \(totalBytesExpectedToWrite)")
        
        print("下载进度: \(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))\n")
    }
    
}

实现上面两个协议方法来显示下载进度,以及将文件下载到指定路径。

我们就实现了后台下载了吗?我们是实际运行一下。发现确实可以下载了,但是当APP进入后台后,下载就停止了,并没有实现后台下载。

经过一番资料查找(苹果官方文档),告诉我们,还有最后一个步没有实现。

首先我们需要在 AppDelegate

通过 UIApplicationDelegate 协议方法获取到completionHandler 闭包保存起来。

ViewController 中,通过实现协议方法 urlSessionDidFinishEvents(forBackgroundURLSession:),执行这个闭包

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    
    print("后台任务下载回来")
    
    DispatchQueue.main.async {
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
            let backgroundHandle = appDelegate.backgroundCompletionHandler else { return }
        
        backgroundHandle()
    }
}

添加了以上的代码后,再运行就可以实现后台下载了。

三、Alamofire后台下载

在上一节中,我们实现了 URLSession 的简单后台下载,那么在 Alamofire 中又该如何实现后台下载呢?

小伙伴们可能会仿照第一节中进行网络请求那样,调用 Alamofire.download 方法,进行下载。运行代码似乎也可以下载,但是却不能后台下载。

查看其源码,Alamofire.download 调用的是 SessionManager.defaultdownload 方法。在第一节中已经分析过了,SessionManager.default 使用的是 URLSessionConfiguration.default 默认配置。但是在 URLSession 的后台下载应该使用 URLSessionConfiguration.background

所以我们应该自定义一个 SessionManager,并且使用 URLSessionConfiguration.background 初始化 SessionManager

struct BOBackgroundManager {
    
    static let `default` = BOBackgroundManager()
    
    let manager: SessionManager = {
       
        let configuration = URLSessionConfiguration.background(withIdentifier: "BOBackground")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        
        return SessionManager(configuration: configuration)
    }()
}

定义 BOBackgroundManager 单例来处理后台下载。

let downloadUrl = "https://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg"

BOBackgroundManager.default.manager
    .download(downloadUrl, to: { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
        
        let documentUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        let fileUrl     = documentUrl?.appendingPathComponent(response.suggestedFilename!)
        return (fileUrl!,[.removePreviousFile,.createIntermediateDirectories])
    })
    .downloadProgress { (progress) in
        print("下载进度:\(progress)")
}

然后使用 BOBackgroundManager.default.manager 开启下载,如上代码。

但是如果需要后台下载,还需要处理在 AppDelegate 中处理 completionHandler

因为 SessionDelegate 已经实现了 func urlSessionDidFinishEvents(forBackgroundURLSession:) 方法。

并且会调用 SessionDelegate.sessionDidFinishEventsForBackgroundURLSession 闭包。

而在第一节分析 SessionManagercommonInit 函数时,已经知道会设置 SessionDelegate.sessionDidFinishEventsForBackgroundURLSession 闭包。并在闭包内部执行 SessionManager.backgroundCompletionHandler 闭包。所以我们只需要在 AppDelegate 中将获取到的 completionHandler 闭包,保存在 SessionManager.backgroundCompletionHandler 即可。

// 通过该方法获取到completionHandler
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    
    BOBackgroundManager.default.manager.backgroundCompletionHandler = {
        print("下载完成了")
        
        completionHandler()
    }
}

为了方便调试,所以添加 print 函数。

经过以上的设置,即可使用 Alamofire 实现后台下载。

本篇名字虽然是Alamofire之SessionManager,但是更多的在记录后台下载的实现。也算是对自己平时项目中所用的一种总结吧。若有不足之处,请评论指正。