前言
到目前為止,作為一個iOS開發(fā)人員详拙,在開發(fā)這條路上也走了2年多時間了帝际,之前一直忙于工作中的各種項目的開發(fā)。之前在一個項目里要做音視頻相關模塊饶辙,那個時候就開始著手研究相關的音視頻的播放蹲诀,還有配套的上傳下載的相關內(nèi)容,于是便有了本片文章的內(nèi)容弃揽。
運行效果
話不多說脯爪,直接上demo的運行效果,下面是進行上傳任務時的效果矿微。
使用方法
初始化
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
/*在App的啟動回調(diào)方法里面初始化上傳管里類*/
[[CWFileUploadManager shardUploadManager] config:[self setUpRequest] maxTask:3];
return YES;
}
/*提供一個可供管理器初始化的參數(shù)*/
- (NSMutableURLRequest *)setUpRequest{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/upload/file",CURRENT_API]];
NSMutableURLRequest* request=[NSMutableURLRequest requestWithURL:url];
request.HTTPMethod=@"POST";
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"1a2b3c"] forHTTPHeaderField:@"Content-Type"];
[request setValue:@"v1" forHTTPHeaderField:@"api_version"];
return request;
}
新建上傳任務并開始上傳
//新建上傳任務 @param filePath 待上傳文件的地址
CWUploadTask *task = [[CWFileUploadManager shardUploadManager] createUploadTask:filePath];
//開始上傳
[task taskResume];
設計思路
這里必須提到一些常規(guī)的上傳方式痕慢,基本上項目中用到的上傳比較多的是圖片上傳,而且一般最多不會超過9張(例如微信)涌矢,所以在不太講究的情況下可以直接使用將圖片讀取為NSData類型加載到內(nèi)存中掖举,然后在使用NSURLSessionTask派生出的一些子類(例如NSURLSessionUploadTask)直接調(diào)用系統(tǒng)提供的api進行上傳就行了。這種類型的文件上傳由于單個文件的體積太小娜庇,即使上傳失敗了再重復上傳也不礙事塔次,所以也不用考慮續(xù)傳的問題。
視頻這種大文件則不能忽略上面這些問題名秀,由于文件體積的增大励负,由量變產(chǎn)生的質(zhì)變之后再使用上面的常規(guī)方式上傳則會產(chǎn)生一系列風險。首先是斷點續(xù)傳的問題匕得,例如視頻上傳的過程中如果由于網(wǎng)絡或者服務端的一些原因而上傳失敗继榆,這個時候在進行重新上傳顯然實不可取的,用戶也難以接受耗跛。再者一次性將文件讀取到內(nèi)存中也會因為文件過大導致內(nèi)存暴漲裕照,這樣app就有崩潰閃退的風險攒发。因此文件的分片再上傳也就成了順理成章的方案调塌。
文件分片這個功能我設計了一個單獨的類cwfilestreamseparation去完成它提供文件路徑讓類的初始化方法返回一個記錄文件分片信息的對象
-(instancetype)initFileOperationAtPath:(NSString*)path forReadOperation:(BOOL)isReadOperation {
if (self = [super init]) {
self.isReadOperation = isReadOperation;
if (_isReadOperation) {
if (![self getFileInfoAtPath:path]) {
return nil;
}
self.readFileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
[self cutFileForFragments];
} else {
NSFileManager *fileMgr = [NSFileManager defaultManager];
if (![fileMgr fileExistsAtPath:path]) {
[fileMgr createFileAtPath:path contents:nil attributes:nil];
}
if (![self getFileInfoAtPath:path]) {
return nil;
}
self.writeFileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
}
}
return self;
}
通過方法對文件進行切割
//切分文件片段
-(void)cutFileForFragments {
NSUInteger offset = CWStreamFragmentMaxSize;
// 塊數(shù)
NSUInteger chunks = (_fileSize%offset==0)?(_fileSize/offset):(_fileSize/(offset) + 1);
NSMutableArray<CWStreamFragment *> *fragments = [[NSMutableArray alloc] initWithCapacity:0];
for (NSUInteger i = 0; i < chunks; i ++) {
CWStreamFragment *fFragment = [[CWStreamFragment alloc] init];
fFragment.fragmentStatus = NO;
fFragment.fragmentId = [[self class] fileKey];
fFragment.fragementOffset = i * offset;
if (i != chunks - 1) {
fFragment.fragmentSize = offset;
} else {
fFragment.fragmentSize = _fileSize - fFragment.fragementOffset;
}
[fragments addObject:fFragment];
}
self.streamFragments = fragments;
將切割文件產(chǎn)生的片段信息保存到類CWStreamFragment里
@property (nonatomic,copy)NSString *fragmentId; //片的唯一標識
@property (nonatomic,assign)NSUInteger fragmentSize; //片的大小
@property (nonatomic,assign)NSUInteger fragementOffset;//片的偏移量
@property (nonatomic,assign)BOOL fragmentStatus; //上傳狀態(tài) YES上傳成功
通過以上操作產(chǎn)生出一個CWFileStreamSeparation類的實例對象,這個對象將作為上傳任務的數(shù)據(jù)模型惠猿。
上傳任務
這里新建一個上傳任務的類CWUploadTask來實例化每個上傳任務對象羔砾,通過上傳任務對象去管理操作每個上傳任務,CWUploadTask會根據(jù)一個CWFileStreamSeparation模型來實例化。
+(instancetype)initWithStreamModel:(CWFileStreamSeparation *)fileStream
{
CWUploadTask *task = [CWUploadTask new];
task.fileStream = fileStream;
task.isSuspendedState = NO;
task.url = [CWFileUploadManager shardUploadManager].url;
return task;
}
由于上傳任務是一個持續(xù)與服務端交互的動作姜凄,因此在設計之初先要與服務端做各種約定政溃,這里我與服務端約定在上傳前先上傳文件信息用于服務端做驗證,同時服務端會返回任務上傳的初始參數(shù)态秧。這個返回的json數(shù)據(jù)里面的參數(shù)只有一個記錄文件分片編號的參數(shù)是會根據(jù)當前上傳的片段去做相應的更改董虱,因此我建立了一個CWUploadTask的Category來專門實現(xiàn)這個獲取參數(shù)與驗證文件的網(wǎng)絡請求方法。
-(void)checkParamFromServer:(CWFileStreamSeparation *_Nonnull)fileStream
paramCallback:(void(^ _Nullable)(NSString *_Nonnull chunkNumName,NSDictionary *_Nullable param))paramBlock
{
NSString *uploadFileInfoUrl=[NSString stringWithFormat:@"%@/upload/checkFileChunk",CURRENT_API];
NSURL *url = [NSURL URLWithString:uploadFileInfoUrl];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *args = [NSString stringWithFormat:@"bizId=%@&fileName=%@&saveName=%@&chunks=%@",fileStream.bizId,fileStream.fileName,fileStream.md5String,[NSString stringWithFormat:@"%zd",fileStream.streamFragments.count]];
NSLog(@"%@",args);
request.HTTPMethod = @"POST";//設置請求類型
[request setValue:@"v1" forHTTPHeaderField:@"api_version"];
request.HTTPBody = [args dataUsingEncoding:NSUTF8StringEncoding];//設置參數(shù)
NSURLSession *session = [NSURLSession sharedSession];
//發(fā)送請求
NSURLSessionDataTask *postTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
//解析得到的數(shù)據(jù)
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
if ([dict[@"code"] isEqualToString:@"500"]) {
NSLog(@"%@",dict[@"desc"]);
return;
}
NSMutableDictionary *tmpParam = [NSMutableDictionary dictionary];
[tmpParam setDictionary:dict[@"data"]];
paramBlock(@"chunk",tmpParam);
}
}];
[postTask resume];
這里我直接使用了NSURLSessionDataTask做POST請求了申鱼,沒有使用AFNetworking愤诱,改寫為其它任意方式都無所謂。
獲取了參數(shù)傳遞的方式捐友,下面就能開始上傳了
//上傳文件的核心方法
-(void)startExe{
//判斷無參數(shù)的情況下先將文件信息上傳并獲得參數(shù)
if (!_param) [self postFileInfo];
dispatch_group_t group = dispatch_group_create();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
if (_fileStream.fileStatus == CWUploadStatusFinished && _successBlock) {
_successBlock(_fileStream);
return;
};
for (NSInteger i=0; i<_fileStream.streamFragments.count; i++) {
CWStreamFragment *fragment = _fileStream.streamFragments[i];
if (fragment.fragmentStatus) continue;
dispatch_group_async(group, queue, ^{
@autoreleasepool {
NSData *data = [_fileStream readDateOfFragment:fragment];
__weak typeof(self) weekSelf = self;
[_param setObject:[NSString stringWithFormat:@"%zd",(i+1)] forKey:_chunkNumName];
[self uploadTaskWithUrl:_url param:_param uploadData:data completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if (!error && httpResponse.statusCode==200) {
_taskRepeatNum = 0;
fragment.fragmentStatus = YES;
_fileStream.fileStatus = CWUploadStatusUpdownloading;
[weekSelf archTaskFileStream];
weekSelf.lastParam = _param;
weekSelf.chunkNo = i+1;
dispatch_async(dispatch_get_main_queue(), ^{
if (_finishBlock) _finishBlock(_fileStream,nil);
[self sendNotionWithKey:@"CWUploadTaskExeing" userInfo:@{@"fileStream":_fileStream,@"lastParam":_lastParam,@"indexNo":@(_chunkNo)}];
});
dispatch_semaphore_signal(semaphore);
}else{
if (_taskRepeatNum<REPEAT_MAX) {
_taskRepeatNum++;
[weekSelf startExe];
}else{
_fileStream.fileStatus = CWUploadStatusFailed;
dispatch_async(dispatch_get_main_queue(), ^{
if (_finishBlock) _finishBlock(_fileStream,error);
[self sendNotionWithKey:@"CWUploadTaskExeError" userInfo:@{@"fileStream":_fileStream,@"error":error}];
});
[weekSelf deallocSession];
return;
}
}
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
});
}
dispatch_group_notify(group, queue, ^{
_fileStream.fileStatus = CWUploadStatusFinished;
[self archTaskFileStream];
if (_finishBlock) _finishBlock(_fileStream,nil);
[self deallocSession];
});
這里對文件的片段做了循環(huán)的上傳淫半,由于網(wǎng)絡請求是一個異步的操作,同時也考慮到太多并發(fā)(當然系統(tǒng)對于網(wǎng)絡請求開辟的線程個數(shù)也有限制)對于手機性能的影響匣砖,因此利用GCD信號量等待這種功能特性讓一個片段上傳完之后再進行下一個片段的上傳。
mark: 在這里有一個坑
當時在調(diào)試上傳時我發(fā)現(xiàn)隨著上傳任務的進行,app所占用的內(nèi)存也會不斷地上漲蚯妇,本來分片上傳就是要去避免內(nèi)存占用過大的問題滞详,但是調(diào)試時還是出現(xiàn)了內(nèi)存不斷增長的問題。之后我開始著手分析問題產(chǎn)生的原因拂共,首先來講它的內(nèi)存是隨著上傳任務的執(zhí)行時間線性的上漲的规伐,通過這個就可以判斷內(nèi)存增長的原因并不是分片讀取失敗而直接讀取大文件到內(nèi)存中。雖然排除了一部分原因匣缘,但是問題還沒有解決猖闪。還是通過內(nèi)存的線性增長這個特性,我隱約感覺到是每一片的上傳任務執(zhí)行完之后并沒有立即的釋放內(nèi)存肌厨,那么在不斷地循環(huán)上傳的過程中內(nèi)存的占用就會不斷升高直到程序崩潰培慌。后面通過調(diào)試證實了這個猜想。知道原因之后就著手解決柑爸,這里說個題外話吵护,ARC這個機制確實好,讓我們iOS開發(fā)過程中基本沒有去寫管理內(nèi)存的代碼表鳍,不過遇到這種情況就必須親自上陣來管理這一部分的內(nèi)存了馅而,在循環(huán)的方法中加了個autoreleasepool,讓執(zhí)行完的任務及時的釋放內(nèi)存譬圣,于是就填上了這個坑瓮恭。
mark:填了個新坑
在文件分片中有生成文件的MD5摘要的方法
+ (NSString *)fileKey {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef cfstring = CFUUIDCreateString(kCFAllocatorDefault, uuid);
const char *cStr = CFStringGetCStringPtr(cfstring,CFStringGetFastestEncoding(cfstring));
unsigned char result[16];
CC_MD5( cStr, (unsigned int)strlen(cStr), result );
CFRelease(uuid);
CFRelease(cfstring);
return [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%08lx",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15],
(unsigned long)(arc4random() % NSUIntegerMax)];
}
注意其中有CF開頭的方法和變量,這是CoreFoundation中的東西厘熟,這個框架里的對象需要自己調(diào)用CFRelease()方法進行回收屯蹦,之前忘了寫维哈,導致了內(nèi)存泄漏。
已經(jīng)能夠?qū)崿F(xiàn)上傳了登澜,接下來當然是任務管理
任務管理
考慮到實際需求阔挠,這里需要提供一個單例來作為上傳任務的管理器。這里就沒有太多可說的了脑蠕,無非是提供一些容器來分門別類的操作不同狀態(tài)的上傳任務购撼。我這里是將一些任務建立,全部任務的刪除谴仙,暫停之類的方法設計在管理類之上
//根據(jù)文件路徑創(chuàng)建上傳任務
- (CWUploadTask *_Nullable)createUploadTask:(NSString *_Nonnull)filePath;
/**
刪除一個上傳任務份招,同時會刪除當前任務上傳的緩存數(shù)據(jù)
@param fileStream 上傳文件的路徑
*/
- (void)removeUploadTask:(CWFileStreamSeparation *_Nonnull)fileStream;
/**
暫停所有上傳任務
*/
- (void)pauseAllUploadTask;
/**
刪除所有上傳任務
*/
- (void)removeAllUploadTask;
以及一些可能常用的對任務分類的容器
//總?cè)蝿諗?shù)
@property (nonatomic,readonly)NSMutableDictionary *allTasks;
//正在上傳中的任務
@property (nonatomic,readonly)NSMutableDictionary *uploadingTasks;
//正在等待上傳的任務
@property (nonatomic,readonly)NSMutableDictionary *uploadWaitTasks;
//已經(jīng)上傳完的任務
@property (nonatomic,readonly)NSMutableDictionary *uploadEndTasks;
同時一些全局相關的設置也放在這個管理類中
//配置全局默認參數(shù)
/**
@param request 默認請求頭
@param num 最大任務數(shù),默認為3
*/
- (void)config:(NSMutableURLRequest * _Nonnull)request maxTask:(NSInteger)num;
PS:到這里為止狞甚,整個文件分片上傳的思路就寫完了锁摔,如果這一篇東西能給大家提供一些思路那最好了,如果有什么疏漏也請各位大佬直接指出哼审。谐腰。。
附上demo