Realm,为移动设备而生。替代SQLite和Core Data

Realm,为移动设备而生。替代SQLite和Core Data。 —更多信息

Nov 16, 2015

应用沉睡之时:

后台传输服务

作者:Gwendolyn Weston

当用户在下载文件的时候,强制让用户一直保持应用的开启就如同当水壶在烧水的时候,强迫人一直看着水有没有烧开。在本次演讲中,Gwendolyn Weston 将会给大家介绍如何使用 iOS 的后台传输服务(Background Transfer Service) API 在后台下载文件,演讲中还涉及到了实现过程中的常见问题以及使用案例。了解如何在您的应用中轻松地实现此功能,以减少用户的时间,提高用户的满意度。🍵

Transcription below provided by Realm: a replacement for SQLite & Core Data that makes building iOS apps a joy.Check out the docs!

Sign up to be notified of new videos— we won’t email you for any other reason, ever.

About the Author: Gwendolyn Weston

Gwendolyn Weston 是 PlanGrid 的一位开发人员,她负责 Construction Blueprints 应用的版本控制工作。工作之余她喜欢去攀岩,最喜欢的颜色是紫色。

@purpleyay

概述(0:00)

大家好,我是Gwendolyn Weston,是PlanGrid公司的一名开发者。今天在这里我想跟大家谈一谈关于“后台传输服务”的有关知识,我们就只围绕后台下载来进行介绍,教大家如何在应用中实现此项功能。首先,我想先讨论一下如何在前台实现下载机制,因为可能有人对NSURLSession不是很熟悉。然后,我将会谈论如何让这个下载功能请求与后台兼容。最后,我会演示如何避开该功能使用过程中常见的陷阱。

后台传输服务 — 我们用水壶来比喻(0:14)

后天传输服务是 iOS 7 引进的 API,它准许应用暂停或者中止之后,在后台继续执行网络服务(比如下载或者上传)。举个例子,这正是 Dropbox 为什么能够在后台执行同步文件到设备功能的原因。

为了解释这个功能为什么很有用,请试想一下我们有一个水壶。我们将水倒入,按下烧水键,这时候我们本应该离开去做别的事,然而不行,我们必须站在水壶旁边,否则的话水壶就不能烧水了。这是一个非常奇怪的规则,我们不得不遵循它。我们只好站在水壶旁边,拿出手机开始刷微博。当我们看到好友们三三两两的出去游玩,而我们却没有办法,为什么呢?“我们必须站在水壶旁边,否则的话水壶就不能烧水”。于是,这个水壶就不会有人用了,就像前台下载也不会有人喜欢一样。

用例(2:02)

我所在的公司PlanGrid可以被形容为是一个“施工图纸的 GitHub”,它为施工项目提供了版本控制以及项目管理的功能。

有一个很常见的用例:某个承包商登入了网站,标记了一个图纸。这个标记会同步到其他承包商的设备中,以节省人们的时间、精力和金钱,因为人们无需再重新打印这个图纸,每一步变化都能够即时展现。

这就意味着我们的用户经常会上传高清的图纸到我们的仓库当中。每个新加入此项目中的人,通常都需要花费数个小时将所有的图纸同步到设备当中。我们必须告诉用户修改它们的设备设置,取消屏幕自动锁定,将应用一直开着直到下载全部完成。这是不是非常让人恼火?同样这个操作也是一个巨大的安全隐患,因为你不知道一个未锁定的设备在你离开期间会发生什么事情,而这个设备此时往往包含了重要的图纸信息。如果我们能够告诉用户:“点击下载后,你就不用管了,都交给我们了!”,那一切就大有不同了。

没有后台传输服务的下载就像一直盯着水壶烧水那样无趣。而包含这项功能的话,你就可以不用盯着水壶烧水了,你只需设定好相关功能,然后做自己的事情就可以了。

前台下载(4:41)

下面是示例代码:

