项目开发中,有时候我们需要将本地的文件上传到服务器,简单的几张图片还好,但是针对iPhone里面的视频文件进行上传,为了用户体验,我们有必要实现断点上传。其实也不是真的断点,这里我们只是模仿断点机制。
需求
既然需要上传文件,那***要有一个上传列表界面,方面用户对上传中的文件进行实时管理。这里我简单搭建了一个上传列表界面,如下图:
该界面实现的功能:左滑删除,单击暂停、取消,清空列表。退出该界面可后台上传,暂停再次开始或则app被kill掉依旧支持续传。上传完成、删除正在上传文件、清空上传列表都会将本地缓存的文件删除。
实现方法
客户端把大文件切片,服务器接收完所有片后拼接成一个完整文件。
1.缓存文件
录制视频或者选择系统相册中的视频后需要写入文件到沙盒。因为如果不缓存,只是通过路径来获取视频,手机中的视频可能被删除。如果是选择系统自带压缩的话,文件只是存在了系统的某个cache文件夹下,系统可能会清理该文件件,那么下次再次根据路径获取视频的时候,就找不到了。
缓存文件就不再细说,在/Library/Caches 目录下面新建一个文件夹Video用来缓存视频文件。之前看到用的文章存到了Documents文件夹下,我是不建议的,之所以在这个目录下面,是因为系统不会清理这个文件夹,而且在进行iCloud备份时也不会备份该文件夹下的内容。如果把一个很大的视频文件放到Documents文件夹下,必然给用户带来不便。还有一点需要注意,正如上面所描述,上传完成、删除正在上传文件、清空上传列表都必须将本地缓存的文件删除。不然会导致app占用系统太多的空间,用户看到后直接把你的app卸载了。
为了防止重名,我在文件名中拼上了时间戳。
- #pragma mark- write cache file
- - (NSString *)writeToCacheVideo:(NSData *)data appendNameString:(NSString *)name {
- NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
- NSString *createPath = [cachesDirectory stringByAppendingPathComponent:@"video"];
- NSFileManager *fileManager = [[NSFileManager alloc] init];
- [fileManager createDirectoryAtPath:createPath withIntermediateDirectories:YES attributes:nil error:nil];
- NSString *path = [cachesDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"/video/%.0f%@",[NSDate date].timeIntervalSince1970,name]];
- [data writeToFile:path atomically:NO];
- return path;
- }
这里随便说下沙盒目录下几个文件夹的作用。
2.切片
切片主要用到NSFileHandle这个类,其实就是通过移动文件指针来读取某段内容。
- // model.filePath 文件路径
- NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:model.filePath];
- // 移动文件指针
- // kSuperUploadBlockSize 上传切片大小 这里是1M, i指已上传片数(i = model.uploadedCount)
- [handle seekToFileOffset:kSuperUploadBlockSize * i];
- //读取数据
- NSData *blockData = [handle readDataOfLength:kSuperUploadBlockSize];
这里我将大文件切成最小1M的小文件来上传。这边使用到一个Model,该数据模型主要存放上传列表中所需要的一些基本数据。因为我们每次上传完一片,需要更新UI。由于这边需要支持断点续传,因此需要记录文件的进度值,已上传的片数我们需要保存下来。保存上传文件路径和文件进度可以使用数据库或则plist文件等方式,这边需要保存的数据不是很多,所以我直接保存在了偏好设置中。每片文件上传成功,设置该模型已上传片数,并且更新本地文件进度值。
我们可以大致看下所用到的Model
YJTUploadManager.h
- @implementation YJTDocUploadModel
- // 上传完毕后更新模型相关数据
- - (void)setUploadedCount:(NSInteger)uploadedCount {
- _uploadedCount = uploadedCount;
- self.uploadPercent = (CGFloat)uploadedCount / self.totalCount;
- self.progressLableText = [NSString stringWithFormat:@"%.2fMB/%.2fMB",self.totalSize * self.uploadPercent /1024.0/1024.0,self.totalSize/1024.0/1024.0];
- if (self.progressBlock) {
- self.progressBlock(self.uploadPercent,self.progressLableText);
- }
- // 刷新本地缓存
- [[YJTUploadManager shareUploadManager] refreshCaches];
- }
- @end
3.上传
上传可以采用同步和异步执行。这里不太建议通过for遍历来开太多的线程上传,开线程是耗内存的。这边我是通过同步的方式。也就是采用递归,一片文件上传完毕后再上传下一片文件,如果失败,再次上传。有一点需要强调,***一片的大小一般都比会小于预设的最小分割值。另外,如果分的片段大小大于文件的总大小也可能会出问题,客户端和服务器沟通好规则处理即可。
关于上传进度,可以粗略计算。也可使用NSURLSessionDataTask的countOfBytesSent实时监控。其实NSURLSessionTask在iOS11以后还提供了progress属性。附上核心代码提供参考。
***调用上传接口
- #pragma mark- first upload 断点
- // 上传初始化
- - (void)uploadData:(NSData *)data withModel:(YJTDocUploadModel *)model {
- // 计算片数
- NSInteger count = data.length / (kSuperUploadBlockSize);
- NSInteger blockCount = data.length % (kSuperUploadBlockSize) == 0 ? count : count + 1;
- // 给model赋值
- model.filePath = [self writeToCacheVideo:data appendNameString:model.lastPathComponent];
- model.totalCount = blockCount;
- model.totalSize = data.length;
- model.uploadedCount = 0;
- model.isRunning = YES;
- // 上传所需参数
- NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
- parameters[@"sequenceNo"] = @0;
- parameters[@"blockSize"] = @(kSuperUploadBlockSize);
- parameters[@"totFileSize"] = @(data.length);
- parameters[@"suffix"] = model.filePath.pathExtension;
- parameters[@"token"] = @"";
- NSString *requestUrl = @"上传接口";
- AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- NSURLSessionDataTask *dataTask = [manager POST:requestUrl parameters:parameters constructingBodyWithBlock:^(id _Nonnull formData) {
- [formData appendPartWithFileData:[NSData data] name:@"block" fileName:model.filePath.lastPathComponent mimeType:@"application/octet-stream"];
- } success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) {
- NSDictionary *dataDict = responseObject[kRet_success_data_key];
- model.upToken = dataDict[@"upToken"];
- NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:model.filePath];
- if (handle == nil) { return; }
- [self continueUploadWithModel:model];
- [self addUploadModel:model];
- [[VMProgressHUD sharedInstance] showTipTextOnly:@"正在后台上传" dealy:2];
- } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
- [[VMProgressHUD sharedInstance] showTipTextOnly:error.localizedDescription dealy:1];
- }];
- model.dataTask = dataTask;
- }
核心代码
- #pragma mark- continue upload
- - (void)continueUploadWithModel:(YJTDocUploadModel *)model {
- if (!model.isRunning) {
- return;
- }
- __block NSInteger i = model.uploadedCount;
- NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
- parameters[@"blockSize"] = @(kSuperUploadBlockSize);
- parameters[@"totFileSize"] = @(model.totalSize);
- parameters[@"suffix"] = model.filePath.pathExtension;
- parameters[@"token"] = @"";
- parameters[@"upToken"] = model.upToken;
- parameters[@"crc"] = @"";
- parameters[@"sequenceNo"] = @(i + 1);
- NSString *requestUrl = [[Api getRootUrl] stringByAppendingString:@"上传接口"];
- AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
- NSURLSessionDataTask *dataTask = [manager POST:requestUrl parameters:parameters constructingBodyWithBlock:^(id _Nonnull formData) {
- NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:model.filePath];
- [handle seekToFileOffset:kSuperUploadBlockSize * i];
- NSData *blockData = [handle readDataOfLength:kSuperUploadBlockSize];
- [formData appendPartWithFileData:blockData name:@"block" fileName:model.filePath.lastPathComponent mimeType:@"application/octet-stream"];
- } success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) {
- i ++;
- model.uploadedCount = i;
- NSDictionary *dataDict = responseObject[kRet_success_data_key];
- NSString *fileUrl = dataDict[@"fileUrl"];
- if ([fileUrl isKindOfClass:[NSString class]]) {
- [model.parameters setValue:fileUrl forKey:@"url"];
- // ***所有片段上传完毕,服务器返回文件url,执行后续操作
- [self saveRequest:model];
- }else {
- if (i < model.totalCount) {
- [self continueUploadWithModel:model];
- }
- }
- } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
- // 上传失败重试
- [self continueUploadWithModel:model];
- }];
- model.dataTask = dataTask;
- }
【中国,广州,2020年8月5日】华为F5G时代企业光网旗舰产品发布会在广州圆满召开...
本文转载自微信公众号「UP技术控」,作者conan5566 。转载本文请联系UP技术控公...
【51CTO.com快译】事实表明,通过将机器人流程自动化、人工智能和机器学习等新兴...
时隔仅一周时间,苹果又发布了iOS13.4以及iPadOS 13.4首个开发者测试版(上一次版...
人工智能一直是近年来的热门技术领域,机器学习是人工智能的一个子集,是整个人...
近日,天风国际分析师郭明錤在***研报中表示,苹果2019款iPhone系列产品可能大幅...
人们在日常生活中经常需要识别不同的面孔,其中既包括我们所熟悉的亲属、朋友、...
2020年,新冠疫情在全球肆意蔓延,给世界经济带来了巨大冲击,并倒逼传统企业朝...
苹果最近虽然召开了发布会,但是并没有发布5G新机,使得不少的果粉们比较着急。...
在当今动荡的商业环境中,市场条件日趋复杂,而整理和挖掘数据以推动基于分析的...