iOS 利用AFNetworking實(shí)現(xiàn)大文件分片上傳
一說(shuō)到文件上傳,想必大家都并不陌生,更何況是利用 AFNetworking (PS:后期統(tǒng)稱AF)來(lái)做,那更是小菜一碟。比如開(kāi)發(fā)中常見(jiàn)的場(chǎng)景:頭像上傳,九宮格圖片上傳...等等,這些場(chǎng)景無(wú)一不使用到文件上傳的功能。如果利用AF來(lái)實(shí)現(xiàn),無(wú)非就是客戶端調(diào)用AF提供的文件上傳接口即可,API如下所示:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(nullable id)parameters constructingBodyWithBlock:(nullable void (^)(id formData))block progress:( nullablevoid (^)( NSProgress *uploadProgress))uploadProgress success:( nullablevoid (^)( NSURLSessionDataTask *task,id _Nullable responseObject))success failure:( nullablevoid (^)( NSURLSessionDataTask * _Nullable task,NSError *error))failure;
上面這種場(chǎng)景,主要是針對(duì)一些小資源文件的上傳,上傳過(guò)程耗時(shí)較短,用戶可以接受。但是一旦資源文件過(guò)大(比如1G以上),則必須要考慮上傳過(guò)程網(wǎng)絡(luò)中斷的情況。試想我們還是采用上述方案,一口氣把這整個(gè)1G的資源文件上傳到服務(wù)器,這顯然是不現(xiàn)實(shí)的,就算服務(wù)器答應(yīng),用戶也不答應(yīng)的。考慮到網(wǎng)絡(luò)使用中斷或服務(wù)器上傳異常...等場(chǎng)景,那么我們恢復(fù)網(wǎng)絡(luò)后又得重新從頭開(kāi)始上傳,那之前已經(jīng)上傳完成的部分資源豈不作廢,這種耗時(shí)耗力的工作,顯然是不符合常理的。為了解決大文件上傳的存在如此雞肋的問(wèn)題,從而誕生了一個(gè)叫: 分片上傳(斷點(diǎn)續(xù)上傳)
分片上傳(斷點(diǎn)續(xù)上傳)主要是為了保證在網(wǎng)絡(luò)中斷后1G的資源文件已上傳的那部分在下次網(wǎng)絡(luò)連接時(shí)不必再重傳。所以我們本地在上傳的時(shí)候,要將大文件進(jìn)行切割分片,比如分成1024*1024B,即將大文件分成1M的片進(jìn)行上傳,服務(wù)器在接收后,再將這些片合并成原始文件,這就是 分片 的基本原理。斷點(diǎn)續(xù)傳要求本地要記錄每一片的上傳的狀態(tài),我通過(guò)三個(gè)狀態(tài)進(jìn)行了標(biāo)記(waiting loading finish),當(dāng)網(wǎng)絡(luò)中斷,再次連接后,從斷點(diǎn)處進(jìn)行上傳。服務(wù)器通過(guò)文件名、總片數(shù)判斷該文件是否已全部上傳完成。
弄懂了 分片上傳(斷點(diǎn)續(xù)上傳) 的基本原理,其核心就是 分片 ,然后將分割出來(lái)的的每一片,按照類似上傳頭像的方式上傳到服務(wù)器即可,全部上傳完后再在服務(wù)端將這些小數(shù)據(jù)片合并成為一個(gè)資源。
分片上傳引入了兩個(gè)概念: 塊(block) 和 片(fragment) 。每個(gè)塊由一到多個(gè)片組成,而一個(gè)資源則由一到多個(gè)塊組成。他們之間的關(guān)系可以用下圖表述:
文件資源組成關(guān)系.png
本文筆者將著重分析 分片上傳 實(shí)現(xiàn)的具體過(guò)程以及細(xì)節(jié)處理,爭(zhēng)取把里面的所有涵蓋的知識(shí)點(diǎn)以及細(xì)節(jié)處理分析透徹。希望為大家提供一點(diǎn)思路,少走一些彎路,填補(bǔ)一些細(xì)坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評(píng)指正。
效果圖如下:
雖然 分片上傳 的原理看似非常簡(jiǎn)單,但是落實(shí)到具體的實(shí)現(xiàn),其中還是具有非常多的細(xì)節(jié)分析和邏輯處理,而且都是我們開(kāi)發(fā)中不常用到的知識(shí)點(diǎn),這里筆者就總結(jié)了一下 分片上傳 所用到的知識(shí)點(diǎn)和使用場(chǎng)景,以及借助一些第三方框架,來(lái)達(dá)到分片上傳的目的。
圖片和視頻資源的獲取
所謂文件上傳,前提必須得有文件,而文件一般是本地文件,本地文件的獲取來(lái)源一般是系統(tǒng)相冊(cè)獲取,關(guān)于如何從系統(tǒng)相冊(cè)中獲取圖片或視頻資源,這里筆者采用 TZImagePickerController 一個(gè)支持多選、選原圖和視頻的圖片選擇器,同時(shí)有預(yù)覽、裁剪功能,支持iOS6+第三方框架。根據(jù)TZImagePickerControllerDelegate返回的資源(圖片、視頻)數(shù)據(jù),然后利用TZImageMananger提供的API,獲取到原始圖片和視頻資源。關(guān)鍵API如下:具體使用請(qǐng)參照TZImagePickerController提供Demo。
/// 獲取原圖 - (void)getOriginalPhotoDataWithAsset:(id)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; - (void)getOriginalPhotoDataWithAsset:(id)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; /// 獲得視頻 - (void)getVideoWithAsset:(id)asset completion:(void (^)(AVPlayerItem * playerItem, NSDictionary * info))completion; - (void)getVideoWithAsset:(id)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(AVPlayerItem *, NSDictionary *))completion;
文件讀寫(xiě)和剪切
文件寫(xiě)入一般用于從相冊(cè)中獲取到圖片的原圖data,然后將其寫(xiě)入到指定的文件夾中,一般調(diào)用NSData提供的方法。
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile
文件剪切一般用于從相冊(cè)中獲取到視頻資源,其視頻格式是mov格式的,需要我們視頻壓縮轉(zhuǎn)成mp4格式,壓縮成功后一般將其導(dǎo)入到APP沙盒文件的tmp目錄下,總所周知,tmp里面一般存放一些臨時(shí)文件,所以需要將其導(dǎo)入到Cache文件夾中去,這里用文件移動(dòng)(剪切)再好不過(guò)了,而且不需要讀取到內(nèi)存中去。 直接調(diào)用 NSFileManager的提供的API即可:
- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error
文件讀取一般主要用于讀取每一個(gè)文件片的大小,需要利用NSFileHandle來(lái)處理,調(diào)用其如下API來(lái)完成。
- (NSData *)readDataOfLength:(NSUInteger)length;- (void)seekToFileOffset:(unsigned long long)offset;+ (nullable instancetype)fileHandleForReadingAtPath:(NSString *)path;
綜上所述:NSData,NSFileManager,NSFileHandle的API的常規(guī)使用得比較熟練。
視頻壓縮
系統(tǒng)的錄制視頻導(dǎo)出的格式是mov,所以一般的做法就是壓縮轉(zhuǎn)化成mp4格式,這樣就得用到系統(tǒng)的視頻壓縮方法,大家可以自行百度AVAssetExportSession的使用。這里筆者采用TZImagePickerController提供的API來(lái)做的,具體請(qǐng)參照TZImageManager提供的方法,大家可以看看其實(shí)現(xiàn)。
/// Export video 導(dǎo)出視頻 presetName: 預(yù)設(shè)名字,默認(rèn)值是AVAssetExportPreset640x480- (void)getVideoOutputPathWithAsset:(id)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;- (void)getVideoOutputPathWithAsset:(id)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
資源緩存
所謂資源緩存,就是一般從系統(tǒng)相冊(cè)中獲取到的資源(圖片、視頻),我們會(huì)將資源另存到在/Library/Caches/Upload目錄下,然后把資源存放的相對(duì)路徑給緩存起來(lái),下次從系統(tǒng)相冊(cè)中選取相同的資源,如果已經(jīng)存在于/Library/Caches/Upload目錄下,則不需要去獲取原始圖片,或者壓縮視頻了。這里筆者采用的是: YYCache 來(lái)做內(nèi)存緩存和磁盤(pán)緩存。具體使用,還請(qǐng)自行百度。
數(shù)據(jù)庫(kù)
數(shù)據(jù)庫(kù)主要用于,保存新建資源,保存上傳資源,保存文件片...等等,利用數(shù)據(jù)庫(kù)的增,刪,改,查等功能,更加方便快捷的處理文件片的上傳狀態(tài),上傳進(jìn)度,獲取或刪除草稿數(shù)據(jù)...等等一些列的操作,大大提供了開(kāi)發(fā)的效率。這里筆者采用的是基于 FMDB 封裝的 BGFMDB 框架,BGFMDB是對(duì)FMDB面相對(duì)象層的封裝,且?guī)缀踔С执鎯?chǔ)iOS所有基本的自帶數(shù)據(jù)類型,讓數(shù)據(jù)的增,刪,改,查分別只需要一行代碼即可。具體使用,還請(qǐng)查看BGFMDB提供的Demo。
多線程
多線程的使用主要用在,① 從系統(tǒng)相冊(cè)獲取到資源(圖片、視頻),對(duì)資源進(jìn)行處理(比如,獲取原圖,壓縮視頻等等); ② 文件分片上傳。其實(shí)現(xiàn)實(shí)開(kāi)發(fā)中,我們使用多線程的的場(chǎng)景并不多,但反觀使用多線程最多的場(chǎng)景就是--面試。多線程其實(shí)是iOS中非常重要的知識(shí)點(diǎn),但是由于平時(shí)疏于練習(xí)和使用,腦子里面可能只有少許多線程的相關(guān)知識(shí)。此次筆者在項(xiàng)目中做大文件分片上傳功能,也讓筆者重拾了多線程的相關(guān)知識(shí),而且運(yùn)用到實(shí)際開(kāi)發(fā)中去,也是一個(gè)不小的收獲。這里筆者就講講本模塊中用到了哪些多線程的知識(shí),當(dāng)然具體的理論知識(shí)和實(shí)踐操作,大家可以參照下面筆者分享的網(wǎng)址去針對(duì)性的學(xué)習(xí)和實(shí)踐多線程的相關(guān)知識(shí)。具體如下:
iOS多線程:『GCD』詳盡總結(jié)
特別提醒: ① 必須掌握GCD 隊(duì)列組:dispatch_group。合理使用dispatch_group_enter、dispatch_group_leave 和 dispatch_group_notify的配套使用。
② 必須掌握GCD 信號(hào)量:dispatch_semaphore。熟練使用dispatch_semaphore_create、dispatch_semaphore_signal和dispatch_semaphore_wait的配套使用,利用dispatch_semaphore保持線程同步,將異步執(zhí)行任務(wù)轉(zhuǎn)換為同步執(zhí)行任務(wù)以及保證線程安全,為線程加鎖。
iOS多線程:『NSOperation、NSOperationQueue』詳盡總結(jié)
模塊關(guān)于筆者在Demo中提供的文件分片上傳的示例程序,雖然不夠華麗,但麻雀雖小,五臟俱全,大家湊合著看咯。但總的來(lái)說(shuō),可以簡(jiǎn)單分為以下幾個(gè)模塊:
資源新建: 系統(tǒng)相冊(cè)獲取資源文件(圖片、視頻);獲取原圖或視頻壓縮,并導(dǎo)入到沙盒指定的文件夾;資源緩存。
后臺(tái)接口: 考慮到示例程序中部分業(yè)務(wù)邏輯是按照后臺(tái)提供的API設(shè)計(jì)的,所以有必要分享一下后臺(tái)提供了哪些API,以及具體的使用的場(chǎng)景。
文件分片: 將新建資源,轉(zhuǎn)化為上傳資源,將資源中存放的每一個(gè)文件塊,按照512k的大小分成若干個(gè)文件片。涉及到新建資源存儲(chǔ)數(shù)據(jù)庫(kù),上傳資源存儲(chǔ)數(shù)據(jù)庫(kù),以及每個(gè)文件片存儲(chǔ)數(shù)據(jù)庫(kù)。
草稿存儲(chǔ): 草稿列表的數(shù)據(jù)來(lái)源主要分為手動(dòng)存草稿和自動(dòng)存草稿。手動(dòng)存草稿一般是指用戶手動(dòng)點(diǎn)擊存草稿按鈕保存草稿,此草稿數(shù)據(jù)可以進(jìn)行二次編輯;自動(dòng)存草稿一般是指用戶點(diǎn)擊提交按鈕上傳資源文件,由于一時(shí)半會(huì)不會(huì)上傳到服務(wù)器上去,所以需要報(bào)存草稿,此草稿數(shù)據(jù)可以顯示上傳進(jìn)度和上傳狀態(tài),用戶可以點(diǎn)擊暫停/開(kāi)始上傳此草稿,但不允許二次編輯。當(dāng)然,草稿數(shù)據(jù)都是可以手動(dòng)刪除的。
分片上傳 <核心> : 將上傳資源中所有分好的文件片,上傳到服務(wù)器中去,當(dāng)網(wǎng)絡(luò)中斷或程序異常都可以支持?jǐn)帱c(diǎn)續(xù)傳,保證在網(wǎng)絡(luò)中斷后該上傳資源中已上傳的那部分文件片在下次網(wǎng)絡(luò)連接時(shí)或程序啟動(dòng)后不必再重傳。涉及到更新資源進(jìn)度,更新資源狀態(tài),以及每一個(gè)文件片的上傳狀態(tài)。
資源新建資源新建模塊的UI搭建,筆者這里就不過(guò)多贅述,這里更多討論的是功能邏輯和細(xì)節(jié)處理。具體內(nèi)容還請(qǐng)查看CMHCreateSourceController.h/m
設(shè)置TZImagePickerController導(dǎo)出圖片寬度
默認(rèn)情況下,TZImagePickerController (PS:后期統(tǒng)稱TZ) 默認(rèn)導(dǎo)出的圖片寬度為828px,具體請(qǐng)查看TZ提供的photoWidth屬性。考慮到手動(dòng)存草稿可以是二次編輯,所以有必要把TZ返回的圖片儲(chǔ)存到數(shù)據(jù)庫(kù)中,所以我們只需要存儲(chǔ)縮略圖即可,何況新建資源模塊本身頁(yè)面也只展示小圖,完全沒(méi)必要導(dǎo)出寬度為828px的圖片,這樣會(huì)導(dǎo)致數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)讀取都異常緩慢,解決方案如下:
/// CoderMikeHe Fixed Bug : 這里新建模塊只需要展示,小圖,所以導(dǎo)出圖片不需要太大,/// 而且導(dǎo)出的圖片需要存入數(shù)據(jù)庫(kù),所以盡量尺寸適量即可,否則會(huì)導(dǎo)致存儲(chǔ)數(shù)據(jù)庫(kù)和讀取數(shù)據(jù)庫(kù)異常的慢 imagePickerVc.photoWidth = ceil(MH_SCREEN_WIDTH / 4);
PHAsset 保存數(shù)據(jù)庫(kù)
默認(rèn)情況下,TZ是支持本地圖片預(yù)覽的,需要我們提供一組selectedAssets,里面裝著PHAsset對(duì)象,如果我們處于新建資源頁(yè)面時(shí),這完全沒(méi)有問(wèn)題;一旦我們手動(dòng)存草稿,進(jìn)行二次編輯時(shí),就會(huì)出現(xiàn)問(wèn)題,原因就是PHAsset不遵守NSCoding協(xié)議,無(wú)法進(jìn)行歸檔。解決方案其實(shí)就是儲(chǔ)存PHAsset的localIdentifier即可。通過(guò)localIdentifier獲取PHAsset代碼如下:
/// 獲取PHAssetPHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[file.localIdentifier] options:nil];PHAsset *asset = fetchResult.firstObject;if (!asset) { // 這種場(chǎng)景就是這張照片儲(chǔ)存完P(guān)HAsset以后,但用戶在手機(jī)上把這張照片刪除}
資源(圖片,視頻)處理
常規(guī)邏輯:第一步,通過(guò)TZ從系統(tǒng)相冊(cè)中獲取一組資源(圖片、視頻)文件,第二步,遍歷資源列表根據(jù)PHAsset去獲取原圖數(shù)據(jù)或壓縮視頻,第三步將處理過(guò)的資源保存到Cache/Upload文件夾中。看起來(lái)該方案看似穩(wěn)如藏獒,但是實(shí)際情況第二步、第三步操作,其實(shí)是非常耗內(nèi)存的,而且每次獲取系統(tǒng)相冊(cè)中同一個(gè)的資源(PHAsset),第二步、第三步處理過(guò)后都是一樣的,如果該資源(PHAsset)之前已經(jīng)通過(guò)第二步、第三步處理過(guò),那么后面在使用到該資源是不是完全沒(méi)有必要進(jìn)行第二步和第三步操作,所以這里就必須用到數(shù)據(jù)緩存(磁盤(pán)緩存+內(nèi)存緩存)。 最終方案如下:
從上圖明顯可知,只有兩種場(chǎng)景才會(huì)去執(zhí)行第二步、第三步處理,且都是由于不存在磁盤(pán)中導(dǎo)致的。這里有一個(gè)比較細(xì)節(jié)的地方:緩存相對(duì)路徑。千萬(wàn)不要緩存絕對(duì)路徑,因?yàn)殡S著APP的更新或重裝,都會(huì)導(dǎo)致應(yīng)用的沙盒的絕對(duì)路徑是會(huì)改變的。
實(shí)現(xiàn)代碼如下:
/// 完成圖片選中- (void)_finishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray<NSDictionary *> *)infos{ /// 選中的相片以及Asset self.selectedPhotos = [NSMutableArray arrayWithArray:photos]; self.selectedAssets = [NSMutableArray arrayWithArray:assets]; /// 記錄一下是否上傳原圖 self.source.selectOriginalPhoto = isSelectOriginalPhoto; /// 生成資源文件 __block NSMutableArray *files = [NSMutableArray array]; /// 記錄之前的源文件 NSMutableArray *srcFiles = [NSMutableArray arrayWithArray:self.source.files]; NSInteger count = MIN(photos.count, assets.count); /// 處理資源 /// CoderMikeHe Fixed Bug : 這里可能會(huì)涉及到選中多個(gè)視頻的情況,且需要壓縮視頻的情況 [MBProgressHUD mh_showProgressHUD:@'正在處理資源...' addedToView:self.view]; NSLog(@'Compress Source Complete Before %@ !!!!' , [NSDate date]); /// 獲取隊(duì)列組 dispatch_group_t group = dispatch_group_create(); /// 創(chuàng)建信號(hào)量 用于線程同步 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (NSInteger i = 0; i < count; i ++ ) { dispatch_group_enter(group); dispatch_async(_compressQueue, ^{ // 異步追加任務(wù) /// 設(shè)置文件類型 PHAsset *asset = assets[i]; /// 圖片或資源 唯一id NSString *localIdentifier = [[TZImageManager manager] getAssetIdentifier:asset]; UIImage *thumbImage = photos[i]; /// 這里要去遍歷已經(jīng)獲取已經(jīng)存在資源的文件 內(nèi)存中 BOOL isExistMemory = NO; for (CMHFile *f in srcFiles.reverseObjectEnumerator) { /// 判斷是否已經(jīng)存在路徑和文件 if ([f.localIdentifier isEqualToString:localIdentifier] && MHStringIsNotEmpty(f.filePath)) { [files addObject:f]; [srcFiles removeObject:f]; isExistMemory = YES; break; } } if (isExistMemory) { NSLog(@'++++ 文件已經(jīng)存在內(nèi)存中 ++++'); dispatch_group_leave(group); }else{ //// 視頻和圖片,需要緩存,這樣會(huì)明顯減緩,應(yīng)用的內(nèi)存壓力 /// 是否已經(jīng)緩存在沙盒 BOOL isExistCache = NO; /// 1. 先去緩存里面去取 NSString *filePath = (NSString *)[[YYCache sharedCache] objectForKey:localIdentifier]; /// 這里必須的判斷一下filePath是否為空! 以免拼接起來(lái)出現(xiàn)問(wèn)題 if (MHStringIsNotEmpty(filePath)) { /// 2. 該路徑的本地資源是否存在, 拼接絕對(duì)路徑,filePath是相對(duì)路徑 NSString * absolutePath = [[CMHFileManager cachesDir] stringByAppendingPathComponent:filePath]; if ([CMHFileManager isExistsAtPath:absolutePath]) { /// 3. 文件存在沙盒中,不需要獲取了 isExistCache = YES; /// 創(chuàng)建文件模型 CMHFile *file = [[CMHFile alloc] init]; file.thumbImage = thumbImage; file.localIdentifier = localIdentifier; /// 設(shè)置文件類型 file.fileType = (asset.mediaType == PHAssetMediaTypeVideo)? CMHFileTypeVideo : CMHFileTypePicture; file.filePath = filePath; [files addObject:file]; } } if (isExistCache) { NSLog(@'++++ 文件已經(jīng)存在磁盤(pán)中 ++++'); dispatch_group_leave(group); }else{ /// 重新獲取 if (asset.mediaType == PHAssetMediaTypeVideo) { /// 視頻 /// 獲取視頻文件 [[TZImageManager manager] getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetMediumQuality success:^(NSString *outputPath) { NSLog(@'+++ 視頻導(dǎo)出到本地完成,沙盒路徑為:%@ %@',outputPath,[NSThread currentThread]); /// Export completed, send video here, send by outputPath or NSData /// 導(dǎo)出完成,在這里寫(xiě)上傳代碼,通過(guò)路徑或者通過(guò)NSData上傳 /// CoderMikeHe Fixed Bug :如果這樣寫(xiě)[NSData dataWithContentsOfURL:xxxx]; 文件過(guò)大,會(huì)導(dǎo)致內(nèi)存吃緊而閃退 /// 解決辦法,直接移動(dòng)文件到指定目錄《類似剪切》 NSString *relativePath = [CMHFile moveVideoFileAtPath:outputPath]; if (MHStringIsNotEmpty(relativePath)) { CMHFile *file = [[CMHFile alloc] init]; file.thumbImage = thumbImage; file.localIdentifier = localIdentifier; /// 設(shè)置文件類型 file.fileType = CMHFileTypeVideo; file.filePath = relativePath; [files addObject:file]; /// 緩存路徑 [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier]; } dispatch_group_leave(group); /// 信號(hào)量+1 向下運(yùn)行 dispatch_semaphore_signal(semaphore); } failure:^(NSString *errorMessage, NSError *error) { NSLog(@'++++ Video Export ErrorMessage ++++ is %@' , errorMessage); dispatch_group_leave(group); /// 信號(hào)量+1 向下運(yùn)行 dispatch_semaphore_signal(semaphore); }]; }else{ /// 圖片 [[TZImageManager manager] getOriginalPhotoDataWithAsset:asset completion:^(NSData *data, NSDictionary *info, BOOL isDegraded) { NSString* relativePath = [CMHFile writePictureFileToDisk:data]; if (MHStringIsNotEmpty(relativePath)) { CMHFile *file = [[CMHFile alloc] init]; file.thumbImage = thumbImage; file.localIdentifier = localIdentifier; /// 設(shè)置文件類型 file.fileType = CMHFileTypePicture; file.filePath = relativePath; [files addObject:file]; /// 緩存路徑 [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier]; } dispatch_group_leave(group); /// 信號(hào)量+1 向下運(yùn)行 dispatch_semaphore_signal(semaphore); }]; } /// 等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } } }); } /// 所有任務(wù)完成 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@'Compress Source Complete After %@ !!!!' , [NSDate date]); /// [MBProgressHUD mh_hideHUDForView:self.view]; /// 這里是所有任務(wù)完成 self.source.files = files.copy; [self.tableView reloadData]; });} 后臺(tái)接口
這里分享一下筆者在實(shí)際項(xiàng)目中用到的后臺(tái)提供斷點(diǎn)續(xù)傳的接口,因?yàn)轫?xiàng)目中部分邏輯處理是根據(jù)后臺(tái)提供的數(shù)據(jù)來(lái)的。這里筆者簡(jiǎn)單分析一下各個(gè)接口的使用場(chǎng)景。
預(yù)加載獲取文件ID(/fileSection/preLoad.do)
使用場(chǎng)景:根據(jù)當(dāng)次上傳的文件數(shù)量,預(yù)先分配好文件ID,APP終端需要做好保存與文件的對(duì)應(yīng)關(guān)系,在續(xù)傳文件時(shí)候作為參數(shù)傳遞。
請(qǐng)求URL: http://uadmin.xxxx.cn/fileSection/preLoad.do (POST)
Preload.png
斷點(diǎn)續(xù)傳文件(/fileSection/upload.do)
使用場(chǎng)景:大文件分片并行上傳。
請(qǐng)求URL: http://uadmin.xxxx.cn/fileSection/upload.do (POST)
Upload.png
刪除文件(/fileSection/delete.do)
使用場(chǎng)景:在App手動(dòng)刪除草稿時(shí)同時(shí)刪除已上傳到服務(wù)器的文件。
請(qǐng)求URL: http://uadmin.xxxx.cn/fileSection/delete.do (POST)
Delete.png
檢查文件是否上傳完畢(/fileSection/isFinish.do)
使用場(chǎng)景:APP中該上傳資源的所有的文件片都上傳到服務(wù)器,服務(wù)器需要檢查這些文件片的合成情況。如果服務(wù)器合成失敗,即finishStatus = 0,服務(wù)器會(huì)把那些合成失敗的文件返回給APP,即failFileIds。APP需要根據(jù)failFileIds去回滾本地?cái)?shù)據(jù)庫(kù),然后繼續(xù)重傳失敗的文件片。
請(qǐng)求URL: http://uadmin.xxxx.cn/fileSection/isFinish.do (POST)
finish.png
文件分片文件分片的過(guò)程主要是在用戶點(diǎn)擊提交資源的過(guò)程。具體內(nèi)容和細(xì)節(jié)還請(qǐng)查看CMHSource.h/m 、CMHFile.h/m、CMHFileSource.h/m、CMHFileBlock.h/m、CMHFileFragment.h/m的實(shí)現(xiàn)。
首先,這里需要將新建資源CMHSource 轉(zhuǎn)成上傳資源CMHFileSource,以及將新建資源的文件列表NSArray *files轉(zhuǎn)成上傳資源的文件塊列表NSArray *fileBlocks。
其次,需要根據(jù)新建資源的文件列表NSArray *files的個(gè)數(shù),即files.count,去調(diào)用后臺(tái)提供的預(yù)加載獲取文件ID(/fileSection/preLoad.do)接口,去獲取文件ID列表,從而為文件列表NSArray *files中每一個(gè)文件(CMHFile)綁定文件ID,然后將CMHFile列表轉(zhuǎn)成CMHFileBlock列表,以及將新建資源CMHSource 轉(zhuǎn)成上傳資源CMHFileSource。 關(guān)鍵代碼如下:
- (void)commitSource:(void (^)(BOOL))complete{/// 1. 通過(guò)要上傳的文件個(gè)數(shù) 去服務(wù)器獲取對(duì)應(yīng)的文件IDNSInteger uploadFileCount = self.files.count;/// 2. 以下通過(guò)真實(shí)的網(wǎng)絡(luò)請(qǐng)求去模擬獲取 文件ID的場(chǎng)景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1/// 類似于實(shí)際開(kāi)發(fā)中調(diào)用服務(wù)器的API: /fileSection/preLoad.do/// 1. 配置參數(shù)CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript];subscript[@'isEnglish'] = @0;subscript[@'devicetype'] = @2;subscript[@'version'] = @'1.0.1';/// 2. 配置參數(shù)模型CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary];/// 3. 發(fā)起請(qǐng)求[[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id responseObject) { /// - 如果到這里了就認(rèn)為獲取文件ID成功,這里模擬后臺(tái)返回的數(shù)據(jù) 有幾個(gè)上傳文件 就對(duì)應(yīng)幾個(gè)上傳文件ID NSMutableArray *fileIds = [NSMutableArray arrayWithCapacity:uploadFileCount]; for (NSInteger i = 0; i < uploadFileCount; i++) {NSString *fileId = [self _cmh_fileKey];[fileIds addObject:fileId]; } /// - 為每個(gè)上傳文件綁定服務(wù)器返回的文件ID,獲取要上傳的文件塊列表 /// 將服務(wù)器文件ID列表轉(zhuǎn)換為,轉(zhuǎn)成json字符串,后期需要存數(shù)據(jù)庫(kù),這個(gè)fileIdsStr很重要 NSString *fileIdsStr = fileIds.yy_modelToJSONString; /// 要上傳的文件塊列表 NSMutableArray *fileBlocks = [NSMutableArray arrayWithCapacity:uploadFileCount]; /// 生成上傳文件以及綁定文件ID for (NSInteger i = 0; i < uploadFileCount; i++) {CMHFile *file = self.files[i];NSString *fileId = fileIds[i];/// 資源中的文件綁定文件IDfile.fileId = fileId;/// 文件塊CMHFileBlock *fileBlcok = [[CMHFileBlock alloc] initFileBlcokAtPath:file.filePath fileId:fileId sourceId:self.sourceId];[fileBlocks addObject:fileBlcok]; } /// 生成上傳文件資源 CMHFileSource *fileSource = [[CMHFileSource alloc] init]; fileSource.sourceId = self.sourceId; fileSource.fileIds = fileIdsStr; fileSource.fileBlocks = fileBlocks.copy; /// 保存文件和資源 /// 非手動(dòng)存草稿 self.manualSaveDraft = NO; /// CoderMikeHe Fixed Bug : 這里必須記錄必須強(qiáng)引用上傳資源 self.fileSource = fileSource; /// 先保存資源 @weakify(self); [self saveSourceToDB:^(BOOL isSuccess) {if (!isSuccess) { !complete ? : complete(isSuccess); [MBProgressHUD mh_showTips:@'保存資源失敗!!!']; return ;}@strongify(self);/// CoderMikeHe Fixed Bug : 這里必須用self.fileSource 而不是 fileSource ,因?yàn)檫@是異步,會(huì)導(dǎo)致 fileSource == nil;/// 保存上傳資源@weakify(self);[self.fileSource saveFileSourceToDB:^(BOOL rst) { !complete ? : complete(rst); @strongify(self); /// 這里需要開(kāi)始上傳 if (rst) {[[CMHFileUploadManager sharedManager] uploadSource:self.sourceId]; }else{[MBProgressHUD mh_showTips:@'保存上傳資源失敗!!!']; }}]; }];} failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) { /// 回調(diào)錯(cuò)誤 !complete ? : complete(NO); /// show error [MBProgressHUD mh_showErrorTips:error];}];}
然后,我們需要將文件塊CMHFileBlock按照512k的大小切割成多個(gè)文件片CMHFileFragment,這里的代碼實(shí)現(xiàn)和屬性生成都是參照這篇文章 HTTP斷點(diǎn)續(xù)傳與斷點(diǎn)上傳之 -- 文件流操作 來(lái)實(shí)現(xiàn)的。關(guān)鍵代碼如下:
// 切分文件片段- (void)_cutFileForFragments { NSUInteger offset = CMHFileFragmentMaxSize; // 總片數(shù) NSUInteger totalFileFragment = (self.totalFileSize%offset==0)?(self.totalFileSize/offset):(self.totalFileSize/(offset) + 1); self.totalFileFragment = totalFileFragment; NSMutableArray<CMHFileFragment *> *fragments = [[NSMutableArray alloc] initWithCapacity:0]; for (NSUInteger i = 0; i < totalFileFragment; i ++) { CMHFileFragment *fFragment = [[CMHFileFragment alloc] init]; fFragment.fragmentIndex = i; fFragment.uploadStatus = CMHFileUploadStatusWaiting; fFragment.fragmentOffset = i * offset; if (i != totalFileFragment - 1) { fFragment.fragmentSize = offset; } else { fFragment.fragmentSize = self.totalFileSize - fFragment.fragmentOffset; } /// 關(guān)聯(lián)屬性 fFragment.fileId = self.fileId; fFragment.sourceId = self.sourceId; fFragment.filePath = self.filePath; fFragment.totalFileFragment = self.totalFileFragment ; fFragment.totalFileSize = self.totalFileSize; fFragment.fileType = self.fileType; fFragment.fileName = [NSString stringWithFormat:@'%@-%ld.%@',self.fileId , (long)i , self.fileName.pathExtension]; [fragments addObject:fFragment]; } self.fileFragments = fragments.copy;}
最后,我們知道一份上傳資源由多個(gè)文件塊組成,而一個(gè)文件塊由多個(gè)文件片組成。所以我們是不是可以這樣理解:一份上傳資源由多個(gè)文件片組成。前提是要保證每一個(gè)文件片,必須含有兩個(gè)屬性sourceId和fileId。
sourceId : 代表這個(gè)文件片所屬于哪個(gè)資源。
fileId : 代表這個(gè)文件片所屬于哪個(gè)文件塊。
一份上傳資源由多個(gè)文件片組成的代碼實(shí)現(xiàn),無(wú)非就是重寫(xiě)CMHFileSource的setFileBlocks即可。關(guān)鍵代碼如下:
- (void)setFileBlocks:(NSArray<CMHFileBlock *> *)fileBlocks{ _fileBlocks = fileBlocks.copy; NSMutableArray *fileFragments = [NSMutableArray array]; for (CMHFileBlock *fileBlock in fileBlocks) { [fileFragments addObjectsFromArray:fileBlock.fileFragments]; self.totalFileFragment = self.totalFileFragment + fileBlock.totalFileFragment; self.totalFileSize = self.totalFileSize + fileBlock.totalFileSize; } self.fileFragments = fileFragments.copy;}
當(dāng)然,我們需要將CMHSource、CMHFileSource、CMHFileFragment保存到數(shù)據(jù)庫(kù)即可。
分片上傳分片上傳是本Demo中一個(gè)比較重要的功能點(diǎn),但其實(shí)功能點(diǎn)并不難,主要復(fù)雜的還是業(yè)務(wù)邏輯以及數(shù)據(jù)庫(kù)處理。分片上傳,其原理還是文件上傳,某個(gè)文件片的上傳和我們平時(shí)上傳頭像的邏輯一模一樣,不同點(diǎn)無(wú)非就是我們需要利用數(shù)據(jù)庫(kù)去記錄每一片的上傳狀態(tài)罷了。詳情請(qǐng)參考:CMHFileUploadManager.h/m
這里筆者以CMHFileUploadManager上傳某個(gè)資源為例,具體講講其中的邏輯以及細(xì)節(jié)處理。具體的代碼實(shí)現(xiàn)請(qǐng)參考:- (void)uploadSource:(NSString *)sourceId;的實(shí)現(xiàn)。注 意:筆者提供的Demo,一次只能上傳一個(gè)資源。 關(guān)于具體的業(yè)務(wù)邏輯分析,筆者已經(jīng)寫(xiě)在寫(xiě)在代碼注釋里面了,這里就不再贅述,還請(qǐng)結(jié)合代碼注釋去理解具體的業(yè)務(wù)邏輯和場(chǎng)景。關(guān)鍵代碼如下:
/// 上傳資源 <核心方法>- (void)uploadSource:(NSString *)sourceId{ if (!MHStringIsNotEmpty(sourceId)) { return; } /// CoderMikeHe Fixed Bug : 解決初次加載的問(wèn)題,不需要驗(yàn)證網(wǎng)絡(luò) if (self.isLoaded) {if (![AFNetworkReachabilityManager sharedManager].isReachable) { /// 沒(méi)有網(wǎng)絡(luò) [self postFileUploadStatusDidChangedNotification:sourceId]; return;} } self.loaded = YES; /// - 獲取該資源下所有未上傳完成的文件片 NSArray *uploadFileFragments = [CMHFileFragment fetchAllWaitingForUploadFileFragment:sourceId]; if (uploadFileFragments.count == 0) {/// 沒(méi)有要上傳的文件片/// 獲取上傳資源CMHFileSource *fileSource = [CMHFileSource fetchFileSource:sourceId];/// 獲取資源CMHSource *source = [CMHSource fetchSource:sourceId];if (MHObjectIsNil(source)) { /// 提交下一個(gè)資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒(méi)有資源,則何須上傳資源,將數(shù)據(jù)庫(kù)里面清掉 [CMHFileSource removeFileSourceFromDB:sourceId complete:NULL]; /// 通知草稿頁(yè) 刪除詞條數(shù)據(jù) [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}]; return;}if (MHObjectIsNil(fileSource)) { /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒(méi)有上傳資源 ,則直接提交 [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; [self _commitSource:sourceId]; return;}if (fileSource.totalFileFragment <= 0) { /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒(méi)有上傳文件片 [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; [self _commitSource:sourceId]; return;}/// 倒了這里 , 證明 fileSource,source 有值,且 fileSource.totalFileFragment > 0CMHFileUploadStatus uploadStatus = [CMHFileSource fetchFileUploadStatus:sourceId];if (uploadStatus == CMHFileUploadStatusFinished) { // 文件全部上傳成 dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25/*延遲執(zhí)行時(shí)間*/ * NSEC_PER_SEC)); dispatch_after(delayTime, dispatch_get_main_queue(), ^{/// 檢查服務(wù)器的文件上傳合成狀態(tài)[self _checkFileFragmentSynthetiseStatusFromService:sourceId]; });}else{ /// 到了這里,則證明這個(gè)草稿永遠(yuǎn)都不會(huì)上傳成功了,這里很遺憾則需要將其從數(shù)據(jù)庫(kù)中移除 /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; [CMHSource removeSourceFromDB:sourceId complete:NULL]; /// 通知草稿頁(yè) 刪除這條數(shù)據(jù) [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}];}return; } /// 0. 這里一定會(huì)新建一個(gè)新的上傳隊(duì)列,一定會(huì)開(kāi)啟一個(gè)新的任務(wù) /// - 看是否存在于上傳數(shù)組中 NSString *findSid = nil; /// - 是否有文件正在上傳 BOOL isUploading = NO; for (NSString *sid in self.uploadFileArray) {/// 上傳資源里面已經(jīng)存在了,findSidif ([sid isEqualToString:sourceId]) { findSid = sid;}/// 查看當(dāng)前是否有上傳任務(wù)正在上傳CMHFileUploadQueue *queue = [self.uploadFileQueueDict objectForKey:sid];if (queue && !queue.isSuspended) { isUploading = YES;} } /// 2. 檢查狀態(tài),插入數(shù)據(jù), if (findSid) { /// 已經(jīng)存在了,那就先刪除,后插入到第0個(gè)元素[self.uploadFileArray removeObject:findSid];[self.uploadFileArray insertObject:sourceId atIndex:0]; }else{ /// 不存在上傳資源數(shù)組中,直接插入到第0個(gè)元素[self.uploadFileArray insertObject:sourceId atIndex:0]; } /// 3. 檢查是否已經(jīng)有上傳任務(wù)了 if (isUploading) { /// 已經(jīng)有正在上傳任務(wù)了,則不需要開(kāi)啟隊(duì)列了,就請(qǐng)繼續(xù)等待/// 發(fā)送通知[self postFileUploadStatusDidChangedNotification:sourceId];return; } /// 4. 如果沒(méi)有上傳任務(wù),你就創(chuàng)建隊(duì)里開(kāi)啟任務(wù)即可 /// 更新這個(gè)上傳文件的狀態(tài) 為 `正在上傳的狀態(tài)` [self updateUpLoadStatus:CMHFileUploadStatusUploading sourceId:sourceId]; /// 創(chuàng)建信號(hào)量 用于線程同步 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); /// 創(chuàng)建一個(gè)隊(duì)列組 dispatch_group_t group = dispatch_group_create(); /// 操作數(shù) NSMutableArray *operations = [NSMutableArray array]; /// 這里采用串行隊(duì)列且串行請(qǐng)求的方式處理每一片的上傳 for (CMHFileFragment *ff in uploadFileFragments) {/// 進(jìn)組dispatch_group_enter(group);// 創(chuàng)建對(duì)象,封裝操作NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ /// 切記:任務(wù)(網(wǎng)絡(luò)請(qǐng)求)是串行執(zhí)行的 ,但網(wǎng)絡(luò)請(qǐng)求結(jié)果回調(diào)是異步的、 [self _uploadFileFragment:ff progress:^(NSProgress *progress) { NSLog(@' n上傳文件ID【%@】n上傳文件片 【%ld】n上傳進(jìn)度為【%@】',ff.fileId, (long)ff.fragmentIndex, progress.localizedDescription); } success:^(id responseObject) { /// 處理成功的文件片 [self _handleUploadFileFragment:ff]; /// 退組 dispatch_group_leave(group); /// 信號(hào)量+1 向下運(yùn)行 dispatch_semaphore_signal(semaphore); } failure:^(NSError *error) { /// 更新數(shù)據(jù) /// 某片上傳失敗 [ff updateFileFragmentUploadStatus:CMHFileUploadStatusWaiting]; /// 退組 dispatch_group_leave(group); /// 信號(hào)量+1 向下運(yùn)行 dispatch_semaphore_signal(semaphore); }]; /// 等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);}];/// 添加操作數(shù)組[operations addObject:operation]; } /// 創(chuàng)建NSOperationQueue CMHFileUploadQueue * uploadFileQueue = [[CMHFileUploadQueue alloc] init]; /// 存起來(lái) [self.uploadFileQueueDict setObject:uploadFileQueue forKey:sourceId]; /// 把操作添加到隊(duì)列中 不需要設(shè)置為等待 [uploadFileQueue addOperations:operations waitUntilFinished:NO]; /// 隊(duì)列組的操作全部完成 dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@'+++dispatch_group_notify+++');/// 0. 如果運(yùn)行到這,證明此`Queue`里面的所有操作都已經(jīng)全部完成了,你如果再使用 [queue setSuspended:YES/NO];將沒(méi)有任何意義,所以你必須將其移除掉[self.uploadFileQueueDict removeObjectForKey:sourceId];/// 1. 隊(duì)列完畢了,清除掉當(dāng)前的資源,開(kāi)啟下一個(gè)資源[self _removeSourceFromUploadFileArray:sourceId];/// CoderMikeHe: 這里先不更新草稿頁(yè)的狀態(tài),等提交完表格再去發(fā)送通知/// 檢查一下資源上傳[self _uploadSourceEnd:sourceId]; }); //// 告知外界其資源狀態(tài)改過(guò)了 [self postFileUploadStatusDidChangedNotification:sourceId];}
這里對(duì)上傳資源下的需要上傳的文件片做了循環(huán)的上傳,由于網(wǎng)絡(luò)請(qǐng)求是一個(gè)異步的操作,同時(shí)也考慮到太多并發(fā)(當(dāng)然系統(tǒng)對(duì)于網(wǎng)絡(luò)請(qǐng)求開(kāi)辟的線程個(gè)數(shù)也有限制)對(duì)于手機(jī)性能的影響,因此利用GCD信號(hào)量等待這種功能特性讓一個(gè)片段上傳完之后再進(jìn)行下一個(gè)片段的上傳。
文件上傳核心代碼如下:
/// 上傳某一片文件 這里用作測(cè)試- (void)_uploadFileFragment:(CMHFileFragment *)fileFragment progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress success:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure{ /// 獲取上傳參數(shù) NSDictionary *parameters = [fileFragment fetchUploadParamsInfo]; /// 獲取上傳數(shù)據(jù) NSData *fileData = [fileFragment fetchFileFragmentData]; /// 資源文件找不到,則直接修改數(shù)據(jù)庫(kù),無(wú)論如何也得讓用戶把資源提交上去,而不是讓其永遠(yuǎn)卡在草稿頁(yè)里,這樣太影響用戶體驗(yàn)了 if (fileData == nil) {/// CoderMikeHe Fixed Bug : V1.6.7之前 修復(fù)文件丟失的情況/// 1. 獲取該片所處的資源CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:fileFragment.sourceId];/// 取出fileIDNSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];NSLog(@' Before -- 文件<%@>未找到個(gè)數(shù) %ld <%@> ' ,fileFragment.fileId , fileIds.count, fileIds);if ([fileIds containsObject:fileFragment.fileId]) { /// 數(shù)據(jù)庫(kù)包含 [fileIds removeObject:fileFragment.fileId]; uploadSource.fileIds = fileIds.yy_modelToJSONString; /// 更新數(shù)據(jù)庫(kù) [uploadSource saveOrUpdate];}NSLog(@' After -- 文件<%@>未找到個(gè)數(shù) %ld <%@> ' ,fileFragment.fileId , fileIds.count, fileIds);/// 一定要回調(diào)為成功,讓用戶誤以為正在上傳,而不是直接卡死在草稿頁(yè)NSDictionary *responseObj = @{@'code' : @200};!success ? : success(responseObj);return; } /// 這里筆者只是模擬一下網(wǎng)絡(luò)情況哈,不要在乎這些細(xì)節(jié) , /// 類似于實(shí)際開(kāi)發(fā)中調(diào)用服務(wù)器的API: /fileSection/upload.do /// 2. 以下通過(guò)真實(shí)的網(wǎng)絡(luò)請(qǐng)求去模擬獲取 文件ID的場(chǎng)景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1 /// 1. 配置參數(shù) CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript]; subscript[@'isEnglish'] = @0; subscript[@'devicetype'] = @2; subscript[@'version'] = @'1.0.1'; /// 2. 配置參數(shù)模型 CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary]; /// 3. 發(fā)起請(qǐng)求 [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id _Nullable responseObject) {#warning CMH TODO 稍微延遲一下,模擬現(xiàn)實(shí)情況下的上傳進(jìn)度NSInteger randomNum = [NSObject mh_randomNumber:0 to:5];[NSThread sleepForTimeInterval:0.1 * randomNum];!success ? : success(responseObject); } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {!failure ? : failure(error); }];#if 0 /// 這個(gè)是真實(shí)上傳,請(qǐng)根據(jù)自身實(shí)際項(xiàng)目出發(fā) /fileSection/upload.do [self _uploadFileFragmentWithParameters:parameters fileType:fileFragment.fileType fileData:fileData fileName:fileFragment.fileName progress:uploadProgress success:success failure:failure];#endif}/// 實(shí)際開(kāi)發(fā)項(xiàng)目中上傳每一片文件,這里請(qǐng)結(jié)合自身項(xiàng)目開(kāi)發(fā)去設(shè)計(jì)- (NSURLSessionDataTask *)_uploadFileFragmentWithParameters:(NSDictionary *)parameters fileType:(CMHFileType)fileType fileData:(NSData *)fileData fileName:(NSString *)fileName progress:(void (^)(NSProgress *))uploadProgresssuccess:(void (^)(id responseObject))successfailure:(void (^)(NSError *error))failure{ /// 配置成服務(wù)器想要的樣式 NSMutableArray *paramsArray = [NSMutableArray array]; [paramsArray addObject:parameters]; /// 生成jsonString NSString *jsonStr = [paramsArray yy_modelToJSONString]; /// 設(shè)置TTPHeaderField [self.uploadService.requestSerializer setValue:jsonStr forHTTPHeaderField:@'file_block']; /// 開(kāi)啟文件任務(wù)上傳 /// PS : 著了完全可以看成,我們平常上傳頭像給服務(wù)器一樣的處理方式 NSURLSessionDataTask *uploadTask = [self.uploadService POST:@'/fileSection/upload.do' parameters:nil/** 一般這里傳的是基本參數(shù) */ constructingBodyWithBlock:^(id _Nonnull formData) { /// 拼接mimeType NSString *mimeType = [ NSString stringWithFormat: @'%@/%@',(fileType ==CMHFileTypePicture) ?@'image': @'video',[[fileName componentsSeparatedByString: @'.'] lastObject]]; /// 拼接數(shù)據(jù) [formData appendPartWithFileData:fileData name: @'sectionFile' fileName:fileName mimeType:mimeType]; } progress:^( NSProgress * progress) { !uploadProgress ? : uploadProgress(progress); } success:^( NSURLSessionDataTask * _Nonnull task,id _Nullable responseObject) { !success ? : success(responseObject); } failure:^( NSURLSessionDataTask * _Nullable task,NSError * _Nonnull error) { !failure ? : failure(error); }]; return uploadTask; }
檢查服務(wù)器文件上傳合成情況的核心代碼如下:
/// 檢查服務(wù)器文件片合成情況- (void)_checkFileFragmentSynthetiseStatusFromService:(NSString *)sourceId{ /// 這里調(diào)用服務(wù)器的接口檢查文件上傳狀態(tài),以這個(gè)為標(biāo)準(zhǔn) CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:sourceId]; /// 沒(méi)意義 if (uploadSource == nil) { return; } /// 如果這里進(jìn)來(lái)了,則證明準(zhǔn)備驗(yàn)證文件片和提交表單,則草稿里面的這塊表單,你不能在讓用戶去點(diǎn)擊了 [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; /// V1.6.5之前的接口老數(shù)據(jù) if (!MHStringIsNotEmpty(uploadSource.fileIds)) {/// 這里可能是老數(shù)據(jù),直接認(rèn)為成功,就不要去跟服務(wù)器打交道了/// 成功[self _commitSource:sourceId];/// 上傳下一個(gè)[self _autoUploadSource:sourceId reUpload:NO];return; } /// 這里筆者只是模擬一下網(wǎng)絡(luò)情況哈,不要在乎這些細(xì)節(jié), /// 類似于實(shí)際開(kāi)發(fā)中調(diào)用服務(wù)器的API: /fileSection/isFinish.do /// 2. 以下通過(guò)真實(shí)的網(wǎng)絡(luò)請(qǐng)求去模擬獲取 文件ID的場(chǎng)景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1 /// 1. 配置參數(shù) CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript]; subscript[@'isEnglish'] = @0; subscript[@'devicetype'] = @2; subscript[@'version'] = @'1.0.1'; /// 2. 配置參數(shù)模型 CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary]; /// 3. 發(fā)起請(qǐng)求 [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id _Nullable responseObject) {/// 模擬后臺(tái)返回的合成結(jié)果CMHFileSynthetise *fs = [[CMHFileSynthetise alloc] init];NSInteger randomNum = [NSObject mh_randomNumber:0 to:20];fs.finishStatus = (randomNum > 0) ? 1 : 0; /// 模擬服務(wù)器合成失敗的場(chǎng)景,畢竟合成失敗的幾率很低if (fs.finishStatus>0) { /// 服務(wù)器合成資源文件成功 /// 成功 [self _commitSource:sourceId]; /// 上傳下一個(gè) [self _autoUploadSource:sourceId reUpload:NO]; return ;}/// 服務(wù)器合成資源文件失敗, 服務(wù)器會(huì)把合成失敗的 fileId 返回出來(lái)/// 也就是 'failFileIds' : 'fileId0,fileId1,...'的格式返回出來(lái)/// 這里模擬后臺(tái)返回合成錯(cuò)誤的文件ID, 這里只是演習(xí)!!這里只是演習(xí)!!/// 取出fileIDNSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];/// 模擬只有一個(gè)文件ID合成失敗NSString *failFileIds = fileIds.firstObject;fs.failFileIds = failFileIds;/// 這里才是模擬真實(shí)的網(wǎng)絡(luò)情況if (MHStringIsNotEmpty(fs.failFileIds)) { /// 1. 回滾數(shù)據(jù) [uploadSource rollbackFailureFile:fs.failureFileIds]; /// 2. 獲取進(jìn)度 CGFloat progress = [CMHFileSource fetchUploadProgress:sourceId]; /// 3. 發(fā)送通知 [MHNotificationCenter postNotificationName:CMHFileUploadProgressDidChangedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId , CMHFileUploadProgressDidChangedKey : @(progress)}]; /// 4. 重新設(shè)置回滾數(shù)據(jù)的經(jīng)度 [CMHSource updateSourceProgress:progress sourceId:sourceId];}else{ /// 無(wú)需回滾,修改狀態(tài)即可 [self postFileUploadStatusDidChangedNotification:sourceId];}/// 合成失敗,繼續(xù)重傳失敗的片,允許用戶點(diǎn)擊草稿頁(yè)的資源[self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO];/// 重傳該資源[self _autoUploadSource:sourceId reUpload:YES]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {/// 1. 服務(wù)器報(bào)錯(cuò)不重傳[MBProgressHUD mh_showErrorTips:error];/// 更新資源狀態(tài)[self updateUpLoadStatus:CMHFileUploadStatusWaiting sourceId:sourceId];/// 更新?tīng)顟B(tài)[self postFileUploadStatusDidChangedNotification:sourceId];/// 文件片合成失敗,允許點(diǎn)擊[self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO]; }];}
總之,文件分片上傳邏輯不止上面這一點(diǎn)點(diǎn)內(nèi)容,還有存在許多邏輯處理和細(xì)節(jié)注意,比如暫停上傳資源;開(kāi)始上傳資源;取消上傳資源;取消所有上傳資源;服務(wù)器合成某些文件失敗,客戶端回滾數(shù)據(jù)庫(kù),重傳失敗的文件片;某個(gè)資源上傳后自動(dòng)重傳下個(gè)資源....等等。大家有興趣可以查看CMHFileUploadManager.h提供的API的具體實(shí)現(xiàn)。 CMHFileUploadManager.h的所有內(nèi)容如下:
/// 某資源的所有片數(shù)據(jù)上傳,完成也就是提交資源到服務(wù)器成功。FOUNDATION_EXTERN NSString *const CMHFileUploadDidFinishedNotification;/// 資源文件上傳狀態(tài)改變的通知FOUNDATION_EXTERN NSString *const CMHFileUploadStatusDidChangedNotification;/// 草稿上傳文件狀態(tài) disable 是否不能點(diǎn)擊 如果為YES 不要修改草稿頁(yè)表單的上傳狀態(tài) 主需要讓用戶不允許點(diǎn)擊上傳按鈕FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusKey;FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusNotification;/// 某資源中的某片數(shù)據(jù)上傳完成FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedNotification;/// 某資源的idFOUNDATION_EXTERN NSString *const CMHFileUploadSourceIdKey;/// 某資源的進(jìn)度FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedKey;@interface CMHFileUploadManager : NSObject/// 存放操作隊(duì)列的字典@property (nonatomic , readonly , strong) NSMutableDictionary *uploadFileQueueDict;/// 聲明單例+ (instancetype)sharedManager;/// 銷(xiāo)毀單例+ (void)deallocManager;/// 基礎(chǔ)配置,主要是后臺(tái)上傳草稿數(shù)據(jù) 一般這個(gè)方法會(huì)放在 程序啟動(dòng)后切換到主頁(yè)時(shí)調(diào)用- (void)configure;/// 上傳資源/// sourceId:文件組Id- (void)uploadSource:(NSString *)sourceId;/// 暫停上傳 -- 用戶操作/// sourceId: 資源Id- (void)suspendUpload:(NSString *)sourceId;/// 繼續(xù)上傳 -- 用戶操作/// sourceId: 資源Id- (void)resumeUpload:(NSString *)sourceId;/// 取消掉上傳 -- 用戶操作/// sourceId: 資源Id- (void)cancelUpload:(NSString *)sourceId;/// 取消掉所有上傳 一般這個(gè)方法會(huì)放在 程序啟動(dòng)后切換到登錄頁(yè)時(shí)調(diào)用- (void)cancelAllUpload;/// 刪除當(dāng)前用戶無(wú)效的資源- (void)clearInvalidDiskCache;//// 以下方法跟服務(wù)器交互,只管調(diào)用即可,無(wú)需回調(diào),/// 清除掉已經(jīng)上傳到服務(wù)器的文件片 fileSection- (void)deleteUploadedFile:(NSString *)sourceId;/// 告知草稿頁(yè),某個(gè)資源的上傳狀態(tài)改變/// sourceId -- 資源ID- (void)postFileUploadStatusDidChangedNotification:(NSString *)sourceId;/// 告知草稿頁(yè),某個(gè)資源不允許點(diǎn)擊- (void)postFileUploadDisableStatusNotification:(NSString *)sourceId fileUploadDisabled:(BOOL)fileUploadDisabled;/// 更新資源的狀態(tài)/// uploadStatus -- 上傳狀態(tài)/// sourceId -- 資源ID- (void)updateUpLoadStatus:(CMHFileUploadStatus)uploadStatus sourceId:(NSString *)sourceId;@end 總結(jié)
以上內(nèi)容,就是筆者在做大文件分片上傳的過(guò)程中的心得體會(huì)。看似簡(jiǎn)單的文件分片上傳功能,但其中涵蓋的知識(shí)面還是比較廣的,結(jié)合筆者前面談及的必備知識(shí)點(diǎn),大家業(yè)余時(shí)間可以系統(tǒng)去學(xué)習(xí)和掌握,最后筆者還是建議大家把多線程的相關(guān)知識(shí)惡補(bǔ)一下和實(shí)踐起來(lái)。當(dāng)然這其中肯定還有一些細(xì)小的邏輯和細(xì)節(jié)問(wèn)題還未暴露出來(lái),如果大家在使用和查看過(guò)程中發(fā)現(xiàn)問(wèn)題或者不理解的地方,以及如果有好的建議或意見(jiàn)都可以指出。
期待文章若對(duì)您有點(diǎn)幫助,請(qǐng)給個(gè)喜歡:heart:,畢竟碼字不易;若對(duì)您沒(méi)啥幫助,請(qǐng)給點(diǎn)建議,切記學(xué)無(wú)止境。
針對(duì)文章所述內(nèi)容,閱讀期間任何疑問(wèn);請(qǐng)?jiān)谖恼碌撞颗u(píng)指正,我會(huì)火速解決和修正問(wèn)題。
GitHub地址: https://github.com/CoderMikeHe
源碼地址:
MHDevelopExample目錄中的Architecture/Contacts/FileUpload文件夾中 <特別強(qiáng)調(diào) 0='' cmhdebug=''>來(lái)自:http://www.cocoachina.com/ios/20180822/24662.html
相關(guān)文章:
1. PHP正則表達(dá)式函數(shù)preg_replace用法實(shí)例分析2. 一個(gè) 2 年 Android 開(kāi)發(fā)者的 18 條忠告3. vue使用moment如何將時(shí)間戳轉(zhuǎn)為標(biāo)準(zhǔn)日期時(shí)間格式4. js select支持手動(dòng)輸入功能實(shí)現(xiàn)代碼5. Android 實(shí)現(xiàn)徹底退出自己APP 并殺掉所有相關(guān)的進(jìn)程6. Android studio 解決logcat無(wú)過(guò)濾工具欄的操作7. 什么是Python變量作用域8. vue-drag-chart 拖動(dòng)/縮放圖表組件的實(shí)例代碼9. Spring的異常重試框架Spring Retry簡(jiǎn)單配置操作10. Vue實(shí)現(xiàn)仿iPhone懸浮球的示例代碼
