时间:2021-08-14 08:32:57 | 栏目:iOS代码 | 点击:次
项目开发中,有时候我们需要将本地的文件上传到服务器,简单的几张图片还好,但是针对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
#import <Foundation/Foundation.h> @interface YJTDocUploadModel : NSObject // 方便操作(暂停取消)正在上传的文件 @property (nonatomic, strong) NSURLSessionDataTask *dataTask; // 总大小 @property (nonatomic, assign) int64_t totalSize; // 总片数 @property (nonatomic, assign) NSInteger totalCount; // 已上传片数 @property (nonatomic, assign) NSInteger uploadedCount; // 上传所需参数 @property (nonatomic, copy) NSString *upToken; // 上传状态标识, 记录是上传中还是暂停 @property (nonatomic, assign) BOOL isRunning; // 缓存文件路径 @property (nonatomic, copy) NSString *filePath; // 用来保存文件名使用 @property (nonatomic, copy) NSString *lastPathComponent; // 以下属性用于给上传列表界面赋值 @property (nonatomic, assign) NSInteger docType; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *progressLableText; @property (nonatomic, assign) CGFloat uploadPercent; @property (nonatomic, copy) void(^progressBlock)(CGFloat uploadPersent,NSString *progressLableText); // 保存上传成功后调用保存接口的参数 @property (nonatomic, strong) NSMutableDictionary *parameters; @end YJTUploadManager.m #import "YJTDocUploadModel.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<AFMultipartFormData> _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<AFMultipartFormData> _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; }
总结