iOS 利用AFNetworking實現大文件分片上傳

概述

一說到文件上傳,想必大家都并不陌生,更何況是利用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;
    

    綜上所述:NSDataNSFileManagerNSFileHandle的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 來做內存緩存和磁盤緩存。具體使用,還請自行百度。

  • 數據庫
    數據庫主要用于,保存新建資源,保存上傳資源,保存文件片...等等,利用數據庫的,,等功能,更加方便快捷的處理文件片的上傳狀態,上傳進度,獲取或刪除草稿數據...等等一些列的操作,大大提供了開發的效率。這里筆者采用的是基于FMDB封裝的BGFMDB框架,BGFMDB是對FMDB面相對象層的封裝,且幾乎支持存儲iOS所有基本的自帶數據類型,讓數據的,,分別只需要一行代碼即可。具體使用,還請查看BGFMDB提供的Demo。

  • 多線程
    多線程的使用主要用在,① 從系統相冊獲取到資源(圖片、視頻),對資源進行處理(比如,獲取原圖,壓縮視頻等等); ② 文件分片上傳。其實現實開發中,我們使用多線程的的場景并不多,但反觀使用多線程最多的場景就是--面試。多線程其實是iOS中非常重要的知識點,但是由于平時疏于練習和使用,腦子里面可能只有少許多線程的相關知識。此次筆者在項目中做大文件分片上傳功能,也讓筆者重拾了多線程的相關知識,而且運用到實際開發中去,也是一個不小的收獲。這里筆者就講講本模塊中用到了哪些多線程的知識,當然具體的理論知識和實踐操作,大家可以參照下面筆者分享的網址去針對性的學習和實踐多線程的相關知識。具體如下:

    • iOS多線程:『GCD』詳盡總結

      特別提醒: ① 必須掌握GCD 隊列組:dispatch_group。合理使用dispatch_group_enter、dispatch_group_leavedispatch_group_notify的配套使用。
      ② 必須掌握GCD 信號量:dispatch_semaphore。熟練使用dispatch_semaphore_createdispatch_semaphore_signaldispatch_semaphore_wait的配套使用,利用dispatch_semaphore保持線程同步,將異步執行任務轉換為同步執行任務以及保證線程安全,為線程加鎖。

    • iOS多線程:『NSOperation、NSOperationQueue』詳盡總結

模塊

關于筆者在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

從上圖??明顯可知,只有兩種場景才會去執行第二步、第三步處理,且都是由于不存在磁盤中導致的。這里有一個比較細節的地方:緩存相對路徑。千萬不要緩存絕對路徑,因為隨著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(@"++++ ??文件已經存在內存中?? ++++");
              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(@"++++ ??文件已經存在磁盤中?? ++++");
                  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(@"??????++++ Video Export ErrorMessage ++++?????? 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/mCMHFile.h/mCMHFileSource.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,這里的代碼實現和屬性生成都是參照這篇文章??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;
    }
    

    最后,我們知道一份上傳資源多個文件塊組成,而一個文件塊多個文件片組成。所以我們是不是可以這樣理解:一份上傳資源多個文件片組成。前提是要保證每一個文件片,必須含有兩個屬性sourceIdfileId。
    sourceId : 代表這個文件片所屬于哪個資源。
    fileId : 代表這個文件片所屬于哪個文件塊。
    一份上傳資源多個文件片組成的代碼實現,無非就是重寫CMHFileSourcesetFileBlocks即可。關鍵代碼如下:

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

    當然,我們需要將CMHSourceCMHFileSourceCMHFileFragment保存到數據庫即可。

分片上傳

分片上傳是本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];
            
            /// 沒有資源,則??何須上傳資源,將數據庫里面清掉
            [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??【%@】\n上傳文件片?? 【%ld】\n上傳進度為??【%@】",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(@"??????+++dispatch_group_notify+++??????");
        /// 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(@"???????? Before -- 文件<%@>未找到個數 %ld <%@> ????????",fileFragment.fileId , fileIds.count, fileIds);
        if ([fileIds containsObject:fileFragment.fileId]) {
            /// 數據庫包含
            [fileIds removeObject:fileFragment.fileId];
            uploadSource.fileIds = fileIds.yy_modelToJSONString;
            /// 更新數據庫
            [uploadSource saveOrUpdate];
        }
        NSLog(@"???????? After -- 文件<%@>未找到個數 %ld <%@> ????????",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
總結

以上內容,就是筆者在做大文件分片上傳的過程中的心得體會??此坪唵蔚?code>文件分片上傳功能,但其中涵蓋的知識面還是比較廣的,結合筆者前面談及的必備知識點,大家業余時間可以系統去學習和掌握,最后筆者還是建議大家把多線程的相關知識惡補一下和實踐起來。當然這其中肯定還有一些細小的邏輯和細節問題還未暴露出來,如果大家在使用和查看過程中發現問題或者不理解的地方,以及如果有好的建議或意見都可以在底部??評論區指出。

期待
  1. 文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
  2. 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:
    MHDevelopExample目錄中的Architecture/Contacts/FileUpload文件夾中 <特別強調: 使用前請全局搜索 CMHDEBUG 字段并將該置為 1即可,默認是0 >
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容