一个系统BUG引发的血案 -- FKDownloader

栏目: IOS · 发布时间: 6年前

内容简介:前几天突然收到一朋友发来的消息, 说是在 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 监听 NSURLSessionDownloadTaskcountOfBytesReceivedcountOfBytesExpectedToReceive 属性来计算当前下载进度, 但很遗憾, 这两个值在重回前台后就没在继续变化, 初步认定是系统在处理数据接收时出现了异常, 导致省略了值的改变, 还有顺便躺枪的进度代理.

上一次遇到这种系统犯法失效的 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 可能存在脏数据是个进步, 但, 这依然不管用!

一个系统BUG引发的血案 -- FKDownloader

最后的最后, 还是测试出来了, 必须在 [AppDelegate applicationDidBecomeActive:] 里面遍历使用 取消->恢复 才能成功

关于下载器的轮子

朋友说你写一个下载第三方吧, 现在的下载器没几个好用的. 当时我还不以为然, 说是 GitHub 上那么多轮子, 不缺我这一个, 而且就算写了也不一定比热门的好, 实在不行还有 AFNetworking 当打底的.

我在很久以前我就打算写一个下载器, 想要重点实现单文件多线程分片下载, 当时数据流下载已经写完了, 数据拼接也基本完成了, 准备支持后台下载才发现, NSURLSessionDataTask 不支持后台下载!!! 好吧, Apple:ox::beer:

我也看了我朋友用的 XXDownload , 虽然 star 少了点, 但这个刚好符合需求. 虽然在实现中大范围使用下划线变量, 而且还在单例上使用代理, 感觉一口老血卡在喉咙里, 但至少改改还是能用的, 毕竟这种第三方也就是提供个框架而已.

而在 GitHub 上, 已经有一堆项目停止维护了, 还在更新的, 因为任务持久化使用了数据库, 引用了其他第三方, 可能导致库冲突, 而那些还在持续维护的纯净版又无法适应一些需求场景.

当然, 这都不是重点, 重点是后台下载场景太稀少了, 自己随手写一个都可以勉强用, 还要什么第三方, 这种吃力不讨好, 还基本没有 Star 的操作我是不会做的.

一个系统BUG引发的血案 -- FKDownloader

FKDownloader -- 最终还是写了

既然都写出来了, 那就必须尽量完美, 除了修复/规避 iOS 的 BUG, 当然还需要支持一些特别的需求.

先列一下 FKDownloader 的整体结构:

  • 主类

    • FKDownloadManager

      • 不可继承, 唯一存在
      • 管理 Task, 进行增删查操作
      • 开始/暂停/恢复/取消 Task, 但实现与状态过滤全权由 Task 实现
      • 所有任务下载进度
      • 在 AppDelegate 处理部分功能, 如后台下载, 加载任务归档, 解决 iOS BUG 等
    • FKConfigure

      • 统一管理特殊配置
      • 设置 Session Identifier
      • 设置是否为后台下载
      • 设置是否自动清理已完成/失败任务
      • 设置是否自动开始任务, 针对载入本地归档任务时
      • 自定义请求超时时间
    • FKTask

      • 开始/暂停/恢复/取消的具体实现
      • Block/Delegate/Notification 的发起者
      • 校验文件
      • 下载速度/预计剩余时间
      • 可添加附带信息, 包括保存文件名, 校验信息, 自定义请求头等信息
  • 辅类

    • FKResumeHelper
      • 解包/封包恢复数据
      • 修复 iOS 特定版本中错误的恢复数据
      • 更新恢复数据的 URL
  • 其他

    • FKDefine: 声明枚举, C 方法, 字符串常量
    • FKDownloadExecutor: 统一处理系统代理
    • FKTaskStorage: 管理任务的归解档
    • FKHashHelper: 计算 Hash
    • FKSystemHelper: 获取设备版本, 系统版本

FKDownloader 不依赖其他任何第三方, 保持纯净性, 其中的方法大部分都偏向于对外简单, 对内复杂, 而且尽量避免高耦合.

FKDownloader 支持与安装

必须 iOS 8 以上, 使用 ARC. 支持 CocoaPodsCarthage 安装. 如有其他需求, 可直接将 FKDownloader 文件夹直接放入项目中.

FKDownloader 特点

  • 重启 App 时恢复下载中任务进度

    也就是开始一个后台下载任务, 完全退出 App 后再次运行 App, 需要重新拿到下载任务的进度与状态, 以达到 UI 上显示任务还在运行中的效果.

    实现这个功能的第三方我只见到一两个, 这其中的重点是 -[NSURLSession getTasksWithCompletionHandler:] 这个系统方法, 它可以将带有 identifierNSURLSession 中所有的后台任务获取到.

  • 支持时效性 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 下载任务.

  • 精细任务状态, 无/预处理/等待/进行中/完成/取消/暂停/恢复/校验/错误, 基本上都有 willdid 双重级别.

  • 文件校验支持 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 代替, 缓存文件路径将不再只是文件名, 而是文件路径, 需要注意, 但影响不大, 运行并无问题.

    一个系统BUG引发的血案 -- FKDownloader

  • 文件校验

    在下载一些大文件时, 为了保证文件完整性而需要进行文件校验, FKDownloader 可配置是否开启文件校验.

    其中, 使用 NSDataReadingMappedIfSafe 选项进行初始化 NSData , 以防止超大文件导致内存溢出.

    经过测试, 6G 大小的文件算出 MD5 需要 4~5秒, 内存占用 < 1M, 但因为 Hash 操作为计算密集型, 导致 CPU 占用 > 90%, 所以一般情况下, 下载小型文件时可开启文件校验, 但超大文件请酌情处理.

  • NSURLSessionDownloadTask

    在调用 -[NSURLSessionDownloadTask cancelByProducingResumeData:] 后, 虽然任务状态改变为 NSURLSessionTaskStateCanceling , 但在之后代理 -[URLSession URLSession:task:didCompleteWithError:] 中获取, 状态为 NSURLSessionTaskStateCompleted , 差点被坑的不轻, 所以目前状态管理完全由 FKTaskstatus 属性代劳.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

一胜九败

一胜九败

柳井正 / 徐静波 / 中信出版社 / 2011-1-19 / 28.00元

优衣库成长的过程,就是一个历经了无数次失败的过程。他们经历过无法从银行融资的焦灼,经历过“衣服因低价热销,但人们买回去之后立即把商标剪掉”的难堪,经历过为上市冲刺而拼命扩张店铺的疯狂,也经历过被消费者冷落、疏离的苦痛……但正是从这些失败中学到的经验与教训,让柳井正走向了成功。 《一胜九败:优衣库风靡全球的秘密》就像是柳井正的错误集,在这里,他毫不隐晦地将公司业绩低迷的原因、进军海外失败的因素......一起来看看 《一胜九败》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具