leturlstring="https://remoteteakettle.com/boiledwater.pdf"letfilepath="Documents/local_teakettle"ifleturl=NSURL(string:urlstring){lettask=NSURLSession.sharedSession().downloadTaskWithURL(url,completionHandler:{(location:NSURL?,response:NSURLResponse?,error:NSError?)inifletloc=location,path=loc.path{try!NSFileManager.defaultManager().moveItemAtPath(path,toPath:filepath)}})task.resume()}

让我们一行一行来看。第一行是我们远程服务器中存放的文件地址。我们前往 remoteteakettle.com 尝试下载一个名为boiledwater.pdf的文件。第二行我们指定了我们想要存储该文件的路径。至于第三行,你会看到从NSURLSession框架中引用了许许多多的成员对象,让我们对其仔细解读。

NSURLSession用于声明和管理网络请求。sharedSession是基于某些默认设置的会话(session)单例。比如说,它使用默认的缓存机制以及超时时间。我们可以自定义自己的会话,不过默认的会话对我们来说就足够了。最后,我们执行基于我们网络请求的NSURLSession的下载任务。

NSURLSessionTask(6:00)

好的,我现在需要暂停一下,先不继续介绍剩余的代码,在此之前,我想先聊一聊NSURLSession下载任务。下载任务实际上是NSURLSessionTask的一个子类,它们实际上有三种类型:

NSURLSessionDownloadTask

NSURLSessionUploadTask

NSURLSessionDataTask(针对即时的网络请求,例如口令验证)

我们通过会话对象来获取我们的会话任务,而不是为了调用某些会话任务的便利构造器方法。也就是说,我们通过会话 URL 来进行反馈。

这意味着什么呢?我喜欢将NSURLSession视为小狗狗,而我们的 URL 则是那些骨头,因此每次我们给它骨头的时候,它就会被欢乐所淹没,然后去抓兔子。而这恰好是 我们想要的NSURLSessionTask。我们可以从同一个NSURLSession中多次获取不同的会话任务,只需要为会话和会话任务创建一对多的关系即可。一个会话可以有多个会话任务,但是每个会话任务只能够给一个会话反馈。在代码中,我们有许多在会话对象中创建任务的方法,比如downloadTaskWithURL(_:completionHandler:)。

好的,我们回到之前的代码,下面是谈论完成处理回调(completion handler) 里面内容的时候了。对于这个完成处理回调来说,它当中有三个参数:

Location (文件即将下载到的临时路径)

Response (可在此获取网络请求的状态码)

Error (如果有问题出现,可在此捕获)

你可能注意到了,我们有对错误进行任何的处理,也没有捕获任何异常。我假设我们的代码处于一个没有错误和异常的世界。因此,我们所有做的就是将文件从临时目录移到在此之前指定的下载路径当中。

代码很成功,但是当用户切到其他应用的时候怎么办?我们不想让我们的这壶热水变成彻头彻尾的耻辱。

后台下载 — NSURLSessionConfiguration(9:23)

要实现后台下载,我们需要使用NSURLSessionConfiguration告知系统这些任务需要后台服务兼容。之前我说过我们可以初始化自定义的会话,这样我们可以自定义某些属性,比如说缓存机制以及超时时间。其实并不是这样的,我们并不会用这个方式来设置会话的某些属性。我们所做的就是在某个“配置对象”中设置这些属性,然后使用该配置来初始化会话。这是我们迈向后台的第一步。

我们通过某种名为后台配置的东西来创建自定义的会话。它只不过是在配置对象上的另一种设置罢了。下面的代码解释得更清晰一些:

letconfig=NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("i am the batman")letsession=NSURLSession(configuration:config,delegate:self,delegateQueue:nil)

我们继续一行一行地看,第一行代码用名为backgroundSessionConfigurationWithIdentifier(_:)的方法创建了一个会话配置,这个标志符可以任意取名,比如说你最喜欢的颜色,Moby Dick 的首字母,或者我们这里写的“I am the batman”。第二行我们用这个配置初始化了一个NSURLSession。我们将这个类本身设为委托,因此它将可以获取该会话接收到的所有委托方法了。最后,我们设置委托队列(delegate queue)。

