iOS 利用AFNetworking實現大檔案分片上傳
概述
一說到 檔案上傳
,想必大家都並不陌生,更何況是利用 ofollow,noindex">AFNetworking (PS:後期統稱AF)來做,那更是小菜一碟。比如開發中常見的場景: 頭像上傳
, 九宮格圖片上傳
...等等,這些場景無一不使用到檔案上傳的功能。如果利用 AF
來實現,無非就是客戶端呼叫 AF
提供的檔案上傳介面即可, API
如下所示:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(nullable id)parameters constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
上面這種場景,主要是針對一些小資原始檔的上傳,上傳過程耗時較短,使用者可以接受。但是一旦資原始檔過大(比如1G以上),則必須要考慮上傳過程網路中斷的情況。試想我們還是採用上述方案,一口氣把這整個1G的資原始檔上傳到伺服器,這顯然是不現實的,就算伺服器答應,使用者也不答應的。考慮到網路使用中斷或伺服器上傳異常...等場景,那麼我們恢復網路後又得重新從頭開始上傳,那之前已經上傳完成的部分資源豈不作廢,這種耗時耗力的工作,顯然是不符合常理的。為了解決大檔案上傳的存在如此雞肋的問題,從而誕生了一個叫: 分片上傳(斷點續上傳)
分片上傳(斷點續上傳)主要是為了保證在網路中斷後1G的資原始檔已上傳的那部分在下次網路連線時不必再重傳。所以我們本地在上傳的時候,要將大檔案進行切割分片,比如分成 1024*1024B
,即將大檔案分成 1M
的片進行上傳,伺服器在接收後,再將這些片合併成原始檔案,這就是 分片 的基本原理。斷點續傳要求本地要記錄每一片的上傳的狀態,我通過三個狀態進行了標記 (waiting loading finish)
,當網路中斷,再次連線後,從斷點處進行上傳。伺服器通過檔名、總片數判斷該檔案是否已全部上傳完成。
弄懂了 分片上傳(斷點續上傳) 的基本原理,其核心就是 分片 ,然後將分割出來的的每一 片 ,按照類似上傳頭像的方式上傳到伺服器即可,全部上傳完後再在服務端將這些小資料片合併成為一個資源。
分片上傳引入了兩個概念: 塊(block) 和 片(fragment) 。每個塊由一到多個片組成,而一個資源則由一到多個塊組成。他們之間的關係可以用下圖表述:

檔案資源組成關係.png
本文筆者將著重分析 分片上傳 實現的具體過程以及細節處理,爭取把裡面的所有涵蓋的知識點以及細節處理分析透徹。希望為大家提供一點思路,少走一些彎路,填補一些細坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。
效果圖如下:

