項(xiàng)目開發(fā)中,有時(shí)候我們需要將本地的文件上傳到服務(wù)器,簡單的幾張圖片還好,但是針對iPhone里面的視頻文件進(jìn)行上傳,為了用戶體驗(yàn),我們有必要實(shí)現(xiàn)斷點(diǎn)上傳。其實(shí)也不是真的斷點(diǎn),這里我們只是模仿斷點(diǎn)機(jī)制。
需求
既然需要上傳文件,那最好要有一個上傳列表界面,方面用戶對上傳中的文件進(jìn)行實(shí)時(shí)管理。這里我簡單搭建了一個上傳列表界面,如下圖:
該界面實(shí)現(xiàn)的功能:左滑刪除,單擊暫停、取消,清空列表。退出該界面可后臺上傳,暫停再次開始或則app被kill掉依舊支持續(xù)傳。上傳完成、刪除正在上傳文件、清空上傳列表都會將本地緩存的文件刪除。
實(shí)現(xiàn)方法
客戶端把大文件切片,服務(wù)器接收完所有片后拼接成一個完整文件。
1.緩存文件
錄制視頻或者選擇系統(tǒng)相冊中的視頻后需要寫入文件到沙盒。因?yàn)槿绻痪彺妫皇峭ㄟ^路徑來獲取視頻,手機(jī)中的視頻可能被刪除。如果是選擇系統(tǒng)自帶壓縮的話,文件只是存在了系統(tǒng)的某個cache文件夾下,系統(tǒng)可能會清理該文件件,那么下次再次根據(jù)路徑獲取視頻的時(shí)候,就找不到了。
緩存文件就不再細(xì)說,在/Library/Caches 目錄下面新建一個文件夾Video用來緩存視頻文件。之前看到用的文章存到了Documents文件夾下,我是不建議的,之所以在這個目錄下面,是因?yàn)橄到y(tǒng)不會清理這個文件夾,而且在進(jìn)行iCloud備份時(shí)也不會備份該文件夾下的內(nèi)容。如果把一個很大的視頻文件放到Documents文件夾下,必然給用戶帶來不便。還有一點(diǎn)需要注意,正如上面所描述,上傳完成、刪除正在上傳文件、清空上傳列表都必須將本地緩存的文件刪除。不然會導(dǎo)致app占用系統(tǒng)太多的空間,用戶看到后直接把你的app卸載了。
為了防止重名,我在文件名中拼上了時(shí)間戳。
#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這個類,其實(shí)就是通過移動文件指針來讀取某段內(nèi)容。
// model.filePath 文件路徑NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:model.filePath];// 移動文件指針// kSuperUploadBlockSize 上傳切片大小 這里是1M, i指已上傳片數(shù)(i = model.uploadedCount)[handle seekToFileOffset:kSuperUploadBlockSize * i];//讀取數(shù)據(jù)NSData *blockData = [handle readDataOfLength:kSuperUploadBlockSize];
這里我將大文件切成最小1M的小文件來上傳。這邊使用到一個Model,該數(shù)據(jù)模型主要存放上傳列表中所需要的一些基本數(shù)據(jù)。因?yàn)槲覀兠看紊蟼魍暌黄枰耈I。由于這邊需要支持?jǐn)帱c(diǎn)續(xù)傳,因此需要記錄文件的進(jìn)度值,已上傳的片數(shù)我們需要保存下來。保存上傳文件路徑和文件進(jìn)度可以使用數(shù)據(jù)庫或則plist文件等方式,這邊需要保存的數(shù)據(jù)不是很多,所以我直接保存在了偏好設(shè)置中。每片文件上傳成功,設(shè)置該模型已上傳片數(shù),并且更新本地文件進(jìn)度值。
我們可以大致看下所用到的Model
YJTUploadManager.h
#import <Foundation/Foundation.h>@interface YJTDocUploadModel : NSObject// 方便操作(暫停取消)正在上傳的文件@property (nonatomic, strong) NSURLSessionDataTask *dataTask;// 總大小@property (nonatomic, assign) int64_t totalSize;// 總片數(shù)@property (nonatomic, assign) NSInteger totalCount;// 已上傳片數(shù)@property (nonatomic, assign) NSInteger uploadedCount;// 上傳所需參數(shù)@property (nonatomic, copy) NSString *upToken;// 上傳狀態(tài)標(biāo)識, 記錄是上傳中還是暫停@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);// 保存上傳成功后調(diào)用保存接口的參數(shù)@property (nonatomic, strong) NSMutableDictionary *parameters;@endYJTUploadManager.m#import "YJTDocUploadModel.h"@implementation YJTDocUploadModel// 上傳完畢后更新模型相關(guān)數(shù)據(jù)- (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.上傳
上傳可以采用同步和異步執(zhí)行。這里不太建議通過for遍歷來開太多的線程上傳,開線程是耗內(nèi)存的。這邊我是通過同步的方式。也就是采用遞歸,一片文件上傳完畢后再上傳下一片文件,如果失敗,再次上傳。有一點(diǎn)需要強(qiáng)調(diào),最后一片的大小一般都比會小于預(yù)設(shè)的最小分割值。另外,如果分的片段大小大于文件的總大小也可能會出問題,客戶端和服務(wù)器溝通好規(guī)則處理即可。
關(guān)于上傳進(jìn)度,可以粗略計(jì)算。也可使用NSURLSessionDataTask的countOfBytesSent實(shí)時(shí)監(jiān)控。其實(shí)NSURLSessionTask在iOS11以后還提供了progress屬性。附上核心代碼提供參考。
首次調(diào)用上傳接口
#pragma mark- first upload 斷點(diǎn)// 上傳初始化- (void)uploadData:(NSData *)data withModel:(YJTDocUploadModel *)model { // 計(jì)算片數(shù) 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; // 上傳所需參數(shù) 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"]; // 最后所有片段上傳完畢,服務(wù)器返回文件url,執(zhí)行后續(xù)操作 [self saveRequest:model]; }else { if (i < model.totalCount) { [self continueUploadWithModel:model]; } } } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) { // 上傳失敗重試 [self continueUploadWithModel:model]; }]; model.dataTask = dataTask;}
總結(jié)
以上所述是小編給大家介紹的iOS 斷點(diǎn)上傳文件的實(shí)現(xiàn)方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時(shí)回復(fù)大家的。在此也非常感謝大家對武林網(wǎng)網(wǎng)站的支持!
新聞熱點(diǎn)
疑難解答
圖片精選