iOS的下载功能实现

1,首先要确定下载好的文件放在哪里

放在缓存目录( NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0])是不行的,因为系统不知道什么时候就会把缓存给清了,这可能会导致用户下载的文件丢失。

所以应该放在用户的document目录,也就是 NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0];

可以在这个目录下新建一个xxx.xxx.download文件夹,作为下载目录。
这里还需要注意: iOS在备份的时候会默认备份放在document目录下面的文件,而一般来说,可下载的内容一般是不需要备份的,
可以手动设置这个下载目录为不需要备份

       do {

            try kOJSFileManager.createDirectory(atPath: kWHGSaveDirectoryPath, withIntermediateDirectories: false, attributes: nil)

            var resourceValues = URLResourceValues()

            resourceValues.isExcludedFromBackup = true

            var url = URL(fileURLWithPath: kWHGSaveDirectoryPath)

            try url.setResourceValues(resourceValues)

        } catch {

            NSLog(error)

        }

2,确定下载所用的网络连接类

其实没啥好选的, iOS7以后已经全面开始推广URLSession了,而且URLSession功能确实很强大,这里需要确定是,是用AFNetworking又封装了一层的AFHTTPSessionManager呢,还是用系统自带的URLSession,区别是AFNetworking提供了block和delegate的回调,而系统的只有delegate的回调方式。这个看个人喜好了,我自己是更倾向于用AFNetworking的,毕竟简单好用才是硬道理。

然后看 URLSessionConfiguration,

    open class var `default`: URLSessionConfiguration { get }

    open class var ephemeral: URLSessionConfiguration { get }

    @available(iOS 8.0, *)

    open class func background(withIdentifier identifier: String) -> URLSessionConfiguration

因为要支持后台下载,所以只能用background,这里需要一个identifier,用来标识session,当重新启动应用时,如果创建和之前有相同identifier的session,系统会找到对应的session数据,并响应-URLSession: task: didCompleteWithError:方法

3,NSURLSessionTask

一个下载任务对应一个task,一个URLSession可以对应多个task,而NSURLSessionTask是一个基类,有四个子类:

1)NSURLSessionDataTask,这是一般的网络请求用的类,不支持后台传输,切换后台会终止下载。AFNetworking的get,post方法都是用的这个类

有几点需要注意,调用cancel方法会立即进入-URLSession: task: didCompleteWithError这个回调;调用suspend方法,即使任务已经暂停,但达到超时时长,也会进入这个回调,可以通过error进行判断;当一个任务调用了resume方法,但还未开始接受数据,这时调用suspend方法是无效的。也可以通过cancel方法实现暂停,只是每次需要重新创建NSURLSessionDataTask。

2)NSURLSessionUploadTask,继承自NSURLSessionDataTask,内容以NSData对象返回,协议方法中可以查看请求时上传内容的过程,支持后台传输。

3)NSURLSessionStreamTask,建立了一个TCP/IP连接,替代NSInputStream/NSOutputStream,新的API可异步读写,自动通过HTTP代理连接远程服务器。

4)NSURLSessionDownloadTask,支持断点续传,资源会下载到一个临时文件,下载完成需将文件移动至想要的路径,系统会删除临时路劲文件,暂停时,系统会返回NSData对象,恢复下载时用这个data创建task,支持后台传输。AFNetworking的下载方法也是用这个类。

downloadTask有两种创建方式,用resumeData就能实现断点续传,直接用request就是从头开始下载。

用 open func cancel(byProducingResumeData completionHandler: @escaping (Data?) -> Void)方法即可产生resumeData。

4,具体下载方法就比较简单了,我这里直接用AFNetworking的download方法。

5,下载的model

把要下载的数据定义为一个model,这个model包含下载的url,保存地址,唯一标志符,下载状态,目标文件大小,已下载的大小等。

其中下载状态包括{none, downloading, waiting, paused, finished, failed}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

综上所述,大体的结构应该是这样:

有一个downloadManager来管理下载相关事务,
它有一个downloadSession来创建下载任务,下载指定类型的model;
有一个dataArray来保存各个状态的model,可以用只读的方法分成finishedArray,downloadingArray等;
这个dataArray应该是可以保存到本地的,不管是用序列化还是数据库,重启的时候应该可以重新拿到这些内容;
需要实现downloadSession的各种回调。

5,下载进度更新

首先将下载进度保存到下载的model当中,然后可以用通知或者kvo的方式来实时改变进度条。

这里稍微注意一下,让ViewController来做observer是不太合适的,下载进度更新很快,每次都在ViewController让tableView reloadData显然是不行的,

所以还是应该让cell自己来监听,而cell是复用的,配置cell用的数据可以一开始是model1,滑动几下就变成model2,model3了,

用通知的话,要判断发送通知的下载任务的model和当然配置cell的model是同一个model才可以更新;

用kvo的话,在要配置cell时,移除对原来model的监听,添加对新model的监听。