FileUpload.gif
知識點
雖然 分片上傳 的原理看似非常簡單,但是落實到具體的實現,其中還是具有非常多的細節分析和邏輯處理,而且都是我們開發中不常用到的知識點,這裡筆者就總結了一下 分片上傳 所用到的知識點和使用場景,以及藉助一些第三方框架,來達到 分片上傳 的目的。
-
圖片和視訊資源的獲取
所謂檔案上傳,前提必須得有檔案,而檔案一般是本地檔案,本地檔案的獲取來源一般是系統相簿獲取,關於如何從系統相簿中獲取圖片或視訊資源,這裡筆者採用 TZImagePickerController 一個支援多選、選原圖和視訊的圖片選擇器,同時有預覽、裁剪功能,支援iOS6+第三方框架。根據
TZImagePickerControllerDelegate
返回的資源(圖片、視訊)資料,然後利用TZImageMananger
提供的API,獲取到原始圖片和視訊資源。關鍵API如下:具體使用請參照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;
-
檔案讀寫和剪下檔案寫入一般用於從相簿中獲取到圖片的原圖
data
,然後將其寫入到指定的資料夾中,一般呼叫NSData
提供的方法。- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
檔案剪下一般用於從相簿中獲取到視訊資源,其視訊格式是
mov
格式的,需要我們視訊壓縮轉成mp4
格式,壓縮成功後一般將其匯入到APP沙盒檔案的tmp
目錄下,總所周知,tmp
裡面一般存放一些臨時檔案,所以需要將其匯入到Cache
資料夾中去,這裡用檔案移動(剪下)再好不過了,而且不需要讀取到記憶體中去。 直接呼叫NSFileManager
的提供的API即可:- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error
檔案讀取一般主要用於讀取每一個檔案
片
的大小,需要利用NSFileHandle
來處理,呼叫其如下API來完成。- (NSData *)readDataOfLength:(NSUInteger)length; - (void)seekToFileOffset:(unsigned long long)offset; + (nullable instancetype)fileHandleForReadingAtPath:(NSString *)path;
綜上所述:
NSData
,NSFileManager
,NSFileHandle
的API的常規使用得比較熟練。 -
視訊壓縮系統的錄製視訊匯出的格式是
mov
,所以一般的做法就是壓縮轉化成mp4
格式,這樣就得用到系統的視訊壓縮方法,大家可以自行百度AVAssetExportSession
的使用。這裡筆者採用TZImagePickerController
提供的API來做的,具體請參照TZImageManager
提供的方法,大家可以看看其實現。/// Export video 匯出視訊 presetName: 預設名字,預設值是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;
-
資源快取所謂資源快取,就是一般從系統相簿中獲取到的資源(圖片、視訊),我們會將資源另存到在
/Library/Caches/Upload
目錄下,然後把資源存放的相對路徑給快取起來,下次從系統相簿中選取相同的資源,如果已經存在於/Library/Caches/Upload
目錄下,則不需要去獲取原始圖片,或者壓縮視訊了。這裡筆者採用的是: YYCache" target="_blank" rel="nofollow,noindex">YYCache 來做記憶體快取和磁碟快取。具體使用,還請自行百度。 -
資料庫資料庫主要用於,儲存新建資源,儲存上傳資源,儲存檔案片...等等,利用資料庫的
增
,刪
,改
,查
等功能,更加方便快捷的處理檔案片的上傳狀態,上傳進度,獲取或刪除草稿資料...等等一些列的操作,大大提供了開發的效率。這裡筆者採用的是基於 FMDB 封裝的 BGFMDB 框架,BGFMDB
是對FMDB
面相物件層的封裝,且幾乎支援儲存iOS所有基本的自帶資料型別,讓資料的增
,刪
,改
,查
分別只需要一行程式碼即可。具體使用,還請檢視BGFMDB
提供的Demo。 -
多執行緒多執行緒的使用主要用在,① 從系統相簿獲取到資源(圖片、視訊),對資源進行處理(比如,獲取原圖,壓縮視訊等等); ② 檔案分片上傳。其實現實開發中,我們使用多執行緒的的場景並不多,但反觀使用多執行緒最多的場景就是-- 面試 。多執行緒其實是iOS中非常重要的知識點,但是由於平時疏於練習和使用,腦子裡面可能只有少許多執行緒的相關知識。此次筆者在專案中做
大檔案分片上傳
功能,也讓筆者重拾了多執行緒的相關知識,而且運用到實際開發中去,也是一個不小的收穫。這裡筆者就講講本模組中用到了哪些多執行緒的知識,當然具體的理論知識和實踐操作,大家可以參照下面筆者分享的網址去針對性的學習和實踐多執行緒的相關知識。具體如下:-
特別提醒:① 必須掌握
GCD 佇列組:dispatch_group
。合理使用dispatch_group_enter
、dispatch_group_leave
和dispatch_group_notify
的配套使用。② 必須掌握
GCD 訊號量:dispatch_semaphore
。熟練使用dispatch_semaphore_create
、dispatch_semaphore_signal
和dispatch_semaphore_wait
的配套使用,利用dispatch_semaphore
保持執行緒同步,將非同步執行任務轉換為同步執行任務以及保證執行緒安全,為執行緒加鎖。
-
模組
關於筆者在Demo中提供的 檔案分片上傳
的示例程式,雖然不夠華麗,但麻雀雖小,五臟俱全,大家湊合著看咯。但總的來說,可以簡單分為以下幾個模組:
-
資源新建:系統相簿獲取資原始檔(圖片、視訊);獲取原圖或視訊壓縮,並匯入到沙盒指定的資料夾;資源快取。
-
後臺介面:考慮到示例程式中部分業務邏輯是按照後臺提供的API設計的,所以有必要分享一下後臺提供了哪些API,以及具體的使用的場景。
-
檔案分片:將新建資源,轉化為上傳資源,將資源中存放的每一個檔案塊,按照
512k
的大小分成若干個檔案片。涉及到新建資源儲存資料庫,上傳資源儲存資料庫,以及每個檔案片儲存資料庫。 -
草稿儲存:草稿列表的資料來源主要分為
手動存草稿
和自動存草稿
。手動存草稿
一般是指使用者手動點選存草稿按鈕儲存草稿,此草稿資料可以進行二次編輯;自動存草稿
一般是指使用者點選提交按鈕上傳資原始檔,由於一時半會不會上傳到伺服器上去,所以需要報存草稿,此草稿資料可以顯示上傳進度和上傳狀態,使用者可以點選暫停/開始上傳此草稿,但不允許二次編輯。當然,草稿資料都是可以手動刪除的。 -
分片上傳<核心>:將上傳資源中所有分好的檔案片,上傳到伺服器中去,當網路中斷或程式異常都可以支援斷點續傳,保證在網路中斷後該上傳資源中已上傳的那部分檔案片在下次網路連線時或程式啟動後不必再重傳。涉及到更新資源進度,更新資源狀態,以及每一個檔案片的上傳狀態。
資源新建
資源新建模組的UI搭建,筆者這裡就不過多贅述,這裡更多討論的是功能邏輯和細節處理。具體內容還請檢視 CMHCreateSourceController.h/m
-
設定
TZImagePickerController
匯出圖片寬度預設情況下,
TZImagePickerController
(PS:後期統稱TZ
) 預設匯出的圖片寬度為828px
,具體請檢視TZ
提供的photoWidth
屬性。考慮到手動存草稿可以是二次編輯,所以有必要把TZ
返回的圖片儲存到資料庫中,所以我們只需要儲存縮圖
即可,何況新建資源模組本身頁面也只展示小圖,完全沒必要匯出寬度為828px
的圖片,這樣會導致資料儲存和資料讀取都異常緩慢,解決方案如下:/// CoderMikeHe Fixed Bug : 這裡新建模組只需要展示,小圖,所以匯出圖片不需要太大, /// 而且匯出的圖片需要存入資料庫,所以儘量尺寸適量即可,否則會導致儲存資料庫和讀取資料庫異常的慢 imagePickerVc.photoWidth = ceil(MH_SCREEN_WIDTH / 4);
-
PHAsset
儲存資料庫預設情況下,
TZ
是支援本地圖片預覽的,需要我們提供一組selectedAssets
,裡面裝著PHAsset
物件,如果我們處於新建資源頁面時,這完全沒有問題;一旦我們手動存草稿,進行二次編輯時,就會出現問題,原因就是PHAsset
不遵守NSCoding
協議,無法進行歸檔。解決方案其實就是儲存PHAsset的localIdentifier
即可。通過localIdentifier
獲取PHAsset
程式碼如下:/// 獲取PHAsset PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[file.localIdentifier] options:nil]; PHAsset *asset = fetchResult.firstObject; if (!asset) { // 這種場景就是這張照片儲存完PHAsset以後,但使用者在手機上把這張照片刪除 }
-
資源(圖片,視訊)處理
常規邏輯:第一步,通過
TZ
從系統相簿中獲取一組資源(圖片、視訊)檔案,第二步,遍歷資源列表根據PHAsset
去獲取原圖資料或壓縮視訊,第三步將處理過的資源儲存到Cache/Upload
資料夾中。看起來該方案看似穩如藏獒,但是實際情況第二步、第三步操作,其實是非常耗記憶體的,而且每次獲取系統相簿中同一個的資源(PHAsset
),第二步、第三步處理過後都是一樣的,如果該資源(PHAsset
)之前已經通過第二步、第三步處理過,那麼後面在使用到該資源是不是完全沒有必要進行第二步和第三步操作,所以這裡就必須用到資料快取(磁碟快取+記憶體快取)
。 最終方案如下:資源處理邏輯.png
從上圖:point_up_2:明顯可知,只有兩種場景才會去執行第二步、第三步處理,且都是由於不存在磁碟中導致的。這裡有一個比較細節的地方: 快取相對路徑
。千萬不要快取絕對路徑,因為隨著APP的更新或重灌,都會導致應用的沙盒的絕對路徑是會改變的。
實現程式碼如下:
/// 完成圖片選中 - (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 : 這裡可能會涉及到選中多個視訊的情況,且需要壓縮視訊的情況 [MBProgressHUD mh_showProgressHUD:@"正在處理資源..." addedToView:self.view]; NSLog(@"Compress Source Complete Before %@ !!!!" , [NSDate date]); /// 獲取佇列組 dispatch_group_t group = dispatch_group_create(); /// 建立訊號量 用於執行緒同步 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (NSInteger i = 0; i < count; i ++ ) { dispatch_group_enter(group); dispatch_async(_compressQueue, ^{ // 非同步追加任務 /// 設定檔案型別 PHAsset *asset = assets[i]; /// 圖片或資源 唯一id NSString *localIdentifier = [[TZImageManager manager] getAssetIdentifier:asset]; UIImage *thumbImage = photos[i]; /// 這裡要去遍歷已經獲取已經存在資源的檔案 記憶體中 BOOL isExistMemory = NO; for (CMHFile *f in srcFiles.reverseObjectEnumerator) { /// 判斷是否已經存在路徑和檔案 if ([f.localIdentifier isEqualToString:localIdentifier] && MHStringIsNotEmpty(f.filePath)) { [files addObject:f]; [srcFiles removeObject:f]; isExistMemory = YES; break; } } if (isExistMemory) { NSLog(@"++++ :two_hearts:檔案已經存在記憶體中:two_hearts: ++++"); dispatch_group_leave(group); }else{ //// 視訊和圖片,需要快取,這樣會明顯減緩,應用的記憶體壓力 /// 是否已經快取在沙盒 BOOL isExistCache = NO; /// 1. 先去快取裡面去取 NSString *filePath = (NSString *)[[YYCache sharedCache] objectForKey:localIdentifier]; /// 這裡必須的判斷一下filePath是否為空! 以免拼接起來出現問題 if (MHStringIsNotEmpty(filePath)) { /// 2. 該路徑的本地資源是否存在, 拼接絕對路徑,filePath是相對路徑 NSString * absolutePath = [[CMHFileManager cachesDir] stringByAppendingPathComponent:filePath]; if ([CMHFileManager isExistsAtPath:absolutePath]) { /// 3. 檔案存在沙盒中,不需要獲取了 isExistCache = YES; /// 建立檔案模型 CMHFile *file = [[CMHFile alloc] init]; file.thumbImage = thumbImage; file.localIdentifier = localIdentifier; /// 設定檔案型別 file.fileType = (asset.mediaType == PHAssetMediaTypeVideo)? CMHFileTypeVideo : CMHFileTypePicture; file.filePath = filePath; [files addObject:file]; } } if (isExistCache) { NSLog(@"++++ :two_hearts:檔案已經存在磁碟中:two_hearts: ++++"); dispatch_group_leave(group); }else{ /// 重新獲取 if (asset.mediaType == PHAssetMediaTypeVideo) {/// 視訊 /// 獲取視訊檔案 [[TZImageManager manager] getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetMediumQuality success:^(NSString *outputPath) { NSLog(@"+++ 視訊匯出到本地完成,沙盒路徑為:%@ %@",outputPath,[NSThread currentThread]); /// Export completed, send video here, send by outputPath or NSData /// 匯出完成,在這裡寫上傳程式碼,通過路徑或者通過NSData上傳 /// CoderMikeHe Fixed Bug :如果這樣寫[NSData dataWithContentsOfURL:xxxx]; 檔案過大,會導致記憶體吃緊而閃退 /// 解決辦法,直接移動檔案到指定目錄《類似剪下》 NSString *relativePath = [CMHFile moveVideoFileAtPath:outputPath]; if (MHStringIsNotEmpty(relativePath)) { CMHFile *file = [[CMHFile alloc] init]; file.thumbImage = thumbImage; file.localIdentifier = localIdentifier; /// 設定檔案型別 file.fileType =CMHFileTypeVideo; file.filePath = relativePath; [files addObject:file]; /// 快取路徑 [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier]; } dispatch_group_leave(group); /// 訊號量+1 向下執行 dispatch_semaphore_signal(semaphore); } failure:^(NSString *errorMessage, NSError *error) { NSLog(@":sob::sob::sob:++++ Video Export ErrorMessage ++++:sob::sob::sob: is %@" , errorMessage); dispatch_group_leave(group); /// 訊號量+1 向下執行 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; /// 設定檔案型別 file.fileType =CMHFileTypePicture; file.filePath = relativePath; [files addObject:file]; /// 快取路徑 [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier]; } dispatch_group_leave(group); /// 訊號量+1 向下執行 dispatch_semaphore_signal(semaphore); }]; } /// 等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } } }); } /// 所有任務完成 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"Compress Source Complete After %@ !!!!" , [NSDate date]); /// [MBProgressHUD mh_hideHUDForView:self.view]; /// 這裡是所有任務完成 self.source.files = files.copy; [self.tableView reloadData]; }); }
後臺介面
這裡分享一下筆者在實際專案中用到的後臺提供斷點續傳的介面,因為專案中部分邏輯處理是根據後臺提供的資料來的。這裡筆者簡單分析一下各個介面的使用場景。
-
預載入獲取檔案ID(
/fileSection/preLoad.do
)使用場景:根據當次上傳的檔案數量,預先分配好檔案ID,APP終端需要做好儲存與檔案的對應關係,在續傳檔案時候作為引數傳遞。
請求URL:
http://uadmin.xxxx.cn/fileSection/preLoad.do
(POST)Preload.png
-
斷點續傳檔案(
/fileSection/upload.do
)使用場景:大檔案分片並行上傳。
請求URL:
http://uadmin.xxxx.cn/fileSection/upload.do
(POST)Upload.png
-
刪除檔案(
/fileSection/delete.do
)使用場景:在App手動刪除草稿時同時刪除已上傳到伺服器的檔案。
請求URL:
http://uadmin.xxxx.cn/fileSection/delete.do
(POST)Delete.png
-
檢查檔案是否上傳完畢(
/fileSection/isFinish.do
)使用場景:APP中該上傳資源的所有的檔案片都上傳到伺服器,伺服器需要檢查這些檔案片的合成情況。如果伺服器合成失敗,即
finishStatus = 0
,伺服器會把那些合成失敗的檔案返回給APP,即failFileIds
。APP需要根據failFileIds
去回滾本地資料庫,然後繼續重傳失敗的檔案片。請求URL:
http://uadmin.xxxx.cn/fileSection/isFinish.do
(POST)finish.png
檔案分片
檔案分片的過程主要是在使用者點選提交資源的過程。具體內容和細節還請檢視
CMHSource.h/m
、CMHFile.h/m
、CMHFileSource.h/m
、CMHFileBlock.h/m
、CMHFileFragment.h/m
的實現。首先,這裡需要將新建資源
CMHSource
轉成上傳資源CMHFileSource
,以及將新建資源的檔案列表NSArray <CMHFile *> *files
轉成上傳資源的檔案塊列表NSArray <CMHFileBlock *> *fileBlocks
。其次,需要根據新建資源的檔案列表
NSArray <CMHFile *> *files
的個數,即files.count
,去呼叫後臺提供的預載入獲取檔案ID(/fileSection/preLoad.do
)介面,去獲取檔案ID列表,從而為檔案列表NSArray <CMHFile *> *files
中每一個檔案(CMHFile
)繫結檔案ID,然後將CMHFile
列表轉成CMHFileBlock
列表,以及將新建資源CMHSource
轉成上傳資源CMHFileSource
。 關鍵程式碼如下:- (void)commitSource:(void (^)(BOOL))complete{ /// 1. 通過要上傳的檔案個數去伺服器獲取對應的檔案ID NSInteger uploadFileCount = self.files.count; /// 2. 以下通過真實的網路請求去模擬獲取 檔案ID的場景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1 /// 類似於實際開發中呼叫伺服器的API:/fileSection/preLoad.do /// 1. 配置引數 CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript]; subscript[@"isEnglish"] = @0; subscript[@"devicetype"] = @2; subscript[@"version"] = @"1.0.1"; /// 2. 配置引數模型 CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary]; /// 3. 發起請求 [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id responseObject) { /// - 如果到這裡了就認為獲取檔案ID成功,這裡模擬後臺返回的資料 有幾個上傳檔案 就對應幾個上傳檔案ID NSMutableArray *fileIds = [NSMutableArray arrayWithCapacity:uploadFileCount]; for (NSInteger i = 0; i < uploadFileCount; i++) { NSString *fileId = [self _cmh_fileKey]; [fileIds addObject:fileId]; } /// - 為每個上傳檔案繫結伺服器返回的檔案ID,獲取要上傳的檔案塊列表 /// 將伺服器檔案ID列表轉換為,轉成json字串,後期需要存資料庫,這個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]; /// 資源中的檔案繫結檔案ID file.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; /// 儲存檔案和資源 /// 非手動存草稿 self.manualSaveDraft = NO; /// CoderMikeHe Fixed Bug : 這裡必須記錄必須強引用上傳資源 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 ,因為這是非同步,會導致 fileSource == nil; /// 儲存上傳資源 @weakify(self); [self.fileSource saveFileSourceToDB:^(BOOL rst) { !complete ? : complete(rst); @strongify(self); /// 這裡需要開始上傳 if (rst) { [[CMHFileUploadManager sharedManager] uploadSource:self.sourceId]; }else{ [MBProgressHUD mh_showTips:@"儲存上傳資源失敗!!!"]; } }]; }]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) { /// 回撥錯誤 !complete ? : complete(NO); /// show error [MBProgressHUD mh_showErrorTips:error]; }]; }
然後,我們需要將檔案塊
CMHFileBlock
按照512k
的大小切割成多個檔案片CMHFileFragment
,這裡的程式碼實現和屬性生成都是參照這篇文章:point_right: HTTP斷點續傳與斷點上傳之 -- 檔案流操作 來實現的。關鍵程式碼如下:// 切分檔案片段 - (void)_cutFileForFragments { NSUInteger offset = CMHFileFragmentMaxSize; // 總片數 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; } /// 關聯屬性 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; }
最後,我們知道
一份上傳資源
由多個檔案塊
組成,而一個檔案塊
由多個檔案片
組成。所以我們是不是可以這樣理解:一份上傳資源
由多個檔案片
組成。前提是要保證每一個檔案片,必須含有兩個屬性sourceId
和fileId
。sourceId
: 代表這個檔案片所屬於哪個資源。fileId
: 代表這個檔案片所屬於哪個檔案塊。一份上傳資源
由多個檔案片
組成的程式碼實現,無非就是重寫CMHFileSource
的setFileBlocks
即可。關鍵程式碼如下:- (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; }
當然,我們需要將
CMHSource
、CMHFileSource
、CMHFileFragment
儲存到資料庫即可。
分片上傳
分片上傳是本Demo中一個比較重要的功能點,但其實功能點並不難,主要複雜的還是業務邏輯以及資料庫處理。分片上傳,其原理還是檔案上傳,某個檔案片的上傳和我們平時上傳頭像的邏輯一模一樣,不同點無非就是我們需要利用資料庫去記錄每一片的上傳狀態罷了。詳情請參考: CMHFileUploadManager.h/m
這裡筆者以 CMHFileUploadManager
上傳某個資源為例,具體講講其中的邏輯以及細節處理。具體的程式碼實現請參考: - (void)uploadSource:(NSString *)sourceId;
的實現。 注意:筆者提供的Demo,一次只能上傳一個資源 。關於具體的業務邏輯分析,筆者已經寫在寫在程式碼註釋裡面了,這裡就不再贅述,還請結合程式碼註釋去理解具體的業務邏輯和場景。關鍵程式碼如下:
/// 上傳資源 <核心方法> - (void)uploadSource:(NSString *)sourceId{ if (!MHStringIsNotEmpty(sourceId)) { return; } /// CoderMikeHe Fixed Bug : 解決初次載入的問題,不需要驗證網路 if (self.isLoaded) { if (![AFNetworkReachabilityManager sharedManager].isReachable) { /// 沒有網路 [self postFileUploadStatusDidChangedNotification:sourceId]; return; } } self.loaded = YES; /// - 獲取該資源下所有未上傳完成的檔案片 NSArray *uploadFileFragments = [CMHFileFragment fetchAllWaitingForUploadFileFragment:sourceId]; if (uploadFileFragments.count == 0) { /// 沒有要上傳的檔案片 /// 獲取上傳資源 CMHFileSource *fileSource = [CMHFileSource fetchFileSource:sourceId]; /// 獲取資源 CMHSource *source = [CMHSource fetchSource:sourceId]; if (MHObjectIsNil(source)) { /// 提交下一個資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒有資源,則:u6709:何須上傳資源,將資料庫裡面清掉 [CMHFileSource removeFileSourceFromDB:sourceId complete:NULL]; /// 通知草稿頁 刪除詞條資料 [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}]; return; } if (MHObjectIsNil(fileSource)) { /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒有上傳資源 ,則直接提交 [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; [self _commitSource:sourceId]; return; } if (fileSource.totalFileFragment <= 0) { /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; /// 沒有上傳檔案片 [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; [self _commitSource:sourceId]; return; } /// 倒了這裡 , 證明 fileSource,source 有值,且 fileSource.totalFileFragment > 0 CMHFileUploadStatus uploadStatus = [CMHFileSource fetchFileUploadStatus:sourceId]; if (uploadStatus == CMHFileUploadStatusFinished) { // 檔案全部上傳成 dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25/*延遲執行時間*/ * NSEC_PER_SEC)); dispatch_after(delayTime, dispatch_get_main_queue(), ^{ /// 檢查伺服器的檔案上傳合成狀態 [self _checkFileFragmentSynthetiseStatusFromService:sourceId]; }); }else{ /// 到了這裡,則證明這個草稿永遠都不會上傳成功了,這裡很遺憾則需要將其從資料庫中移除 /// 提交資源 [self _autoUploadSource:sourceId reUpload:NO]; [CMHSource removeSourceFromDB:sourceId complete:NULL]; /// 通知草稿頁 刪除這條資料 [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}]; } return; } /// 0. 這裡一定會新建一個新的上傳佇列,一定會開啟一個新的任務 /// - 看是否存在於上傳陣列中 NSString *findSid = nil; /// - 是否有檔案正在上傳 BOOL isUploading = NO; for (NSString *sid in self.uploadFileArray) { /// 上傳資源裡面已經存在了,findSid if ([sid isEqualToString:sourceId]) { findSid = sid; } /// 檢視當前是否有上傳任務正在上傳 CMHFileUploadQueue *queue = [self.uploadFileQueueDict objectForKey:sid]; if (queue && !queue.isSuspended) { isUploading = YES; } } /// 2. 檢查狀態,插入資料, if (findSid) { /// 已經存在了,那就先刪除,後插入到第0個元素 [self.uploadFileArray removeObject:findSid]; [self.uploadFileArray insertObject:sourceId atIndex:0]; }else{ /// 不存在上傳資源陣列中,直接插入到第0個元素 [self.uploadFileArray insertObject:sourceId atIndex:0]; } /// 3. 檢查是否已經有上傳任務了 if (isUploading) { /// 已經有正在上傳任務了,則不需要開啟隊列了,就請繼續等待 /// 傳送通知 [self postFileUploadStatusDidChangedNotification:sourceId]; return; } /// 4. 如果沒有上傳任務,你就建立隊裡開啟任務即可 /// 更新這個上傳檔案的狀態 為 `正在上傳的狀態` [self updateUpLoadStatus:CMHFileUploadStatusUploading sourceId:sourceId]; /// 建立訊號量 用於執行緒同步 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); /// 建立一個佇列組 dispatch_group_t group = dispatch_group_create(); /// 運算元 NSMutableArray *operations = [NSMutableArray array]; /// 這裡採用序列佇列且序列請求的方式處理每一片的上傳 for (CMHFileFragment *ff in uploadFileFragments) { /// 進組 dispatch_group_enter(group); // 建立物件,封裝操作 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ /// 切記:任務(網路請求)是序列執行的 ,但網路請求結果回撥是非同步的、 [self _uploadFileFragment:ff progress:^(NSProgress *progress) { NSLog(@" \n上傳檔案ID:point_right:【%@】\n上傳檔案片:point_right: 【%ld】\n上傳進度為:point_right:【%@】",ff.fileId, (long)ff.fragmentIndex, progress.localizedDescription); } success:^(id responseObject) { /// 處理成功的檔案片 [self _handleUploadFileFragment:ff]; /// 退組 dispatch_group_leave(group); /// 訊號量+1 向下執行 dispatch_semaphore_signal(semaphore); } failure:^(NSError *error) { /// 更新資料 /// 某片上傳失敗 [ff updateFileFragmentUploadStatus:CMHFileUploadStatusWaiting]; /// 退組 dispatch_group_leave(group); /// 訊號量+1 向下執行 dispatch_semaphore_signal(semaphore); }]; /// 等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }]; /// 新增運算元組 [operations addObject:operation]; } /// 建立NSOperationQueue CMHFileUploadQueue * uploadFileQueue = [[CMHFileUploadQueue alloc] init]; /// 存起來 [self.uploadFileQueueDict setObject:uploadFileQueue forKey:sourceId]; /// 把操作新增到佇列中 不需要設定為等待 [uploadFileQueue addOperations:operations waitUntilFinished:NO]; /// 佇列組的操作全部完成 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@":grin::grin::grin:+++dispatch_group_notify+++:grin::grin::grin:"); /// 0. 如果執行到這,證明此`Queue`裡面的所有操作都已經全部完成了,你如果再使用 [queue setSuspended:YES/NO];將沒有任何意義,所以你必須將其移除掉 [self.uploadFileQueueDict removeObjectForKey:sourceId]; /// 1. 佇列完畢了,清除掉當前的資源,開啟下一個資源 [self _removeSourceFromUploadFileArray:sourceId]; /// CoderMikeHe: 這裡先不更新草稿頁的狀態,等提交完表格再去傳送通知 /// 檢查一下資源上傳 [self _uploadSourceEnd:sourceId]; }); //// 告知外界其資源狀態改過了 [self postFileUploadStatusDidChangedNotification:sourceId]; }
這裡對上傳資源下的需要上傳的檔案片做了迴圈的上傳,由於網路請求是一個非同步的操作,同時也考慮到太多併發(當然系統對於網路請求開闢的執行緒個數也有限制)對於手機效能的影響,因此利用GCD訊號量等待這種功能特性 讓一個片段上傳完之後再進行下一個片段的上傳
。
檔案上傳核心程式碼如下:
/// 上傳某一片檔案 這裡用作測試 - (void)_uploadFileFragment:(CMHFileFragment *)fileFragment progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress success:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure{ /// 獲取上傳引數 NSDictionary *parameters = [fileFragment fetchUploadParamsInfo]; /// 獲取上傳資料 NSData *fileData = [fileFragment fetchFileFragmentData]; /// 資原始檔找不到,則直接修改資料庫,無論如何也得讓使用者把資源提交上去,而不是讓其永遠卡在草稿頁裡,這樣太影響使用者體驗了 if (fileData == nil) { /// CoderMikeHe Fixed Bug : V1.6.7之前 修復檔案丟失的情況 /// 1. 獲取該片所處的資源 CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:fileFragment.sourceId]; /// 取出fileID NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject]; NSLog(@":sob::sob::sob::sob: Before -- 檔案<%@>未找到個數 %ld <%@> :sob::sob::sob::sob:",fileFragment.fileId , fileIds.count, fileIds); if ([fileIds containsObject:fileFragment.fileId]) { /// 資料庫包含 [fileIds removeObject:fileFragment.fileId]; uploadSource.fileIds = fileIds.yy_modelToJSONString; /// 更新資料庫 [uploadSource saveOrUpdate]; } NSLog(@":sob::sob::sob::sob: After -- 檔案<%@>未找到個數 %ld <%@> :sob::sob::sob::sob:",fileFragment.fileId , fileIds.count, fileIds); /// 一定要回調為成功,讓使用者誤以為正在上傳,而不是直接卡死在草稿頁 NSDictionary *responseObj = @{@"code" : @200}; !success ? : success(responseObj); return; } /// 這裡筆者只是模擬一下網路情況哈,不要在乎這些細節 , /// 類似於實際開發中呼叫伺服器的API:/fileSection/upload.do /// 2. 以下通過真實的網路請求去模擬獲取 檔案ID的場景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1 /// 1. 配置引數 CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript]; subscript[@"isEnglish"] = @0; subscript[@"devicetype"] = @2; subscript[@"version"] = @"1.0.1"; /// 2. 配置引數模型 CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary]; /// 3. 發起請求 [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id_Nullable responseObject) { #warning CMH TODO 稍微延遲一下,模擬現實情況下的上傳進度 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 /// 這個是真實上傳,請根據自身實際專案出發/fileSection/upload.do [self _uploadFileFragmentWithParameters:parameters fileType:fileFragment.fileType fileData:fileData fileName:fileFragment.fileName progress:uploadProgress success:success failure:failure]; #endif } /// 實際開發專案中上傳每一片檔案,這裡請結合自身專案開發去設計 - (NSURLSessionDataTask *)_uploadFileFragmentWithParameters:(NSDictionary *)parameters fileType:(CMHFileType)fileType fileData:(NSData *)fileData fileName:(NSString *)fileName progress:(void (^)(NSProgress *))uploadProgress success:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure{ /// 配置成伺服器想要的樣式 NSMutableArray *paramsArray = [NSMutableArray array]; [paramsArray addObject:parameters]; /// 生成jsonString NSString *jsonStr = [paramsArray yy_modelToJSONString]; /// 設定TTPHeaderField [self.uploadService.requestSerializer setValue:jsonStr forHTTPHeaderField:@"file_block"]; /// 開啟檔案任務上傳 /// PS : 著了完全可以看成,我們平常上傳頭像給伺服器一樣的處理方式 NSURLSessionDataTask *uploadTask = [self.uploadService POST:@"/fileSection/upload.do" parameters:nil/** 一般這裡傳的是基本引數 */ constructingBodyWithBlock:^(id<AFMultipartFormData>_Nonnull formData) { /// 拼接mimeType NSString *mimeType = [NSString stringWithFormat:@"%@/%@",(fileType == CMHFileTypePicture) ? @"image":@"video",[[fileName componentsSeparatedByString:@"."] lastObject]]; /// 拼接資料 [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; }
檢查伺服器檔案上傳合成情況的核心程式碼如下:
/// 檢查伺服器檔案片合成情況 - (void)_checkFileFragmentSynthetiseStatusFromService:(NSString *)sourceId{ /// 這裡呼叫伺服器的介面檢查檔案上傳狀態,以這個為標準 CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:sourceId]; /// 沒意義 if (uploadSource == nil) { return; } /// 如果這裡進來了,則證明準備驗證檔案片和提交表單,則草稿裡面的這塊表單,你不能在讓使用者去點選了 [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES]; /// V1.6.5之前的介面老資料 if (!MHStringIsNotEmpty(uploadSource.fileIds)) { /// 這裡可能是老資料,直接認為成功,就不要去跟伺服器打交道了 /// 成功 [self _commitSource:sourceId]; /// 上傳下一個 [self _autoUploadSource:sourceId reUpload:NO]; return; } /// 這裡筆者只是模擬一下網路情況哈,不要在乎這些細節, /// 類似於實際開發中呼叫伺服器的API:/fileSection/isFinish.do /// 2. 以下通過真實的網路請求去模擬獲取 檔案ID的場景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1 /// 1. 配置引數 CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript]; subscript[@"isEnglish"] = @0; subscript[@"devicetype"] = @2; subscript[@"version"] = @"1.0.1"; /// 2. 配置引數模型 CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary]; /// 3. 發起請求 [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id_Nullable responseObject) { /// 模擬後臺返回的合成結果 CMHFileSynthetise *fs = [[CMHFileSynthetise alloc] init]; NSInteger randomNum = [NSObject mh_randomNumber:0 to:20]; fs.finishStatus = (randomNum > 0) ? 1 : 0;/// 模擬伺服器合成失敗的場景,畢竟合成失敗的機率很低 if (fs.finishStatus>0) { /// 伺服器合成資原始檔成功 /// 成功 [self _commitSource:sourceId]; /// 上傳下一個 [self _autoUploadSource:sourceId reUpload:NO]; return ; } /// 伺服器合成資原始檔失敗, 伺服器會把合成失敗的 fileId 返回出來 /// 也就是 "failFileIds" : "fileId0,fileId1,..."的格式返回出來 /// 這裡模擬後臺返回合成錯誤的檔案ID, 這裡只是演習!!這裡只是演習!! /// 取出fileID NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject]; /// 模擬只有一個檔案ID合成失敗 NSString *failFileIds = fileIds.firstObject; fs.failFileIds = failFileIds; /// 這裡才是模擬真實的網路情況 if (MHStringIsNotEmpty(fs.failFileIds)) { /// 1. 回滾資料 [uploadSource rollbackFailureFile:fs.failureFileIds]; /// 2. 獲取進度 CGFloat progress = [CMHFileSource fetchUploadProgress:sourceId]; /// 3. 傳送通知 [MHNotificationCenter postNotificationName:CMHFileUploadProgressDidChangedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId , CMHFileUploadProgressDidChangedKey : @(progress)}]; /// 4. 重新設定回滾資料的經度 [CMHSource updateSourceProgress:progress sourceId:sourceId]; }else{ /// 無需回滾,修改狀態即可 [self postFileUploadStatusDidChangedNotification:sourceId]; } /// 合成失敗,繼續重傳失敗的片,允許使用者點選草稿頁的資源 [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO]; /// 重傳該資源 [self _autoUploadSource:sourceId reUpload:YES]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) { /// 1. 伺服器報錯不重傳 [MBProgressHUD mh_showErrorTips:error]; /// 更新資源狀態 [self updateUpLoadStatus:CMHFileUploadStatusWaiting sourceId:sourceId]; /// 更新狀態 [self postFileUploadStatusDidChangedNotification:sourceId]; /// 檔案片合成失敗,允許點選 [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO]; }]; }
總之,檔案分片上傳邏輯不止上面這一點點內容,還有存在許多邏輯處理和細節注意,比如暫停上傳資源;開始上傳資源;取消上傳資源;取消所有上傳資源;伺服器合成某些檔案失敗,客戶端回滾資料庫,重傳失敗的檔案片;某個資源上傳後自動重傳下個資源....等等。大家有興趣可以檢視 CMHFileUploadManager.h
提供的API的具體實現。 CMHFileUploadManager.h
的所有內容如下:
/// 某資源的所有片資料上傳,完成也就是提交資源到伺服器成功。 FOUNDATION_EXTERN NSString *const CMHFileUploadDidFinishedNotification; /// 資原始檔上傳狀態改變的通知 FOUNDATION_EXTERN NSString *const CMHFileUploadStatusDidChangedNotification; /// 草稿上傳檔案狀態 disable 是否不能點選 如果為YES 不要修改草稿頁表單的上傳狀態 主需要讓使用者不允許點選上傳按鈕 FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusKey; FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusNotification; /// 某資源中的某片資料上傳完成 FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedNotification; /// 某資源的id FOUNDATION_EXTERN NSString *const CMHFileUploadSourceIdKey; /// 某資源的進度 FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedKey; @interface CMHFileUploadManager : NSObject /// 存放操作佇列的字典 @property (nonatomic , readonly , strong) NSMutableDictionary *uploadFileQueueDict; /// 宣告單例 + (instancetype)sharedManager; /// 銷燬單例 + (void)deallocManager; /// 基礎配置,主要是後臺上傳草稿資料一般這個方法會放在 程式啟動後切換到主頁時呼叫 - (void)configure; /// 上傳資源 /// sourceId:檔案組Id - (void)uploadSource:(NSString *)sourceId; /// 暫停上傳 -- 使用者操作 /// sourceId: 資源Id - (void)suspendUpload:(NSString *)sourceId; /// 繼續上傳 -- 使用者操作 /// sourceId: 資源Id - (void)resumeUpload:(NSString *)sourceId; /// 取消掉上傳 -- 使用者操作 /// sourceId: 資源Id - (void)cancelUpload:(NSString *)sourceId; /// 取消掉所有上傳 一般這個方法會放在 程式啟動後切換到登入頁時呼叫 - (void)cancelAllUpload; /// 刪除當前使用者無效的資源 - (void)clearInvalidDiskCache; //// 以下方法跟伺服器互動,只管呼叫即可,無需回撥, /// 清除掉已經上傳到伺服器的檔案片 fileSection - (void)deleteUploadedFile:(NSString *)sourceId; /// 告知草稿頁,某個資源的上傳狀態改變 /// sourceId -- 資源ID - (void)postFileUploadStatusDidChangedNotification:(NSString *)sourceId; /// 告知草稿頁,某個資源不允許點選 - (void)postFileUploadDisableStatusNotification:(NSString *)sourceId fileUploadDisabled:(BOOL)fileUploadDisabled; /// 更新資源的狀態 /// uploadStatus -- 上傳狀態 /// sourceId -- 資源ID - (void)updateUpLoadStatus:(CMHFileUploadStatus)uploadStatus sourceId:(NSString *)sourceId; @end
總結
以上內容,就是筆者在做 大檔案分片上傳
的過程中的心得體會。看似簡單的 檔案分片上傳
功能,但其中涵蓋的知識面還是比較廣的,結合筆者前面談及的必備知識點,大家業餘時間可以系統去學習和掌握,最後筆者還是建議大家把 多執行緒
的相關知識惡補一下和實踐起來。當然這其中肯定還有一些細小的邏輯和細節問題還未暴露出來,如果大家在使用和檢視過程中發現問題或者不理解的地方,以及如果有好的建議或意見都可以在底部:point_down:評論區指出。
期待
- 文章若對您有點幫助,請給個喜歡:heart:,畢竟碼字不易;若對您沒啥幫助,請給點建議:heartpulse:,切記學無止境。
- 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
- GitHub地址: https://github.com/CoderMikeHe
- 原始碼地址:
MHDevelopExample目錄中的Architecture/Contacts/FileUpload資料夾中 < 特別強調: 使用前請全域性搜尋CMHDEBUG
欄位並將該 巨集 置為1
即可,預設是0
>