在项目开发过程中,有时我们需要将本地文件上传到服务器。几张简单的图片就可以了,但是在iPhone中上传视频文件是为了用户体验。我们有必要实现断点上传。其实并不是真正的断点,这里我们只是模仿断点机制。需求由于需要上传文件,所以最好有上传列表界面,方便用户实时管理上传的文件。这里我简单的搭建了一个上传列表界面,如下图所示:该界面实现的功能:左滑删除,点击暂停,取消,清空列表。退出此界面后台上传,暂停重启或应用被杀,仍支持续传。上传完成后,删除正在上传的文件,清空上传列表,会删除本地缓存的文件。实现方法客户端对一个大文件进行切片,服务端收到所有切片后拼接成一个完整的文件。1.录制视频或在系统相册中选择视频后,需要将缓存文件写入沙箱。因为如果不缓存,只是通过路径获取视频,手机中的视频可能会被删除。如果选择系统自带压缩,文件只存放在系统的某个缓存文件夹中,系统可能会清理该文件,下次再根据路径获取视频时,不会被发现。我不会详细介绍缓存文件。在/Library/Caches目录下新建文件夹Video,用于缓存视频文件。我看到用过的文章都保存在Documents文件夹中。我不推荐它。之所以在这个目录下是因为系统不会清理这个文件夹,在进行iCloud备份的时候也不会备份这个文件夹。内容。如果在Documents文件夹下放一个较大的视频文件,难免会给用户带来不便。另外需要注意的是,如上所述,上传完成后一定要删除本地缓存的文件,删除正在上传的文件,清空上传列表。否则应用会占用太多系统空间,用户看到后会直接卸载你的应用。为了防止重复,我在文件名中加上了时间戳。#pragmamark-writecachefile-(NSString*)writeToCacheVideo:(NSData*)dataappendNameString:(NSString*)name{NSString*cachesDirectory=NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES).firstObject;NSString*createPath=[cachesDirectorystringByAppendingPathComponent]:@"vid;NSFileManager*fileManager=[[NSFileManageralloc]init];[fileManagercreateDirectoryAtPath:createPathwithIntermediateDirectories:YESattributes:nilerror:nil];NSString*path=[cachesDirectorystringByAppendingPathComponent:[NSStringstringWithFormat:@"/video/%.0f%@".[NSDatedate]timeIntervalSince1970,name]];[datawriteToFile:pathatomically:NO];returnpath;}这里随便说说sandbox目录下的几个文件夹的作用。文档:只有用户生成的文档和其他数据,或者您的应用程序无法以其他方式重新创建的文档和其他数据,才应存储在/Documents目录中,并由iCloud自动备份。//用户生成的数据,程序无法重新创建生成的数据会通过iCloud备份到Library:可以再次下载或重新生成的数据应该存放在/Library/Caches目录下。应该放在Caches目录中的文件示例包括数据库缓存文件和可下载内容,例如杂志、报纸和地图应用程序使用的内容。//可以下载的数据tmp:暂时使用的数据应该存放在/tmp目录下。虽然这些文件没有备份到iCloud,但请记得在用完这些文件后将其删除,以免它们继续占用用户设备上的空间。//临时数据2.SlicingSlicing主要使用NSFileHandle类,它实际上是通过移动文件指针来读取某段内容。//model.filePath文件路径NSFileHandle*handle=[NSFileHandlefileHandleForReadingAtPath:model.filePath];//移动文件指针//kSuperUploadBlockSize这里的上传切片大小为1M,i指的是上传切片的个数(i=model.uploadedCount)[handleseekToFileOffset:kSuperUploadBlockSize*i];//读取数据NSData*blockData=[handlereadDataOfLength:kSuperUploadBlockSize];这里我把大文件切割成最小1M大小的小文件上传。这里使用了一个Model,主要存放上传列表需要的一些基础数据。因为每次上传一个作品,我们都需要更新UI。由于这里需要支持断点续传,所以需要记录文件的进度值,我们需要保存上传的片数。您可以使用数据库或plist文件来保存上传的文件路径和文件进度。这里要保存的数据不多,所以我直接保存在偏好设置中。每份文件上传成功,设置模型上传份数,更新本地文件进度值。我们可以大致看下ModelYJTUploadManager.h@implementationYJTDocUploadModel//上传后更新模型相关数据-(void)setUploadedCount:(NSInteger)uploadedCount{_uploadedCount=uploadedCount;self.uploadPercent=(CGFloat)uploadedCount/self.totalCount;self.progressLableText=[NSStringstringWithFormat:@"%.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);}//刷新本地缓存[[YJTUploadManagershareUploadManager]refreshCaches];}@end3.上传和上传可以同步和异步进行。不建议用于遍历开太多线程上传。打开线程会消耗内存。在这里,我正在同步进行。也就是说,使用了递归。上传一个文件后,再上传下一个文件。如果失败,请重新上传。需要强调一点,最后一个切片的大小一般小于预设的最小分割值。另外,如果分片的大小大于文件的总大小,可能会出现问题,客户端和服务端可以通过规则进行通信和处理。关于上传进度,可以粗略计算一下。也可以使用NSURLSessionDataTask的countOfBytesSent进行实时监控。其实NSURLSessionTask在iOS11之后也提供了progress属性。附上核心代码供参考。***调用上传接口#pragmamark-firstuploadbreakpoint//上传初始化-(void)uploadData:(NSData*)datawithModel:(YJTDocUploadModel*)model{//计算分片个数NSIntegercount=data.length/(kSuperUploadBlockSize);NSIntegerblockCount=data.length%(kSuperUploadBlockSize)==0?count:count+1;//给模型赋值model.filePath=[selfwriteToCacheVideo:dataappendNameString:model.lastPathComponent];model.totalCount=blockCount;model.totalSize=data.length;model.uploadedCount=0;model.isRunning=YES;//上传所需参数NSMutableDictionary*parameters=[NSMutableDictionarydictionary];parameters[@"sequenceNo"]=@0;parameters[@"blockSize"]=@(kSuperUploadBlockSize);parameters[@"totFileSize"]=@(data.length);parameters[@"suffix"]=model.filePath.pathExtension;parameters[@"token"]=@"";NSString*requestUrl=@"上传接口";AFHTTPSessionManager*manager=[AFHTTPSessionManagermanager];NSURLSessionDataTask*dataTask=[managerPOST:requestUrlparameters:parametersconstructingBodyWithBlock:^(id_NonnullformData){[formDataappendPartWithFileData:[NSDatadata]name:@"block"fileName:model.filePath.lastPathComponentmimeType:@"application/octet-stream"];}成功:^(NSURLSessionDataTask*_Nonnulltask,id_NonnullresponseObject){NSDictionary*dataDict=responseObject[kRet_success_data_key];model.upToken=dataDict[@"upToken"];NSFileHandle*handle=[NSFileHandlefileHandleForReadingAtPath:model.filePath];if(handle==nil){return;}[selfcontinueUploadWithModel:model];[selfaddUploadModel:model];[[VMProgressHUDsharedInstance]showTipTextOnly:@"正在后台上传"dealy:2];}failure:^(NSURLSessionDataTask*_Nonnulltask,NSError*_Nonnullerror){[[VMProgressHUDsharedInstance]showTipTextOnly:error.localizedDescriptiondealy:1];}];模型。dataTask=dataTask;}核心代码#pragmamark-continueupload-(void)continueUploadWithModel:(YJTDocUploadModel*)model{if(!model.isRunning){return;}__blockNSIntegeri=model.uploadedCount;NSMutableDictionary*parameters=[NSMutableDictionarydictionary];参数[@"blockSize"]=@(kSuperUploadBlockSize);参数[@"totFileSize"]=@(model.totalSize);参数[@"suffix"]=model.filePath.pathExtension;参数[@"token"]=@"";参数[@"upToken"]=model.upToken;参数[@"crc"]=@"";参数[@"sequenceNo"]=@(i+1);NSString*requestUrl=[[ApigetRootUrl]stringByAppendingString:@"上传接口"];AFHTTPSessionManager*manager=[AFHTTPSessionManagermanager];NSURLSessionDataTask*dataTask=[managerPOST:requestUrlparameters:parametersconstructingBodyWithBlock:^(id_NonnullformData){NSFileHandle*handlePath[HandleReadingfile].handleseekToFileOffset:kSuperUploadBlockSize*i];NSData*blockData=[handlereadDataOfLength:kSuperUploadBlockSize];[formDataappendPartWithFileData:blockDataname:@"block"文件名:model.filePath.lastPathComponentmimeType:@"application/octet-stream"];}成功:^(NSURLSessionDataTask*_Nonnulltask,id_NonnullresponseObject){i++;model.uploadedCount=i;NSDictionary*dataDict=responseObject[kRet_success_data_key];NSString*fileUrl=dataDict[@"fileUrl"];if([fileUrlisKindOfClass:[NSStringclass]]){[model.parameterssetValue:fileUrlforKey:@"url"];//***所有分片上传完毕,服务器返回文件url,进行后续操作[selfsaveRequest:model];}else{if(i
