内容简介:前几天突然收到一朋友发来的消息, 说是在 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
属性代劳.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。