前几天忽然收到一朋友发来的消息, 说是在 iOS 12 上遇到了一个新的 BUG, 问我怎么看? 我说新系统遇到 BUG 不是很正常吗? 大概是个什么状况?
通过朋友说明, 大概是这么个现象: 他用了一个第三方下载管理器进行视频下载, 明明是设置了后台下载的, 但 App 一推到后台再回到前台, 下载进度就不动了, 但任务依然还在继续下载. 系统是 iOS 12, 手机是 iPhone 7.html
刚一开始还觉得第三方在进度处理方面写的有问题, 但我把这个第三方的 Demo 下载运行后, 发现这根本不是第三方问题, 而是系统问题, 系统代理 -[NSURLSessionDownloadTask URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:]
根本没有被调用, 因此下载进度根本就没法继续计算.
而后我改成使用 KVO 监听 NSURLSessionDownloadTask
的 countOfBytesReceived
和 countOfBytesExpectedToReceive
属性来计算当前下载进度, 但很遗憾, 这两个值在重回前台后就没在继续变化, 初步认定是系统在处理数据接收时出现了异常, 致使省略了值的改变, 还有顺便躺枪的进度代理.
上一次遇到这种系统犯法失效的 BUG 仍是在 iOS 11.1/11.2 上, 当时开发录屏直播, 系统方法 -[RPBroadcastSampleHandler processSampleBuffer:withType:]
没有被调用, 直接坑掉了一个大功能模块, 但幸亏, 这一回遇到的 BUG 不算严重, 解决方法仍是有的.ios
这回的进度 BUG 在虚拟机上是不会出现的, 必须真机, 并且通过测试, 发现只在 iOS 12/12.1, iPhone 8 如下才会出现.
在测试时还发现 App 彻底退出后, 后台下载任务会直接取消, 可是带有恢复数据.
进入前台后, 手动进行 暂停->继续
操做后, 代理/KVO 就会继续工做.git
既然手动 暂停->继续
能够修复 BUG, 那只要用代码重现一遍就能够了吧? 别急, 事情没有那么简单.
直接在 -[AppDelegate applicationWillEnterForeground:]
开始遍历全部下载任务, 都执行一遍 暂停->继续
操做, 这个方法很简单, 很粗暴, 但, 这无论用!
那么使用 -[NSURLSessionDownloadTask cancelByProducingResumeData:]
-> -[NSURLSession downloadTaskWithResumeData:]
代替 暂停->继续
呢? 不错, 意识到当前的 NSURLSessionDownloadTask
可能存在脏数据是个进步, 但, 这依然无论用!github
最后的最后, 仍是测试出来了, 必须在 [AppDelegate applicationDidBecomeActive:]
里面遍历使用 取消->恢复
才能成功数据库
朋友说你写一个下载第三方吧, 如今的下载器没几个好用的. 当时我还不觉得然, 说是 GitHub 上那么多轮子, 不缺我这一个, 并且就算写了也不必定比热门的好, 实在不行还有 AFNetworking
当打底的.
我在好久之前我就打算写一个下载器, 想要重点实现单文件多线程分片下载, 当时数据流下载已经写完了, 数据拼接也基本完成了, 准备支持后台下载才发现, NSURLSessionDataTask
不支持后台下载!!! 好吧, Apple🐂🍺🤪
我也看了我朋友用的 XXDownload, 虽然 star 少了点, 但这个恰好符合需求. 虽然在实现中大范围使用下划线变量, 并且还在单例上使用代理, 感受一口老血卡在喉咙里, 但至少改改仍是能用的, 毕竟这种第三方也就是提供个框架而已.
而在 GitHub 上, 已经有一堆项目中止维护了, 还在更新的, 由于任务持久化使用了数据库, 引用了其余第三方, 可能致使库冲突, 而那些还在持续维护的纯净版又没法适应一些需求场景.
其中 HWIFileDownload 就属于一直在更新, 也很纯净的第三方, 通常项目使用足以胜任. 但在某些特殊需求上就有点相形见绌了, 好比支持时效性下载连接, 持久化任务列表, 文件校验, 对恢复数据深度处理等.
固然, 这都不是重点, 重点是后台下载场景太稀少了, 本身随手写一个均可以勉强用, 还要什么第三方, 这种吃力不讨好, 还基本没有 Star 的操做我是不会作的.缓存
既然都写出来了, 那就必须尽可能完美, 除了修复/规避 iOS 的 BUG, 固然还须要支持一些特别的需求.
先列一下 FKDownloader 的总体结构:bash
主类网络
FKDownloadManagersession
FKConfigure多线程
FKTask
辅类
其余
FKDownloader 不依赖其余任何第三方, 保持纯净性, 其中的方法大部分都偏向于对外简单, 对内复杂, 并且尽可能避免高耦合.
必须 iOS 8 以上, 使用 ARC. 支持 CocoaPods
和 Carthage
安装. 若有其余需求, 可直接将 FKDownloader
文件夹直接放入项目中.
自加载
使用 +[NSObject load]
加载单例, 没必要再显式调用来建立单例. 所以能够提早监听 AppDelegate
通知, 修复进度 BUG 将能够自处理, 没必要显示调用.
重启 App 时恢复下载中任务进度
也就是开始一个后台下载任务, 彻底退出 App 后再次运行 App, 须要从新拿到下载任务的进度与状态, 以达到 UI 上显示任务还在运行中的效果.
实现这个功能的第三方我只见到一两个, 这其中的重点是 -[NSURLSession getTasksWithCompletionHandler:]
这个系统方法, 它能够将带有 identifier
的 NSURLSession
中全部的后台任务获取到.
支持时效性 URL
获取到 FKTask 后, 可直接经过 -[FKTask resumeFilePath]
获取 ResumeData 保存路径, 以后用 +[FKResumeHelper updateResumeData:url:]
拿到更新后的 ResumeData, 再保存后便可.
也能够直接使用 -[FKTask updateURL:]
直接更新, 但对进行中的任务无效, 且必须已存在恢复数据.
FKDownloader 只使用 URL 的 scheme://host/path
建立标识符, 因此参数能够随意修改, 若是是使用请求头完成过时操做的, 可以使用自定义请求头.
根据网络状态执行特定操做
检测当前网络状态, 若是没有网络则暂停进行中任务, 取消等待中任务.
当恢复网络时, 就会将由于无网络而中断的任务继续下载.
使用 NSCoding
持久化下载任务, 不依赖数据库
直接保存任务信息, 包括 URL, 任务状态, 保存文件名, 校验信息, 自定义请求头, 文件总大小, 已接收字节数等信息, 保证重启 App 后 UI 信息和退出 App 前保持一致.
代价就是不能高度自定义要保存的数据, 但 FKTask
向外暴露的属性彻底知足外接式数据处理需求, 也可使用项目中已存在的数据库进行自定义管理.
预见性处理状态/进度
设置代理时会将当前全部协议方法触发一遍, 保证 UI 获取的信息为最新.
任务状态/进度的监听
能够自由使用 Block/Delegate/Notification 获取, 最大化覆盖应用场景.
自定义任务附加信息
目前支持保存文件名, 文件校验值, 自定义请求头.
支持 URL 中参数可变
FKTask
只使用 scheme://host/path
建立标识符, parameters
信息将直接忽略, 以识别时效性 URL 下载任务.
精细任务状态
无/预处理/等待/进行中/完成/取消/暂停/恢复/校验/错误, 基本上都有 will
和 did
双重级别.
文件校验
支持 MD5, SHA1, SHA256, SHA512, 但校验特大文件时, CPU占用过大, 因此默认配置为关闭验证.
兼容 Swift 支持在 Swift 项目中进行使用.
// 添加任务, 但不执行, 适合批量添加任务的场景
[[FKDownloadManager manager] add:@“URL”];
// 添加任务, 并附加额外信息, 目前支持 URL, 自定义保存文件名, 校验值, 校验类型, 自定义请求头
[[FKDownloadManager manager] addInfo:@{FKTaskInfoURL: url,
FKTaskInfoFileName: @"xCode7",
FKTaskInfoVerificationType: @(VerifyTypeMD5),
FKTaskInfoVerification: @"5f75fe52c15566a12b012db21808ad8c",
FKTaskInfoRequestHeader: @{} }];
// 开始执行任务
[[FKDownloadManager manager] start:@“URL”];
// 根据 URL 获取任务
[[FKDownloadManager manager] acquire:@“URL”];
// 暂停任务
[[FKDownloadManager manager] suspend:@“URL”];
// 恢复任务
[[FKDownloadManager manager] resume:@“URL”];
// 取消任务
[[FKDownloadManager manager] cancel:@“URL”];
// 移除任务
[[FKDownloadManager manager] remove:@“URL”];
// 设置任务代理
[[FKDownloadManager manager] acquire:@“URL”].delegate = self;
// 设置任务 Block
[[FKDownloadManager manager] acquire:@“URL”].statusBlock = ^(FKTask *task) {
// 状态改变时被调用
};
[[FKDownloadManager manager] acquire:@“URL”].speedBlock = ^(FKTask *task) {
// 下载速度, 默认 1s 调用一次
};
[[FKDownloadManager manager] acquire:@“URL”].progressBlock = ^(FKTask *task) {
// 进度改变时被调用
};
复制代码
// 与代理同价, 可按照代理的使用方式使用通知.
extern FKNotificationName const FKTaskPrepareNotification;
extern FKNotificationName const FKTaskDidIdleNotification;
extern FKNotificationName const FKTaskWillExecuteNotification;
extern FKNotificationName const FKTaskDidExecuteNotication;
extern FKNotificationName const FKTaskProgressNotication;
extern FKNotificationName const FKTaskDidResumingNotification;
extern FKNotificationName const FKTaskWillChecksumNotification;
extern FKNotificationName const FKTaskDidChecksumNotification;
extern FKNotificationName const FKTaskDidFinishNotication;
extern FKNotificationName const FKTaskErrorNotication;
extern FKNotificationName const FKTaskWillSuspendNotication;
extern FKNotificationName const FKTaskDidSuspendNotication;
extern FKNotificationName const FKTaskWillCancelldNotication;
extern FKNotificationName const FKTaskDidCancelldNotication;
extern FKNotificationName const FKTaskSpeedInfoNotication;
复制代码
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
// 保存后台下载所需的系统 Block, 区别 identifier 以防止与其余第三方冲突
if ([identifier isEqualToString:[FKDownloadManager manager].configure.sessionIdentifier]) {
[FKDownloadManager manager].configure.backgroundHandler = completionHandler;
}
}
复制代码
NSURLSessionResumeByteRange
字段致使一些奇怪的问题, 可使用 FKResumeHelper
先读取, 在删除字段, 而后封包, 也可本身进行删除, 目前 FKDownloader 已自行处理.+[NSKeyedUnarchiver unarchiveObjectWithData:]
直接进行解包-[NSKeyedUnarchiver decodeTopLevelObjectForKey:error:]
方法, key
为 NSKeyedArchiveRootObjectKey
来进行解包(而系统默认的 key
是 root
, Apple 我不是很懂你啊😂), 但以前版本须要使用 +[NSPropertyListSerialization propertyListWithData:roptions:format:error:]
进行解包, 封包时也要注意区分.NSURLSessionResumeInfoVersion
版本过旧, 新版本的 NSURLSessionResumeInfoTempFileName
会被 NSURLSessionResumeInfoLocalPath
代替, 缓存文件路径将再也不只是文件名, 而是文件路径, 须要注意, 但影响不大, 运行并没有问题.
文件校验
在下载一些大文件时, 为了保证文件完整性而须要进行文件校验, FKDownloader
可配置是否开启文件校验.
其中, 使用 NSDataReadingMappedIfSafe
选项进行初始化 NSData
, 以防止超大文件致使内存溢出.
通过测试, 6G 大小的文件算出 MD5 须要 4~5秒, 内存占用 < 1M, 但由于 Hash 操做为计算密集型, 致使 CPU 占用 > 90%, 因此通常状况下, 下载小型文件时可开启文件校验, 但超大文件请酌情处理.
NSURLSessionDownloadTask
在调用 -[NSURLSessionDownloadTask cancelByProducingResumeData:]
后, 虽然任务状态改变为 NSURLSessionTaskStateCanceling
, 但在以后代理 -[URLSession URLSession:task:didCompleteWithError:]
中获取, 状态为 NSURLSessionTaskStateCompleted
, 差点被坑的不轻, 因此目前状态管理彻底由 FKTask
的 status
属性代劳.
网络可达性 Network Reachability
使用 官方文件 处理网络状态的检测与监听, 但官方的方式只适合真机运行, 在虚拟机中只可监听到失去网络的状态, 而再次链接网络的状态没法获取, 但在真机中全部状态均可监听, 因此测试网络状态时请使用真机测试.
请查看运行 Demo