項目開發中,有時候我們需要將本地的文件上傳到服務器,簡單的幾張圖片還好,但是針對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;@endYJTUploadManager.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;}
總結
以上所述是小編給大家介紹的iOS 斷點上傳文件的實現方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對VEVB武林網網站的支持!
新聞熱點
疑難解答