
用户期望网络“神奇地”工作并且未被注意。 这种魔力取决于系统和应用程序的开发人员。 影响系统非常困难,因此我们将限制自己的应用范围。
这个主题很复杂,存在无数问题。 我们将讨论最近几个月遇到的问题。 我马上为您的音量道歉。 简而言之,绝对没有太多值得关注的小事情。
首先,让我们处理术语。
数据传输有两个方向:
- 下载 (下载,从服务器下载数据),
- 上传 (将数据发送到服务器)。
该应用程序可能处于活动状态,但可能在后台运行。 正式地,他还有其他州 ,但我们仅对以下州感兴趣:
- 背景 (最小化应用程序时),
- 活动 (当应用程序处于活动状态时,在屏幕上)。
有用的模式: 回调 , 委托 ( Cocoa Design Patterns , 关于Wikipedia上的回调 )。 您还需要知道 URLSession
(在文章中,链接还提到了网络的后台工作,但要顺便提及)。
所有示例均以Swift 5编写,可在iOS 11及更高版本(在iOS 11和12上测试)上运行,并假定使用常规HTTP请求。 从iOS 9开始,所有这些功能大部分都可以使用,但有一些细微差别。
使用网络的一般方案。 URLSession
使用网络并不是特别困难:
一个简单的示例如下所示:
let session = URLSession(configuration: .default) let url = URL(...) let dataTask = session.dataTask(with: url) { data, response, error in ...
该方案对于各种任务都是类似的,只有很小的变化。 在用户关闭应用程序之后,直到我们不需要继续使用网络之前,一切都相对简单。
我立即注意到,即使在这种情况下,也有很多有趣的事情。 有时您需要使用棘手的重定向,有时需要授权,SSL固定或全部。 您可以阅读很多有关此的内容。 由于某些原因,在后台状态下使用网络的工作描述得少得多。
创建一个在后台工作的会话
后台URLSession和通常的URL有什么区别? 它可以在应用程序外部,系统内部某个地方工作。 因此,在完成申请过程时,它不会“死亡”。 它称为后台会话(以及应用程序的状态,这有点令人困惑),并且需要特定的设置。 例如,这:
let configuration = URLSessionConfiguration.background(withIdentifier: "com.my.app") configuration.sessionSendsLaunchEvents = true configuration.isDiscretionary = true configuration.allowsCellularAccess = true configuration.shouldUseExtendedBackgroundIdleMode = true configuration.waitsForConnectivity = true URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
该配置还有许多其他参数,但是这些参数直接与后台会话有关:
- 标识符 (在初始化程序中传递)是一个字符串,用于在应用程序重新启动时匹配后台会话。 如果应用程序重新启动,并且您使用另一个后台会话中已经使用的标识符创建了一个后台会话,则新会话将可以访问上一个会话的任务。 由此得出的结论很简单。 为了正确操作,您需要此标识符对于您的应用程序是唯一的并且是永久的(例如,您可以使用bundleId应用程序的派生类 );
- sessionSendsLaunchEvents指示完成数据传输后,后台会话是否应启动应用程序。 如果将此参数设置为
false,
则触发器不会发生,并且应用程序下次启动时将接收所有事件。 如果该参数为true
,则在数据传输完成之后,系统将启动应用程序并调用相应的AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:)
completeHandler AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法; - isDiscretionary使系统可以更少地安排任务。 一方面,这可以延长电池寿命,另一方面,它可以减慢任务速度。 或加快速度。 例如,如果下载量很大,则系统将能够暂停任务,直到它连接到WiFi,然后快速下载所有内容,而不会花费缓慢的移动互联网(如果允许的话,下一步是什么)。 如果任务是在应用程序已经在后台运行时创建的,则此参数会自动设置为
true
; - allowCellularAccess-一个参数,显示您可以使用蜂窝通信与网络配合使用。 我没有仔细地和他玩耍,但是根据评论,那里(连同类似的系统切换)布置了大量的耙子。
- shouldUseExtendedBackgroundIdleMode。 一个有用的参数,表明当应用程序进入后台时,系统应与服务器保持更长的连接时间。 否则,连接将断开。
- 在移动设备中, waitsForConnectivity可能会在短时间内消失。 此时创建的任务可以挂起直到出现连接,或者立即返回“无连接”错误。 该参数允许您控制此行为。 如果为
false,
则在没有通信的情况下,任务将立即因错误而中断。 如果为true
,请等待直到出现链接。 - 最后一行(会话初始化程序)包含一个重要的参数, 委托。 关于他-多一点。
委托与回调
正如我上面所说的,有两种方法可以从任务/会话中获取事件。 第一个是回调:
session.dataTask(with: request) { data, response, error in ... }
在这种情况下,任务完成事件将发送到闭包,您需要在此处检查是否存在错误,答案中的内容以及到达的数据。
使用会话的第二个选项是通过委托。 在这种情况下,我们必须创建一个实现URLSessionDataDelegate
协议和(或)附近的协议的类(对于不同类型的任务,协议略有不同)。 对此类实例的引用位于会话中,并且在将事件传递给委托时调用其方法。 链接可以由初始化程序注册在会话中。 在示例中, self.
URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
对于常规会话,两种方法都可用。 后台会话只能由委托使用。
因此,我们设置了会话,创建了会话,让我们看一下如何下载内容。
在后台下载数据的通用方案
要下载数据,通常需要形成一个 (URLRequest)
,在其中注册必要的参数/标头/数据,创建一个URLSessionDownloadTask
并运行它以执行。 像这样:
var request = URLRequest(...)
此时,与通常的下载任务没有什么大不同。 的确, 出现了两个参数countOfBytesClientExpectsToSend / countOfBytesClientExpectsToReceive ,它们显示了我们计划在请求中发送并在响应中返回的数据量。 这是必要的,以便系统可以更正确地计划任务工作,更快地下载文件而不会过度工作。 这些值不一定是准确的。
在resume()
任务将执行。 在数据传输过程中,将传输进度(关于进度-请阅读下面的内容,那里还有选项),完成后将执行几种委托方法。 其中,有一项非常重要:
urlSession(_:downloadTask:didFinishDownloadingTo:)
事实是,下载是在一个临时文件中进行的,此后,应用程序就有机会将该文件移到某个位置或对其进行其他操作。 此临时文件仅在此方法内可用,退出该文件后,该文件将被删除,无法对其进行任何处理。
在此重要方法之后,将调用另一个方法,如果发生错误,该错误将落在该位置。 如果没有error
,则error
为nil.
urlSession(_:task:didCompleteWithError:)
如果应用程序进入后台或已完成,最终会发生什么? 如何调用委托方法? 这不容易。
如果由应用程序启动的内容的下载已完成,并且sessionSendsLaunchEvents
标志位于会话配置中,则系统将启动应用程序(在后台)并在AppDelegate,
调用应用程序(_:handleEventsForBackgroundURLSession: AppDelegate,
:)方法。
在这种方法中,应用程序应:
- 保存
completionHandler
(一段时间后,需要在主线程中异步调用它); - 重新创建一个与以前具有相同标识符的后台会话(如果有多个后台会话,则将其传递给此方法);
- 在新创建的会话中,事件将到达委托(特别是非常重要的
urlSession(_:downloadTask:didFinishDownloadingTo:)
),您需要对其进行处理,并在所需的位置复制文件; - 在调用所有方法之后,将调用另一个委托方法,该方法称为
urlSessionDidFinishEvents(forBackgroundURLSession:)
,在其中必须调用之前存储的completionHandler.
处理程序completionHandler.
这很重要。 必须使用DispatchQueue.main.async(...)
在主线程中调用completionHandler
DispatchQueue.main.async(...)
。
同时,您需要记住所有这些都是在后台运行的应用程序中发生的。 这意味着资源(执行时间)有限。 快速将文件保存在需要的位置,更改应用程序中的必要状态并关闭-这就是所有可以完成的事情。 如果要执行更多操作,可以使用UIApplication.beginBackgroundTask()
或新的BackgroundTasks 。
通用背景数据发送方案
将文件上传到服务器也有限制。 但是,一切都以类似的方式开始:我们形成一个请求,创建一个任务(现在将是URLSessionUploadTask)
,运行该任务。 怎么了
问题是我们如何创建请求。 通常,我们将发送的数据形成为Data
。 后台URLSession,
不知道如何使用它。 并与流式请求( uploadTask(withStreamedRequest:)
)也不知道如何。 有必要将所有需要发送的内容写入文件,并从该文件创建发送任务。 原来是这样的:
var fileUrl = methodThatSavesFileAndRetursItsUrl(...) var request = URLRequest(...) let task = session.uploadTask(with: request, fromFile: fileUrl) task.resume()
但是不需要注册大小, URLSession
可以自己查看。 发送后,将与下载时一样urlSession(_:task:didCompleteWithError:)
相同的委托方法urlSession(_:task:didCompleteWithError:)
。 就像这样,如果应用程序在发送过程中被杀死或进入后台,则application(_:handleEventsForBackgroundURLSession:completionHandler:),
将到达application(_:handleEventsForBackgroundURLSession:completionHandler:),
必须按照与下载数据时相同的规则进行处理。
什么是完整的申请表?
要测试后台下载和发送,您需要模拟应用程序的完成情况(网络的后台工作是专门设计用来承受这种情况的)。 怎么做? 最初-没办法。 也就是说,没有常规的(授权的,公开的)方法可以做到这一点。 让我们看看耙子在哪里。
- 首先,仅关闭应用程序(通过按“主页”按钮或做出适当的手势)将不起作用。 这不会杀死应用程序,而只会将其发送到后台。 使用后台会话的含义是,即使应用程序被“完全,完全”杀死,它也可以正常工作。
- 其次,您无法启用调试器(AppCode,Xcode或仅LLDB)的连接,即使在“关闭”一段时间后,它也不会使应用程序死亡。
- 第三,您无法从任务栏(任务管理器,双主屏幕或缓慢向上滑动“向上”)终止应用程序。 因此,被杀死的应用程序被视为“永久”被杀死,并且该系统连同该操作一起停止与该应用程序相关联的后台会话。
- 第四,您需要在真实设备上测试此过程。 日志记录没有问题(请参见下文),并且已对其进行了更多调试。 有人认为模拟器也应该按其应有的方式工作。 但是我注意到莫名其妙的怪异之处,除了模拟器故障外,我无法用其他任何方式来解释。 通常,在设备上进行测试;
- 唯一可行的方法是使用
exit(int)
函数。 众所周知,您无法将其上传到服务器( 这与要求直接矛盾 ),但是目前我们仅在测试-这并不可怕。 我知道使用此功能的两个合理选择:
- 在
AppDelegate.applicationDidEnterBackground(_:)
方法中自动调用它,以使应用程序在退出跳板后立即被杀死; - 通过单击,在界面中制作组件(例如,按钮或将动作挂在手势上),方法
exit(...).
在这种情况下,该应用程序将被终止,并且与网络的后台工作应继续进行。 而且,一段时间后,我们应该调用application(_:handleEventsForBackgroundURLSession:completionHandler:).
如果无法使用Xcode调试控制台,如何记录应用程序?
好吧,那是不可能的。 如果您确实愿意,可以。 您不能从Xcode开始,并且例如,如果应用程序由于系统事件而已经重新启动,则可以将应用程序附加(附加到进程)并出队。 但是这种解决方案很一般,您需要以某种方式测试重新启动过程本身。
您可以使用协议(日志,日志) 。 它们的实现有几种选择:
print.
它通常被用作“让我们快速得到一些东西”。 在我们的情况下,由于无法访问设备上的控制台,因此无法使用该应用程序。NSLog.
由于它使用第三种方法,因此它将起作用。os_log.
最正确的方法,它使您可以正确配置日志,将日志附加所需的类型,在调试后禁用,而无需剪切代码本身,等等。
注意! 对于os_log
存在一些问题(例如,缺少调试日志),这些问题只能在模拟器中播放,而不能在此设备上播放。 使用设备。
如何使用os_log,
阅读Apple文档中的如何正确配置它。 特别是,您应该启用debug
和info
日志,默认情况下它们是隐藏的。
跟踪下载或发送数据的进度
在数据传输过程中,我想了解已经发送了多少,还剩下多少。 有两种方法可以做到这一点。 第一种是使用委托方法:
- 要发送,您需要使用
urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
- 有类似的
urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)
下载方法urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)
每次下载或发送下一个数据时,都会调用这些方法。 它们不一定与完成过程的方法一致;也可以在数据完全下载或发送之后调用它们,因此,不可能确定“一切都已完成”。
第二种方法更有趣。 事实上,每个任务都提供一个Progress
类型的对象(位于task.progress
字段中),该对象可以监视任意过程,包括数据传输过程。 他有多有趣? 两件事:
- 在
Progress
对象中,您可以创建一个任务执行树,该树的每个节点将显示其包含的所有任务的Progress
。 例如,如果您需要发送五个文件,则可以为每个文件取得进度,取得总体进度,向其中添加五个其他文件,并监视一个(父文件的)进度,并将其更新链接到某个界面元素; - 您可以将进度添加到此树中,也可以暂停和取消与添加进度相关的操作。
这与后台下载或发送数据有什么关系? 没办法 委托方法不会被调用,并且在应用程序终止时进度对象会死亡。 对于后台会话,此方法不合适。
将任务从常规会话“转移”到后台会话
嗯,进行后台会议更加困难。 但这很方便! 不会丢失任何一项任务,我们将获得请求的所有数据,为什么不总是使用后台会话?
不幸的是,她有缺陷,也有严重缺陷。 例如,后台会话较慢。 在我的实验中,速度变化了数倍。 其次,任务的后台执行可能会延迟(特别是如果isDiscretionary
了isDiscretionary
参数,正如我提到的那样,对于在后台运行应用程序时创建的任务始终是true
的。
因此,每次创建任务时,都需要准确了解其工作的标准,将任务添加到常规或后台会话的位置。 正常运行更快,立即启动。 背景-更长(不是立即),但是如果用户关闭应用程序,则不会被杀死。
如果没有明显的了解该任务应该在后台会话中执行(例如,非关键性传输大量数据,例如同步或备份),那么值得这样做:
- 在常规会话中启动任务。 在这种情况下,请运行backgroundTask,以便系统了解我们需要时间来完成任务。 这会花费一些时间(最多几分钟,但是在iOS 13中发生了故障,目前尚不清楚发生了什么),因此可以完成任务。
- 如果没有时间,则在backgroundTask结束时, 我们将任务从常规会话转移到后台会话,在该会话中它将继续工作,并在可能的时候结束。
如何转让? 没办法 只需杀死(取消)通常的任务,然后创建一个相似的(具有相同的请求)背景即可。 为什么将其称为“转移”? 以及为什么用引号引起来?
没有传输来发送数据。 正是所描述的。 他们杀死了一个任务,启动了另一个任务,第一次发送的所有数据都丢失了。
对于下载,情况有所不同。 系统知道请求下载到哪个文件。 例如,如果您运行多个任务来下载相同的URL,它将不会多次执行该请求。 数据被下载一次,然后最终的委托方法(或回调)将被执行多次。 此处描述的实验证实了这一点。 与浏览器中一样,很可能在内部使用标准的HTTP缓存。
这是执行此操作的示例代码:
let request = URLRequest(url: url) let task = foregroundSession.downloadTask(with: request) let backgroundId = UIApplication.shared.beginBackgroundTask { task.cancel() let task = backgroundSession.downloadTask(with: request) task.resume() } task.resume()
如果任务在expirationHandler
工作之前完成,则必须记住调用UIApplication.shared.endBackgroundTask(backgroundId)
。 文档中对此进行了更详细的描述 。
为了帮助系统继续下载(例如,取消它可能导致临时文件在后台下载恢复之前被删除),有一些特殊方法:
let request = URLRequest(url: url) let task = foregroundSession.downloadTask(with: request) let backgroundId = UIApplication.shared.beginBackgroundTask { task.cancel { data in let task: URLSessionDownloadTask if let data = data { task = backgroundSession.downloadTask(withResumeData: data) } else { task = backgroundSession.downloadTask(with: request) } task.resume() } }
,
日志
— , . — , . background , .
, , background -, , , ( UI, ). , , — . , — , , os_log.
( NSLog)
-
- , . , - . , , , ( ) . , , -, , . — — , . — , - ( ), , .
. ( ), . , , , .
局限性
:
- ,
(task.taskIdentifier)
, (Dictionary). , 1, . - ,
URLSession.getAllTasks
. , background . , . , . ¯\_(ツ)_/¯ - , , , , .
, background , . , - . : https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1 . , :
If your app extension initiates a background NSURLSession task, you must also set up a shared container that both the extension and its containing app can access. Use the sharedContainerIdentifier property of the NSURLSessionConfiguration class to specify an identifier for the shared container so that you can access it later.