【iOS】關于大文件(如視頻)的分片上傳

前言

到目前為止,作為一個iOS開發(fā)人員详拙,在開發(fā)這條路上也走了2年多時間了帝际,之前一直忙于工作中的各種項目的開發(fā)。之前在一個項目里要做音視頻相關模塊饶辙,那個時候就開始著手研究相關的音視頻的播放蹲诀,還有配套的上傳下載的相關內(nèi)容,于是便有了本片文章的內(nèi)容弃揽。

運行效果

話不多說脯爪,直接上demo的運行效果,下面是進行上傳任務時的效果矿微。
image

使用方法

初始化


- (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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涩盾,一起剝皮案震驚了整個濱河市十气,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌春霍,老刑警劉巖砸西,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異址儒,居然都是意外死亡芹枷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門莲趣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸳慈,“玉大人,你說我怎么就攤上這事喧伞∽哂螅” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵潘鲫,是天一觀的道長翁逞。 經(jīng)常有香客問我,道長溉仑,這世上最難降的妖魔是什么挖函? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮彼念,結(jié)果婚禮上挪圾,老公的妹妹穿的比我還像新娘。我一直安慰自己逐沙,他們只是感情好哲思,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吩案,像睡著了一般棚赔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徘郭,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天靠益,我揣著相機與錄音,去河邊找鬼残揉。 笑死胧后,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的抱环。 我是一名探鬼主播壳快,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼镇草!你這毒婦竟也來了眶痰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤梯啤,失蹤者是張志新(化名)和其女友劉穎竖伯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體因宇,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡七婴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了察滑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片本姥。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖杭棵,靈堂內(nèi)的尸體忽然破棺而出婚惫,到底是詐尸還是另有隱情,我是刑警寧澤魂爪,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布先舷,位于F島的核電站,受9級特大地震影響滓侍,放射性物質(zhì)發(fā)生泄漏蒋川。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一撩笆、第九天 我趴在偏房一處隱蔽的房頂上張望捺球。 院中可真熱鬧缸浦,春花似錦、人聲如沸氮兵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泣栈。三九已至卜高,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間南片,已是汗流浹背掺涛。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疼进,地道東北人薪缆。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像伞广,于是被迫代替她去往敵國和親矮燎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容