委托队列可以设置为任意一个您打算用以调用委托方法的队列。不过,如果设置为nil的话,它会使用默认的委托队列。在我们将这些代码加回我们的初始示例之前,我想要强调一下这些标识符必须是唯一的。至于为什么,我们首先需要了解一下后台请求的生命周期。

后台请求的生命周期(11:02)

我们在 Dropbox 应用中执行这个前台网络请求。我们将文件同步到设备中,接着杀死这个应用。这个时候,会话中的所有网络请求仍在继续,这是因为这些请求都在后台进行。当系统回告应用,说“该会话中的所有请求都已结束”的时候,将会调用所有的UIAppDelegate方法,从而让我们知道接下来该做什么操作。

唯一区别是,除了调用application:finishLoadingWithOptions之外,它还会触发一个新的方法,名为application:handleEventsForBackgroundURLSession。这个方法中会传回你所定义的会话标志符。比如说我们之前所定义的“I am the Batman”。整个过程就像这样:名为“I am the Batman” 的会话结束了!之后您打算怎么做呢?无论我们之后是在发生错误时处理错误,还是会话成功完成,我们都必须要在这个方法中重新创建这个会话。至于代码是什么样子的,它实际上意味着使用同一个标识符创建另一个后台配置,然后用这个后台配置创建另一个会话。

funcapplication(application:UIApplication,handleEventsForBackgroundURLSessionidentifier:String,completionHandler:()->Void){letconfig=NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier)letsession=NSURLSession(configuration:config,delegate:self,delegateQueue:NSOperationQueue.mainQueue())session.getTasksWithCompletionHandler{(dataTasks,uploadTasks,downloadTasks)->Voidin// 执行您自己的任务请求!}}

这就是您需要执行的循环链,当我们重新获取了新的会话任务后,系统会知道“会话重新激活,现有的任务都是与该会话建立关联的任务”。

然而,由于系统只知道是哪一个任务是被会话标识符单独重建的,但是如果有两个会话拥有同一个标识符的话,它就力不从心了。这样一来,系统该如何知晓哪一个任务是您想要重新创建的呢?事实上文档中有一个很不好的消息是,多个共享相同标志符的会话行为是不确定的。因此我们不要这样做。现在我们将刚才创建的后台配置的代码放到之前的代码当中去。绝大部分代码都差不多一样,除了这两行新代码:

leturlstring="https://remoteteakettle.com/boiledwater.pdf"letfilepath="Documents/local_teakettle"ifleturl=NSURL(string:urlstring){letconfig=NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))letsession=NSURLSession(configuration:config,delegate:self,delegateQueue:nil)lettask=NSURLSession.sharedSession().downloadTaskWithURL(url,completionHandler:{(location:NSURL?,response:NSURLResponse?,error:NSError?)inifletloc=location,path=loc.path{try!NSFileManager.defaultManager().moveItemAtPath(path,toPath:filepath)}})task.resume()}

陷阱 #1: 没有完成处理回调(13:03)

很遗憾的是,我们并不能让后台请求结束时立即做些什么。当您试图为后台任务创建一个拥有完成处理回调的任务时,控制台会警报说这项功能不被支持。因此我们需要使用委托方法。当应用重新启动以及在handleEventsForBackgroundURLSession()方法中重建会话之后,应用将调用该会话中的所有委托方法。通常情况下,我们一般关注的是URLSession(_:downloadTask:didFinishDownloadingToURL:)这个方法。它将会给予回调:

会话对象

完成的任务

文件被下载到的临时路径

这就是我们要做的第二个步骤。我们将完成处理回调中的代码移到这个委托方法中。下面就是代码应该有的样子:

leturlstring="https://remoteteakettle.com/boiledwater.pdf"letfilepath="Documents/local_teakettle"ifleturl=NSURL(string:urlstring){letconfig=NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))letsession=NSURLSession(configuration:config,delegate:self,delegateQueue:nil)lettask=session.downloadTaskWithURL(url)task.resume()}funcURLSession(session:NSURLSession,downloadTask:NSURLSessionDownloadTask,didFinishDownloadingToURLlocation:NSURL){ifletpath=location.path{try!NSFileManager.defaultManager().moveItemAtPath(path,toPath:filepath)}}