6,允许蜂窝网络下载

NSURLSessionConfiguration本身就有一个属性allowsCellularAccess,默认为YES,允许蜂窝网络下载。但是对于正在下载的任务,修改这个属性是无效的,即我们已经通过session创建了task对象,开启了任务,再试图用session.configuration.allowsCellularAccess = NO;去修改这个选项是无效的。如果一定要用这个属性修改这个选项,那么只能重新创建session。

所以如果想达到设置属性或者切换网络就暂停下载的效果,还是需要自定义一个allowsCellularAccess属性,在设置属性的时候,手动暂停正在下载的任务

7,支持后台下载

支持后台下载,首先要用background的URLConfigSession,

然后在AppDelegate中实现方法:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)

在其中保存completionHandler这个block;

然后在URLSession的delegate方法

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

中,调用这个block;

借用别人的流程图:(https://www.jianshu.com/p/1211cf99dfc3

image.jpeg

当APP进入后台时,系统会接管这个background的URLSession,当下载任务完成时,系统会调用AppDelegate的handleEventsForBackgroundURLSession方法,

关于completionHandler的作用,可以参考https://www.jianshu.com/p/2ccb34c460fd

8,断点续传

断点续传的功能URLSession已经封装实现了,就是靠resumeData,这个resumeData并不是真正的已经下载的文件,它只是一个描述已下载文件的描述文件,相当于用迅雷下载的时候产生的cfg文件,真正的下载中的文件应该是系统下载到临时文件夹了,下载完成后会将文件从临时文件夹移动到指定的下载目录。

需要注意的是iOS10,iOS10.1系统中(也有说法是iOS11和iOS11.1),downloadTaskWithResumeData方法有问题,需要特殊处理,参考https://benscheirman.com/2016/09/resume-data-broken-in-ios-10/,在iOS11.2之后的版本应该是已经修复了。

注意:如果想要APP在后台被kill掉时依然保存resumeData,那就不能使用AFNetworking的download方法中的那个几个block,因为AFNetworking文档有提到,这些block可能会丢失(因为是保存在内存里的嘛,没有本地化,当APP被kill掉时就没了),只能用delegate的方式,或者用session的 setXXXBlock方法,这些方法在session重新生成的时候会重新调用,所以不存在丢失的问题。

注意:正在下载的过程中,手动kill掉APP,这时候是拿不到resumeData的,resumeData是系统保存起来的,当APP再次启动,重新创建了跟原来backgroundSession的identifier一致的session时,系统会调用这个session的delegate方法

 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

这个error的userInfo中,会有resumeData和task的url等信息。

在APP willTerminate的通知里调用cancelDownloadByProducingResumeData也是是不行的,因为这个方法是异步的,需要执行时间的,可能还没执行完APP就已经被kill了

另外,貌似直接调用task的resume是可行,会直接开始断点续传,但这种操作应该不是用户希望看到的。所以还是保存下来resumeData,将任务设置为paused,等用户手动开启比较好。

还有,如果APP被kill了,那delegate方法中拿到的task,跟原来开始的task不是同一个对象,但是它们所有属性都相同(我也没有一个一个挨个看,task的指针地址是不一样的,但是taskIdentifier,taskDescription,currentRequest等都是相同的)。这里尤其需要注意的是,如果用了AFNetworking,AFURLSession在初始化的时候,会调用getTasksWithCompletionHandler方法
再其block回调中会调用addDelegateForDownloadDataTask方法,这个方法会修改task的taskDescription,所以就算之前自己手动修改了task的taskDescription,重启后获取的还是会不一样。。。这个稍微有点坑!!!

还有一个特别容易坑的地方,也是使用AFNetworking才会有的。就是didCompleteWithError方法调用时机的问题,AFURLSession内部是用delegate方式实现回调的,提供给外部block的回调方式。而didCompleteWithError方法会在session初始化的时候( + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;)就调用,(目测应该是在设置delegate后的下一个runloop或几个runloop之内)因为session初始化后对session设置block回调是可以的,打断点观察delegate方法的调用也在getTasksWithCompletionHandler方法的回调之后,可能是新建delegateQueue需要一点时间。总之,如果在session初始化后没有立刻给session设置block回调,而是执行了某些可能会耗时的操作(比如io读写之类),那可能在设置block回调之前delegate方法就被调用了,从而导致回调block没有没触发!!!AFURLSession内部是用delegate的方式,所以不存在这个问题~

9,下载链接失效的问题

如果下载链接失效了,继续用这个链接下载的话,就会触发didCompleteWithError方法但是error可能为nil,这时候貌似只能用task.response的statusCode来判断,注意!!!

参考:

https://blog.csdn.net/hero_wqb/article/details/80407478

https://benscheirman.com/2016/09/resume-data-broken-in-ios-10/

https://www.jianshu.com/p/1211cf99dfc3

https://www.jianshu.com/p/2ccb34c460fd

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

推荐阅读更多精彩内容