内容简介:前几天突然收到一朋友发来的消息, 说是在 iOS 12 上遇到了一个新的 BUG, 问我怎么看? 我说新系统遇到 BUG 不是很正常吗? 大概是个什么情况?经过朋友说明, 大概是这么个现象: 他用了一个第三方下载管理器进行视频下载, 明明是设置了后台下载的, 但 App 一推到后台再回到前台, 下载进度就不动了, 但任务依然还在继续下载. 系统是 iOS 12, 手机是 iPhone 7.刚一开始还以为第三方在进度处理方面写的有问题, 但我把这个第三方的
前几天突然收到一朋友发来的消息, 说是在 iOS 12 上遇到了一个新的 BUG, 问我怎么看? 我说新系统遇到 BUG 不是很正常吗? 大概是个什么情况?
经过朋友说明, 大概是这么个现象: 他用了一个第三方下载管理器进行视频下载, 明明是设置了后台下载的, 但 App 一推到后台再回到前台, 下载进度就不动了, 但任务依然还在继续下载. 系统是 iOS 12, 手机是 iPhone 7.
BUG 详情
刚一开始还以为第三方在进度处理方面写的有问题, 但我把这个第三方的 Demo 下载运行后, 发现这根本不是第三方问题, 而是系统问题, 系统代理 -[NSURLSessionDownloadTask URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:] 根本没有被调用, 所以下载进度根本就无法继续计算.
然后我改为使用 KVO 监听 NSURLSessionDownloadTask 的 countOfBytesReceived 和 countOfBytesExpectedToReceive 属性来计算当前下载进度, 但很遗憾, 这两个值在重回前台后就没在继续变化, 初步认定是系统在处理数据接收时出现了异常, 导致省略了值的改变, 还有顺便躺枪的进度代理.
上一次遇到这种系统犯法失效的 BUG 还是在 iOS 11.1/11.2 上, 当时开发录屏直播, 系统方法 -[RPBroadcastSampleHandler processSampleBuffer:withType:] 没有被调用, 直接坑掉了一个大功能模块, 但幸好, 这一回遇到的 BUG 不算严重, 解决方法还是有的.
开始测试
这回的进度 BUG 在虚拟机上是不会出现的, 必须真机, 而且经过测试, 发现只在 iOS 12/12.1, iPhone 8 以下才会出现.
在测试时还发现 App 完全退出后, 后台下载任务会直接取消, 但是带有恢复数据.
进入前台后, 手动进行 暂停->继续 操作后, 代理/KVO 就会继续工作.
尝试修复 BUG
既然手动 暂停->继续 可以修复 BUG, 那只要用代码重现一遍就可以了吧? 别急, 事情没有那么简单.
直接在 -[AppDelegate applicationWillEnterForeground:] 开始遍历所有下载任务, 都执行一遍 暂停->继续 操作, 这个方法很简单, 很粗暴, 但, 这不管用!
那么使用 -[NSURLSessionDownloadTask cancelByProducingResumeData:] -> -[NSURLSession downloadTaskWithResumeData:] 代替 暂停->继续 呢? 不错, 意识到当前的 NSURLSessionDownloadTask 可能存在脏数据是个进步, 但, 这依然不管用!
最后的最后, 还是测试出来了, 必须在 [AppDelegate applicationDidBecomeActive:] 里面遍历使用 取消->恢复 才能成功
关于下载器的轮子
朋友说你写一个下载第三方吧, 现在的下载器没几个好用的. 当时我还不以为然, 说是 GitHub 上那么多轮子, 不缺我这一个, 而且就算写了也不一定比热门的好, 实在不行还有 AFNetworking 当打底的.
我在很久以前我就打算写一个下载器, 想要重点实现单文件多线程分片下载, 当时数据流下载已经写完了, 数据拼接也基本完成了, 准备支持后台下载才发现, NSURLSessionDataTask 不支持后台下载!!! 好吧, Apple:ox::beer:
我也看了我朋友用的 XXDownload , 虽然 star 少了点, 但这个刚好符合需求. 虽然在实现中大范围使用下划线变量, 而且还在单例上使用代理, 感觉一口老血卡在喉咙里, 但至少改改还是能用的, 毕竟这种第三方也就是提供个框架而已.
而在 GitHub 上, 已经有一堆项目停止维护了, 还在更新的, 因为任务持久化使用了数据库, 引用了其他第三方, 可能导致库冲突, 而那些还在持续维护的纯净版又无法适应一些需求场景.
当然, 这都不是重点, 重点是后台下载场景太稀少了, 自己随手写一个都可以勉强用, 还要什么第三方, 这种吃力不讨好, 还基本没有 Star 的操作我是不会做的.
FKDownloader -- 最终还是写了
既然都写出来了, 那就必须尽量完美, 除了修复/规避 iOS 的 BUG, 当然还需要支持一些特别的需求.
先列一下 FKDownloader 的整体结构:
-
主类
-
FKDownloadManager
- 不可继承, 唯一存在
- 管理 Task, 进行增删查操作
- 开始/暂停/恢复/取消 Task, 但实现与状态过滤全权由 Task 实现
- 所有任务下载进度
- 在 AppDelegate 处理部分功能, 如后台下载, 加载任务归档, 解决 iOS BUG 等
-
FKConfigure
- 统一管理特殊配置
- 设置 Session Identifier
- 设置是否为后台下载
- 设置是否自动清理已完成/失败任务
- 设置是否自动开始任务, 针对载入本地归档任务时
- 自定义请求超时时间
-
FKTask
- 开始/暂停/恢复/取消的具体实现
- Block/Delegate/Notification 的发起者
- 校验文件
- 下载速度/预计剩余时间
- 可添加附带信息, 包括保存文件名, 校验信息, 自定义请求头等信息
-
-
辅类
- FKResumeHelper
- 解包/封包恢复数据
- 修复 iOS 特定版本中错误的恢复数据
- 更新恢复数据的 URL
- FKResumeHelper
-
其他
- FKDefine: 声明枚举, C 方法, 字符串常量
- FKDownloadExecutor: 统一处理系统代理
- FKTaskStorage: 管理任务的归解档
- FKHashHelper: 计算 Hash
- FKSystemHelper: 获取设备版本, 系统版本
FKDownloader 不依赖其他任何第三方, 保持纯净性, 其中的方法大部分都偏向于对外简单, 对内复杂, 而且尽量避免高耦合.
FKDownloader 支持与安装
必须 iOS 8 以上, 使用 ARC. 支持 CocoaPods 和 Carthage 安装. 如有其他需求, 可直接将 FKDownloader 文件夹直接放入项目中.
FKDownloader 特点
-
重启 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 中参数可变, 只使用
scheme://host/path创建标识符,parameters信息将直接忽略, 以识别时效性 URL 下载任务. -
精细任务状态, 无/预处理/等待/进行中/完成/取消/暂停/恢复/校验/错误, 基本上都有
will和did双重级别. -
文件校验支持 MD5, SHA1, SHA256, SHA512, 但校验特大文件时, CPU占用过大, 所以默认配置为关闭验证.
FKDownloader 简单使用
- 任务管理
// 添加任务, 但不执行, 适合批量添加任务的场景
[[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; 复制代码
- 需要在 AppDelegate 中调用的
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 初始化统一配置, 最好在 App 最开始配置好, 如果不进行设置将直接使用默认配置
FKConfigure *config = [FKConfigure defaultConfigure];
config.isBackgroudExecute = YES;
config.isAutoClearTask = NO;
config.isAutoStart = NO;
config.isFileChecksum = YES;
config.speedRefreshInterval = 1;
[FKDownloadManager manager].configure = config;
// 恢复持久化的任务与状态, 并获取正在进行的后台任务的进度
[[FKDownloadManager manager] restory];
return YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// 修复特定设备与版本出现的进度无法改变的 BUG
[[FKDownloadManager manager] fixProgressNotChanage];
}
- (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;
}
}
复制代码
FKDownloader 处理的一些细节
-
ResumeData
恢复数据在 iOS 10.0/10.1 中出现了格式错误, 官方在 iOS 10.2 中修复成功, 但为了兼容, 还是需要修复一番的, 具体解决方案在这里.
而在 iOS 11 中, 因为多出了
NSURLSessionResumeByteRange字段导致一些奇怪的问题, 可以使用FKResumeHelper先读取, 在删除字段, 然后封包, 也可自己进行删除, 目前 FKDownloader 已自行处理.虽然没有出错, 但在 iOS 12 中, ResumeData 的封包格式发生了改变, 现在可使用
+[NSKeyedUnarchiver unarchiveObjectWithData:]直接进行解包, 但之前版本需要使用+[NSPropertyListSerialization propertyListWithData:roptions:format:error:]进行解包, 封包时也要注意区分.在 iOS 8 中, 因为
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属性代劳.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。