我们使用此委托方法而不是初始化一个带有完成处理回调的下载任务。不幸的是,这个办法也不是很成功,因为filepath在委托方法的识别范围之外。

陷阱 #2: 没有附属请求信息(14:49)

由于我们必须获取不同方法中关于网络请求的相关信息,比如说最终的文件路径,因此我们必须用某种方式将这些信息存储下来。我们可以使用taskDescription来进行处理,但是文档说明“这个属性需要包含用户可读的字符串,也就是说它可以展示在应用界面当中”。由于我们可能会想要存储许多请求信息,比如说文件路径、模型 UUID 或者文件名字等等内容,因此这个方法不是最佳选择。NSURLSessionDownloadTask的子类同样也不是一个好选择,因为我们的NSURLSession只会返回预定义的类,并不能返回我们自定义的子类。

解决方法: 存储请求信息(16:43)

如果系统不给我们提供这个功能,那么我们就自己实现它!我们将要自己存储这些数据:

publicclassHalfBoiledWater:NSObject{publicletsessionId:StringpubliclettaskId:Intpublicletfilepath:Stringinit(sessionId:String,taskId:Int,filepath:String){self.sessionId=sessionIdself.taskId=taskIdself.filepath=filepath}funcsave(){// 保存到数据存储区域}}publicfuncfetchModel(sessionId:String,taskId:Int)->HalfBoiledWater{// 从数据存储区域中检索}

好的,现在我们可以有多种方法来对其进行存储了。我们每个人都有自己最喜欢的数据存储方式。它可能是 FMDB、SQLite,甚至是 Core Data。我打算让大家自行决定,在这两个方法save()和fetchModel(_:taskId:)中自行实现。注意到我们在这个打算放入数据库并执行检索的序列化模型中添加了文件路径。我们同样也使用sessionId和taskId作为主键以便能够存储这个模型。下面是我们添加的示例代码:

leturlstring="https://remoteteakettle.com/boiledwater.pdf"letfilepath="Documents/local_teakettle"ifleturl=NSURL(string:urlstring){letconfig=NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))letsession=NSURLSession(configuration:config,delegate:self,delegateQueue:nil)lettask=session.downloadTaskWithURL(url)ifletsessionId=session.configuration.identifier{letpersistedModel=HalfBoiledWater(sessionId:sessionId,taskId:task.taskIdentifier,filepath:filepath)persistedModel.save()}task.resume()}funcURLSession(session:NSURLSession,downloadTask:NSURLSessionDownloadTask,didFinishDownloadingToURLlocation:NSURL){ifletsessionId=session.configuration.identifier{letpersistedModel=fetchModel(sessionId,taskId:downloadTask.taskIdentifier)ifletpath=location.path{try!NSFileManager.defaultManager().moveItemAtPath(path,toPath:persistedModel.filepath)}}

我们创建了下载任务,还同样创建了所有我们所需要的模型信息,并将其存储在数据库当中。接下来,在我们的委托方法中,当我们需要返回文件路径的时候,我们只需要用会话 ID 以及任务标识符作为键来检索这个模型即可。

结论(18:35)

一般情况下,作为最基本的后台下载来说,这些代码就已经足够了。总的来说,我们首先创建了后台配置用以告知系统我们想让该会话中所有的任务都能够在后台运行。接着,我们将代码从完成处理回调中移到了委托方法里面。然后,我们用某种方法存储了我们在委托方法中所需的请求信息,这样我们就可以在需要的时候检索调用。

这并没有结束,借助这个 API 我们还有许多事情可以做。我们可以显示进度,取消下载,或者在错误发生的时候重置下载等等。此外,还有很多潜在的问题等着你去发现,去解决。

我希望本次讲座能够帮助大家将后台下载功能添加到应用当中,这样用户就不必一直开着前台等待下载了。谢谢大家!

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

推荐阅读更多精彩内容