中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

iOS 利用AFNetworking實現(xiàn)大文件分片上傳

2018-08-29    來源:編程學(xué)習(xí)網(wǎng)

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

概述

一說到文件上傳,想必大家都并不陌生,更何況是利用 AFNetworking (PS:后期統(tǒng)稱AF)來做,那更是小菜一碟。比如開發(fā)中常見的場景:頭像上傳,九宮格圖片上傳...等等,這些場景無一不使用到文件上傳的功能。如果利用AF來實現(xiàn),無非就是客戶端調(diào)用AF提供的文件上傳接口即可,API如下所示:

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
              constructingBodyWithBlock:(nullable void (^)(id 

  
  
   
   
  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以上),則必須要考慮上傳過程網(wǎng)絡(luò)中斷的情況。試想我們還是采用上述方案,一口氣把這整個1G的資源文件上傳到服務(wù)器,這顯然是不現(xiàn)實的,就算服務(wù)器答應(yīng),用戶也不答應(yīng)的?紤]到網(wǎng)絡(luò)使用中斷或服務(wù)器上傳異常...等場景,那么我們恢復(fù)網(wǎng)絡(luò)后又得重新從頭開始上傳,那之前已經(jīng)上傳完成的部分資源豈不作廢,這種耗時耗力的工作,顯然是不符合常理的。為了解決大文件上傳的存在如此雞肋的問題,從而誕生了一個叫: 分片上傳(斷點續(xù)上傳)

分片上傳(斷點續(xù)上傳)主要是為了保證在網(wǎng)絡(luò)中斷后1G的資源文件已上傳的那部分在下次網(wǎng)絡(luò)連接時不必再重傳。所以我們本地在上傳的時候,要將大文件進行切割分片,比如分成1024*1024B,即將大文件分成1M的片進行上傳,服務(wù)器在接收后,再將這些片合并成原始文件,這就是 分片 的基本原理。斷點續(xù)傳要求本地要記錄每一片的上傳的狀態(tài),我通過三個狀態(tài)進行了標(biāo)記(waiting loading finish),當(dāng)網(wǎng)絡(luò)中斷,再次連接后,從斷點處進行上傳。服務(wù)器通過文件名、總片數(shù)判斷該文件是否已全部上傳完成。

弄懂了 分片上傳(斷點續(xù)上傳) 的基本原理,其核心就是 分片 ,然后將分割出來的的每一片,按照類似上傳頭像的方式上傳到服務(wù)器即可,全部上傳完后再在服務(wù)端將這些小數(shù)據(jù)片合并成為一個資源。

分片上傳引入了兩個概念: 塊(block)片(fragment) 。每個塊由一到多個片組成,而一個資源則由一到多個塊組成。他們之間的關(guān)系可以用下圖表述:

文件資源組成關(guān)系.png

本文筆者將著重分析 分片上傳 實現(xiàn)的具體過程以及細節(jié)處理,爭取把里面的所有涵蓋的知識點以及細節(jié)處理分析透徹。希望為大家提供一點思路,少走一些彎路,填補一些細坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。

效果圖如下:

 

知識點

雖然 分片上傳 的原理看似非常簡單,但是落實到具體的實現(xiàn),其中還是具有非常多的細節(jié)分析和邏輯處理,而且都是我們開發(fā)中不常用到的知識點,這里筆者就總結(jié)了一下 分片上傳 所用到的知識點和使用場景,以及借助一些第三方框架,來達到分片上傳的目的。

  • 圖片和視頻資源的獲取

    所謂文件上傳,前提必須得有文件,而文件一般是本地文件,本地文件的獲取來源一般是系統(tǒng)相冊獲取,關(guān)于如何從系統(tǒng)相冊中獲取圖片或視頻資源,這里筆者采用 TZImagePickerController 一個支持多選、選原圖和視頻的圖片選擇器,同時有預(yù)覽、裁剪功能,支持iOS6+第三方框架。根據(jù)TZImagePickerControllerDelegate返回的資源(圖片、視頻)數(shù)據(jù),然后利用TZImageMananger提供的API,獲取到原始圖片和視頻資源。關(guān)鍵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,然后將其寫入到指定的文件夾中,一般調(diào)用NSData提供的方法。

    - (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile
    

    文件剪切一般用于從相冊中獲取到視頻資源,其視頻格式是mov格式的,需要我們視頻壓縮轉(zhuǎn)成mp4格式,壓縮成功后一般將其導(dǎo)入到APP沙盒文件的tmp目錄下,總所周知,tmp里面一般存放一些臨時文件,所以需要將其導(dǎo)入到Cache文件夾中去,這里用文件移動(剪切)再好不過了,而且不需要讀取到內(nèi)存中去。 直接調(diào)用 NSFileManager的提供的API即可:

    - (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error

    文件讀取一般主要用于讀取每一個文件片的大小,需要利用NSFileHandle來處理,調(diào)用其如下API來完成。

    - (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來做的,具體請參照TZImageManager提供的方法,大家可以看看其實現(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)相冊中獲取到的資源(圖片、視頻),我們會將資源另存到在/Library/Caches/Upload目錄下,然后把資源存放的相對路徑給緩存起來,下次從系統(tǒng)相冊中選取相同的資源,如果已經(jīng)存在于/Library/Caches/Upload目錄下,則不需要去獲取原始圖片,或者壓縮視頻了。這里筆者采用的是: YYCache 來做內(nèi)存緩存和磁盤緩存。具體使用,還請自行百度。

  • 數(shù)據(jù)庫  

    數(shù)據(jù)庫主要用于,保存新建資源,保存上傳資源,保存文件片...等等,利用數(shù)據(jù)庫的增,刪,改,查等功能,更加方便快捷的處理文件片的上傳狀態(tài),上傳進度,獲取或刪除草稿數(shù)據(jù)...等等一些列的操作,大大提供了開發(fā)的效率。這里筆者采用的是基于 FMDB 封裝的 BGFMDB 框架,BGFMDB是對FMDB面相對象層的封裝,且?guī)缀踔С执鎯OS所有基本的自帶數(shù)據(jù)類型,讓數(shù)據(jù)的增,刪,改,查分別只需要一行代碼即可。具體使用,還請查看BGFMDB提供的Demo。

  • 多線程  

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

    • iOS多線程:『GCD』詳盡總結(jié)

      特別提醒: ① 必須掌握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保持線程同步,將異步執(zhí)行任務(wù)轉(zhuǎn)換為同步執(zhí)行任務(wù)以及保證線程安全,為線程加鎖。

    • iOS多線程:『NSOperation、NSOperationQueue』詳盡總結(jié)

模塊

關(guān)于筆者在Demo中提供的文件分片上傳的示例程序,雖然不夠華麗,但麻雀雖小,五臟俱全,大家湊合著看咯。但總的來說,可以簡單分為以下幾個模塊:

  • 資源新建: 系統(tǒng)相冊獲取資源文件(圖片、視頻);獲取原圖或視頻壓縮,并導(dǎo)入到沙盒指定的文件夾;資源緩存。

  • 后臺接口: 考慮到示例程序中部分業(yè)務(wù)邏輯是按照后臺提供的API設(shè)計的,所以有必要分享一下后臺提供了哪些API,以及具體的使用的場景。

  • 文件分片: 將新建資源,轉(zhuǎn)化為上傳資源,將資源中存放的每一個文件塊,按照512k的大小分成若干個文件片。涉及到新建資源存儲數(shù)據(jù)庫,上傳資源存儲數(shù)據(jù)庫,以及每個文件片存儲數(shù)據(jù)庫。

  • 草稿存儲: 草稿列表的數(shù)據(jù)來源主要分為手動存草稿和自動存草稿。手動存草稿一般是指用戶手動點擊存草稿按鈕保存草稿,此草稿數(shù)據(jù)可以進行二次編輯;自動存草稿一般是指用戶點擊提交按鈕上傳資源文件,由于一時半會不會上傳到服務(wù)器上去,所以需要報存草稿,此草稿數(shù)據(jù)可以顯示上傳進度和上傳狀態(tài),用戶可以點擊暫停/開始上傳此草稿,但不允許二次編輯。當(dāng)然,草稿數(shù)據(jù)都是可以手動刪除的。

  • 分片上傳 <核心> : 將上傳資源中所有分好的文件片,上傳到服務(wù)器中去,當(dāng)網(wǎng)絡(luò)中斷或程序異常都可以支持?jǐn)帱c續(xù)傳,保證在網(wǎng)絡(luò)中斷后該上傳資源中已上傳的那部分文件片在下次網(wǎng)絡(luò)連接時或程序啟動后不必再重傳。涉及到更新資源進度,更新資源狀態(tài),以及每一個文件片的上傳狀態(tài)。

資源新建

資源新建模塊的UI搭建,筆者這里就不過多贅述,這里更多討論的是功能邏輯和細節(jié)處理。具體內(nèi)容還請查看CMHCreateSourceController.h/m

  • 設(shè)置TZImagePickerController導(dǎo)出圖片寬度

    默認(rèn)情況下,TZImagePickerController (PS:后期統(tǒng)稱TZ) 默認(rèn)導(dǎo)出的圖片寬度為828px,具體請查看TZ提供的photoWidth屬性?紤]到手動存草稿可以是二次編輯,所以有必要把TZ返回的圖片儲存到數(shù)據(jù)庫中,所以我們只需要存儲縮略圖即可,何況新建資源模塊本身頁面也只展示小圖,完全沒必要導(dǎo)出寬度為828px的圖片,這樣會導(dǎo)致數(shù)據(jù)存儲和數(shù)據(jù)讀取都異常緩慢,解決方案如下:

    /// CoderMikeHe Fixed Bug : 這里新建模塊只需要展示,小圖,所以導(dǎo)出圖片不需要太大,
    /// 而且導(dǎo)出的圖片需要存入數(shù)據(jù)庫,所以盡量尺寸適量即可,否則會導(dǎo)致存儲數(shù)據(jù)庫和讀取數(shù)據(jù)庫異常的慢
      imagePickerVc.photoWidth = ceil(MH_SCREEN_WIDTH / 4);
  • PHAsset 保存數(shù)據(jù)庫

    默認(rèn)情況下,TZ是支持本地圖片預(yù)覽的,需要我們提供一組selectedAssets,里面裝著PHAsset對象,如果我們處于新建資源頁面時,這完全沒有問題;一旦我們手動存草稿,進行二次編輯時,就會出現(xiàn)問題,原因就是PHAsset不遵守NSCoding協(xié)議,無法進行歸檔。解決方案其實就是儲存PHAsset的localIdentifier即可。通過localIdentifier獲取PHAsset代碼如下:

    /// 獲取PHAsset
    PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[file.localIdentifier] options:nil];
    PHAsset *asset = fetchResult.firstObject;
    if (!asset) {
      // 這種場景就是這張照片儲存完P(guān)HAsset以后,但用戶在手機上把這張照片刪除
    }
  • 資源(圖片,視頻)處理

    常規(guī)邏輯:第一步,通過TZ從系統(tǒng)相冊中獲取一組資源(圖片、視頻)文件,第二步,遍歷資源列表根據(jù)PHAsset去獲取原圖數(shù)據(jù)或壓縮視頻,第三步將處理過的資源保存到Cache/Upload文件夾中?雌饋碓摲桨缚此品(wěn)如藏獒,但是實際情況第二步、第三步操作,其實是非常耗內(nèi)存的,而且每次獲取系統(tǒng)相冊中同一個的資源(PHAsset),第二步、第三步處理過后都是一樣的,如果該資源(PHAsset)之前已經(jīng)通過第二步、第三步處理過,那么后面在使用到該資源是不是完全沒有必要進行第二步和第三步操作,所以這里就必須用到數(shù)據(jù)緩存(磁盤緩存+內(nèi)存緩存)。  最終方案如下:

     

從上圖明顯可知,只有兩種場景才會去執(zhí)行第二步、第三步處理,且都是由于不存在磁盤中導(dǎo)致的。這里有一個比較細節(jié)的地方:緩存相對路徑。千萬不要緩存絕對路徑,因為隨著APP的更新或重裝,都會導(dǎo)致應(yīng)用的沙盒的絕對路徑是會改變的。

實現(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 : 這里可能會涉及到選中多個視頻的情況,且需要壓縮視頻的情況
  [MBProgressHUD mh_showProgressHUD:@"正在處理資源..." addedToView:self.view];

  NSLog(@"Compress Source Complete Before %@ !!!!" , [NSDate date]);

  /// 獲取隊列組
  dispatch_group_t group = dispatch_group_create();
  /// 創(chuàng)建信號量 用于線程同步
  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{
              //// 視頻和圖片,需要緩存,這樣會明顯減緩,應(yīng)用的內(nèi)存壓力
              /// 是否已經(jīng)緩存在沙盒
              BOOL isExistCache = NO;

              /// 1. 先去緩存里面去取
              NSString *filePath = (NSString *)[[YYCache sharedCache] objectForKey:localIdentifier];
              /// 這里必須的判斷一下filePath是否為空! 以免拼接起來出現(xiàn)問題
              if (MHStringIsNotEmpty(filePath)) {
                  /// 2. 該路徑的本地資源是否存在, 拼接絕對路徑,filePath是相對路徑
                  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)存在磁盤中 ++++");
                  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)出完成,在這里寫上傳代碼,通過路徑或者通過NSData上傳
                          /// CoderMikeHe Fixed Bug :如果這樣寫[NSData dataWithContentsOfURL:xxxx]; 文件過大,會導(dǎo)致內(nèi)存吃緊而閃退
                          /// 解決辦法,直接移動文件到指定目錄《類似剪切》
                          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);
                          /// 信號量+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;
                              /// 設(shè)置文件類型
                              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);
              }
          }
      });
  }

  /// 所有任務(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];
  });
}

后臺接口

這里分享一下筆者在實際項目中用到的后臺提供斷點續(xù)傳的接口,因為項目中部分邏輯處理是根據(jù)后臺提供的數(shù)據(jù)來的。這里筆者簡單分析一下各個接口的使用場景。

  • 預(yù)加載獲取文件ID(/fileSection/preLoad.do)

    使用場景:根據(jù)當(dāng)次上傳的文件數(shù)量,預(yù)先分配好文件ID,APP終端需要做好保存與文件的對應(yīng)關(guān)系,在續(xù)傳文件時候作為參數(shù)傳遞。

    請求URL: http://uadmin.xxxx.cn/fileSection/preLoad.do  (POST)

    Preload.png

  • 斷點續(xù)傳文件(/fileSection/upload.do)

    使用場景:大文件分片并行上傳。

    請求URL: http://uadmin.xxxx.cn/fileSection/upload.do  (POST)

    Upload.png

  • 刪除文件(/fileSection/delete.do)

    使用場景:在App手動刪除草稿時同時刪除已上傳到服務(wù)器的文件。

    請求URL: http://uadmin.xxxx.cn/fileSection/delete.do  (POST)

    Delete.png

  • 檢查文件是否上傳完畢(/fileSection/isFinish.do)

    使用場景:APP中該上傳資源的所有的文件片都上傳到服務(wù)器,服務(wù)器需要檢查這些文件片的合成情況。如果服務(wù)器合成失敗,即finishStatus = 0,服務(wù)器會把那些合成失敗的文件返回給APP,即failFileIds。APP需要根據(jù)failFileIds去回滾本地數(shù)據(jù)庫,然后繼續(xù)重傳失敗的文件片。

    請求URL: http://uadmin.xxxx.cn/fileSection/isFinish.do  (POST)

    finish.png

文件分片

文件分片的過程主要是在用戶點擊提交資源的過程。具體內(nèi)容和細節(jié)還請查看CMHSource.h/m 、CMHFile.h/m、CMHFileSource.h/m、CMHFileBlock.h/m、CMHFileFragment.h/m的實現(xiàn)。

首先,這里需要將新建資源CMHSource 轉(zhuǎn)成上傳資源CMHFileSource,以及將新建資源的文件列表NSArray *files轉(zhuǎn)成上傳資源的文件塊列表NSArray *fileBlocks。

其次,需要根據(jù)新建資源的文件列表NSArray *files的個數(shù),即files.count,去調(diào)用后臺提供的預(yù)加載獲取文件ID(/fileSection/preLoad.do)接口,去獲取文件ID列表,從而為文件列表NSArray *files中每一個文件(CMHFile)綁定文件ID,然后將CMHFile列表轉(zhuǎn)成CMHFileBlock列表,以及將新建資源CMHSource 轉(zhuǎn)成上傳資源CMHFileSource。 關(guān)鍵代碼如下:

- (void)commitSource:(void (^)(BOOL))complete{

/// 1. 通過要上傳的文件個數(shù)  去服務(wù)器獲取對應(yīng)的文件ID
NSInteger uploadFileCount = self.files.count;

/// 2. 以下通過真實的網(wǎng)絡(luò)請求去模擬獲取 文件ID的場景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1
/// 類似于實際開發(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ā)起請求
[[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id responseObject) {
    /// - 如果到這里了就認(rèn)為獲取文件ID成功,這里模擬后臺返回的數(shù)據(jù) 有幾個上傳文件 就對應(yīng)幾個上傳文件ID
    NSMutableArray *fileIds = [NSMutableArray arrayWithCapacity:uploadFileCount];
    for (NSInteger i = 0; i < uploadFileCount; i++) {
        NSString *fileId = [self _cmh_fileKey];
        [fileIds addObject:fileId];
    }
    /// - 為每個上傳文件綁定服務(wù)器返回的文件ID,獲取要上傳的文件塊列表
    /// 將服務(wù)器文件ID列表轉(zhuǎn)換為,轉(zhuǎn)成json字符串,后期需要存數(shù)據(jù)庫,這個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 ,因為這是異步,會導(dǎo)致 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) {
    /// 回調(diào)錯誤
    !complete ? : complete(NO);
    /// show error
    [MBProgressHUD mh_showErrorTips:error];
}];
}

然后,我們需要將文件塊CMHFileBlock按照512k的大小切割成多個文件片CMHFileFragment,這里的代碼實現(xiàn)和屬性生成都是參照這篇文章 HTTP斷點續(xù)傳與斷點上傳之 -- 文件流操作 來實現(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;
}

最后,我們知道一份上傳資源由多個文件塊組成,而一個文件塊由多個文件片組成。所以我們是不是可以這樣理解:一份上傳資源由多個文件片組成。前提是要保證每一個文件片,必須含有兩個屬性sourceId和fileId。

sourceId : 代表這個文件片所屬于哪個資源。

fileId : 代表這個文件片所屬于哪個文件塊。

一份上傳資源由多個文件片組成的代碼實現(xiàn),無非就是重寫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ù)庫即可。

分片上傳

分片上傳是本Demo中一個比較重要的功能點,但其實功能點并不難,主要復(fù)雜的還是業(yè)務(wù)邏輯以及數(shù)據(jù)庫處理。分片上傳,其原理還是文件上傳,某個文件片的上傳和我們平時上傳頭像的邏輯一模一樣,不同點無非就是我們需要利用數(shù)據(jù)庫去記錄每一片的上傳狀態(tài)罷了。詳情請參考:CMHFileUploadManager.h/m

這里筆者以CMHFileUploadManager上傳某個資源為例,具體講講其中的邏輯以及細節(jié)處理。具體的代碼實現(xiàn)請參考:- (void)uploadSource:(NSString *)sourceId;的實現(xiàn)。注 意:筆者提供的Demo,一次只能上傳一個資源。 關(guān)于具體的業(yè)務(wù)邏輯分析,筆者已經(jīng)寫在寫在代碼注釋里面了,這里就不再贅述,還請結(jié)合代碼注釋去理解具體的業(yè)務(wù)邏輯和場景。關(guān)鍵代碼如下:

/// 上傳資源 
 
   <核心方法>
  
    
   
    
 
   
   
    
- (void)uploadSource:(NSString *)sourceId{

    if (!MHStringIsNotEmpty(sourceId)) { return; }

    /// CoderMikeHe Fixed Bug : 解決初次加載的問題,不需要驗證網(wǎng)絡(luò)
    if (self.isLoaded) {
        if (![AFNetworkReachabilityManager sharedManager].isReachable) { /// 沒有網(wǎng)絡(luò)
            [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];

            /// 沒有資源,則何須上傳資源,將數(shù)據(jù)庫里面清掉
            [CMHFileSource removeFileSourceFromDB:sourceId complete:NULL];
            /// 通知草稿頁 刪除詞條數(shù)據(jù)
            [[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/*延遲執(zhí)行時間*/ * NSEC_PER_SEC));
            dispatch_after(delayTime, dispatch_get_main_queue(), ^{
                /// 檢查服務(wù)器的文件上傳合成狀態(tài)
                [self _checkFileFragmentSynthetiseStatusFromService:sourceId];
            });
        }else{
            /// 到了這里,則證明這個草稿永遠都不會上傳成功了,這里很遺憾則需要將其從數(shù)據(jù)庫中移除
            /// 提交資源
            [self _autoUploadSource:sourceId reUpload:NO];

            [CMHSource removeSourceFromDB:sourceId complete:NULL];
            /// 通知草稿頁 刪除這條數(shù)據(jù)
            [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}];
        }
        return;
    }


    /// 0. 這里一定會新建一個新的上傳隊列,一定會開啟一個新的任務(wù)
    /// - 看是否存在于上傳數(shù)組中
    NSString *findSid = nil;
    /// - 是否有文件正在上傳
    BOOL isUploading = NO;

    for (NSString *sid in self.uploadFileArray) {
        /// 上傳資源里面已經(jīng)存在了,findSid
        if ([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個元素
        [self.uploadFileArray removeObject:findSid];
        [self.uploadFileArray insertObject:sourceId atIndex:0];
    }else{ /// 不存在上傳資源數(shù)組中,直接插入到第0個元素
        [self.uploadFileArray insertObject:sourceId atIndex:0];
    }

    /// 3. 檢查是否已經(jīng)有上傳任務(wù)了
    if (isUploading) { /// 已經(jīng)有正在上傳任務(wù)了,則不需要開啟隊列了,就請繼續(xù)等待
        /// 發(fā)送通知
        [self postFileUploadStatusDidChangedNotification:sourceId];
        return;
    }
    /// 4. 如果沒有上傳任務(wù),你就創(chuàng)建隊里開啟任務(wù)即可

    /// 更新這個上傳文件的狀態(tài) 為 `正在上傳的狀態(tài)`
    [self updateUpLoadStatus:CMHFileUploadStatusUploading sourceId:sourceId];

    /// 創(chuàng)建信號量 用于線程同步
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    /// 創(chuàng)建一個隊列組
    dispatch_group_t group = dispatch_group_create();
    /// 操作數(shù)
    NSMutableArray *operations = [NSMutableArray array];

    /// 這里采用串行隊列且串行請求的方式處理每一片的上傳
    for (CMHFileFragment *ff in uploadFileFragments) {
        /// 進組
        dispatch_group_enter(group);
        // 創(chuàng)建對象,封裝操作
        NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{

            /// 切記:任務(wù)(網(wǎng)絡(luò)請求)是串行執(zhí)行的 ,但網(wǎng)絡(luò)請求結(jié)果回調(diào)是異步的、
            [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) {
                                  /// 更新數(shù)據(jù)
                                  /// 某片上傳失敗
                                  [ff updateFileFragmentUploadStatus:CMHFileUploadStatusWaiting];
                                  /// 退組
                                  dispatch_group_leave(group);
                                  /// 信號量+1 向下運行
                                  dispatch_semaphore_signal(semaphore);

                              }];
            /// 等待
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        }];
        /// 添加操作數(shù)組
        [operations addObject:operation];
    }
    /// 創(chuàng)建NSOperationQueue
    CMHFileUploadQueue * uploadFileQueue = [[CMHFileUploadQueue alloc] init];
    /// 存起來
    [self.uploadFileQueueDict setObject:uploadFileQueue forKey:sourceId];
    /// 把操作添加到隊列中 不需要設(shè)置為等待
    [uploadFileQueue addOperations:operations waitUntilFinished:NO];

    /// 隊列組的操作全部完成
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"+++dispatch_group_notify+++");
        /// 0. 如果運行到這,證明此`Queue`里面的所有操作都已經(jīng)全部完成了,你如果再使用 [queue setSuspended:YES/NO];將沒有任何意義,所以你必須將其移除掉
        [self.uploadFileQueueDict removeObjectForKey:sourceId];
        /// 1. 隊列完畢了,清除掉當(dāng)前的資源,開啟下一個資源
        [self _removeSourceFromUploadFileArray:sourceId];
        /// CoderMikeHe: 這里先不更新草稿頁的狀態(tài),等提交完表格再去發(fā)送通知
        /// 檢查一下資源上傳
        [self _uploadSourceEnd:sourceId];
    });

    //// 告知外界其資源狀態(tài)改過了
    [self postFileUploadStatusDidChangedNotification:sourceId];
}

這里對上傳資源下的需要上傳的文件片做了循環(huán)的上傳,由于網(wǎng)絡(luò)請求是一個異步的操作,同時也考慮到太多并發(fā)(當(dāng)然系統(tǒng)對于網(wǎng)絡(luò)請求開辟的線程個數(shù)也有限制)對于手機性能的影響,因此利用GCD信號量等待這種功能特性讓一個片段上傳完之后再進行下一個片段的上傳。

文件上傳核心代碼如下:

/// 上傳某一片文件 這里用作測試
- (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ù)庫,無論如何也得讓用戶把資源提交上去,而不是讓其永遠卡在草稿頁里,這樣太影響用戶體驗了
    if (fileData == nil) {
        /// CoderMikeHe Fixed Bug : V1.6.7之前 修復(fù)文件丟失的情況
        /// 1. 獲取該片所處的資源
        CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:fileFragment.sourceId];
        /// 取出fileID
        NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];

        NSLog(@" Before -- 文件<%@>未找到個數(shù) %ld <%@> "
 
   
   
    
 
   
   
    ,fileFragment.fileId , fileIds.count, fileIds);
        if ([fileIds containsObject:fileFragment.fileId]) {
            /// 數(shù)據(jù)庫包含
            [fileIds removeObject:fileFragment.fileId];
            uploadSource.fileIds = fileIds.yy_modelToJSONString;
            /// 更新數(shù)據(jù)庫
            [uploadSource saveOrUpdate];
        }
        NSLog(@" After -- 文件<%@>未找到個數(shù) %ld <%@> "
 
   
   
    
 
   
   
    ,fileFragment.fileId , fileIds.count, fileIds);

        /// 一定要回調(diào)為成功,讓用戶誤以為正在上傳,而不是直接卡死在草稿頁
        NSDictionary *responseObj = @{@"code" : @200};
        !success ? : success(responseObj);
        return;
    }

    /// 這里筆者只是模擬一下網(wǎng)絡(luò)情況哈,不要在乎這些細節(jié) ,
    /// 類似于實際開發(fā)中調(diào)用服務(wù)器的API:  /fileSection/upload.do
    /// 2. 以下通過真實的網(wǎng)絡(luò)請求去模擬獲取 文件ID的場景 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ā)起請求
    [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id  _Nullable responseObject) {
#warning CMH TODO 稍微延遲一下,模擬現(xià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
    /// 這個是真實上傳,請根據(jù)自身實際項目出發(fā)  /fileSection/upload.do
    [self _uploadFileFragmentWithParameters:parameters
                                   fileType:fileFragment.fileType
                                   fileData:fileData
                                   fileName:fileFragment.fileName
                                   progress:uploadProgress
                                    success:success
                                    failure:failure];
#endif

}


/// 實際開發(fā)項目中上傳每一片文件,這里請結(jié)合自身項目開發(fā)去設(shè)計
- (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{
    /// 配置成服務(wù)器想要的樣式
    NSMutableArray *paramsArray = [NSMutableArray array];
    [paramsArray addObject:parameters];

    /// 生成jsonString
    NSString *jsonStr = [paramsArray yy_modelToJSONString];

    /// 設(shè)置TTPHeaderField
    [self.uploadService.requestSerializer setValue:jsonStr forHTTPHeaderField:@"file_block"];

    /// 開啟文件任務(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),以這個為標(biāo)準(zhǔn)
    CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:sourceId];
    /// 沒意義
    if (uploadSource == nil) { return; }

    /// 如果這里進來了,則證明準(zhǔn)備驗證文件片和提交表單,則草稿里面的這塊表單,你不能在讓用戶去點擊了
    [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES];

    /// V1.6.5之前的接口老數(shù)據(jù)
    if (!MHStringIsNotEmpty(uploadSource.fileIds)) {
        /// 這里可能是老數(shù)據(jù),直接認(rèn)為成功,就不要去跟服務(wù)器打交道了
        /// 成功
        [self _commitSource:sourceId];
        /// 上傳下一個
        [self _autoUploadSource:sourceId reUpload:NO];
        return;
    }
    /// 這里筆者只是模擬一下網(wǎng)絡(luò)情況哈,不要在乎這些細節(jié),
    /// 類似于實際開發(fā)中調(diào)用服務(wù)器的API:  /fileSection/isFinish.do
    /// 2. 以下通過真實的網(wǎng)絡(luò)請求去模擬獲取 文件ID的場景 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ā)起請求
    [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id  _Nullable responseObject) {

        /// 模擬后臺返回的合成結(jié)果
        CMHFileSynthetise *fs = [[CMHFileSynthetise alloc] init];
        NSInteger randomNum = [NSObject mh_randomNumber:0 to:20];
        fs.finishStatus = (randomNum > 0) ? 1 : 0;  /// 模擬服務(wù)器合成失敗的場景,畢竟合成失敗的幾率很低

        if (fs.finishStatus>0) {
            /// 服務(wù)器合成資源文件成功
            /// 成功
            [self _commitSource:sourceId];
            /// 上傳下一個
            [self _autoUploadSource:sourceId reUpload:NO];
            return ;
        }

        /// 服務(wù)器合成資源文件失敗, 服務(wù)器會把合成失敗的 fileId 返回出來
        /// 也就是 "failFileIds" : "fileId0,fileId1,..."的格式返回出來
        /// 這里模擬后臺返回合成錯誤的文件ID, 這里只是演習(xí)!這里只是演習(xí)!
        /// 取出fileID
        NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];
        /// 模擬只有一個文件ID合成失敗
        NSString *failFileIds = fileIds.firstObject;
        fs.failFileIds = failFileIds;

        /// 這里才是模擬真實的網(wǎng)絡(luò)情況
        if (MHStringIsNotEmpty(fs.failFileIds)) {
            /// 1. 回滾數(shù)據(jù)
            [uploadSource rollbackFailureFile:fs.failureFileIds];
            /// 2. 獲取進度
            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{
            /// 無需回滾,修改狀態(tài)即可
            [self postFileUploadStatusDidChangedNotification:sourceId];
        }

        /// 合成失敗,繼續(xù)重傳失敗的片,允許用戶點擊草稿頁的資源
        [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO];
        /// 重傳該資源
        [self _autoUploadSource:sourceId reUpload:YES];

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {
        /// 1. 服務(wù)器報錯不重傳
        [MBProgressHUD mh_showErrorTips:error];

        /// 更新資源狀態(tài)
        [self updateUpLoadStatus:CMHFileUploadStatusWaiting sourceId:sourceId];

        /// 更新狀態(tài)
        [self postFileUploadStatusDidChangedNotification:sourceId];
        /// 文件片合成失敗,允許點擊
        [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO];
    }];
}

總之,文件分片上傳邏輯不止上面這一點點內(nèi)容,還有存在許多邏輯處理和細節(jié)注意,比如暫停上傳資源;開始上傳資源;取消上傳資源;取消所有上傳資源;服務(wù)器合成某些文件失敗,客戶端回滾數(shù)據(jù)庫,重傳失敗的文件片;某個資源上傳后自動重傳下個資源....等等。大家有興趣可以查看CMHFileUploadManager.h提供的API的具體實現(xiàn)。 CMHFileUploadManager.h的所有內(nèi)容如下:

/// 某資源的所有片數(shù)據(jù)上傳,完成也就是提交資源到服務(wù)器成功。
FOUNDATION_EXTERN NSString *const CMHFileUploadDidFinishedNotification;
/// 資源文件上傳狀態(tài)改變的通知
FOUNDATION_EXTERN NSString *const CMHFileUploadStatusDidChangedNotification;

/// 草稿上傳文件狀態(tài) disable 是否不能點擊 如果為YES 不要修改草稿頁表單的上傳狀態(tài) 主需要讓用戶不允許點擊上傳按鈕
FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusKey;
FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusNotification;

/// 某資源中的某片數(shù)據(jù)上傳完成
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;

/// 基礎(chǔ)配置,主要是后臺上傳草稿數(shù)據(jù)  一般這個方法會放在 程序啟動后切換到主頁時調(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;

/// 取消掉所有上傳 一般這個方法會放在 程序啟動后切換到登錄頁時調(diào)用
- (void)cancelAllUpload;

/// 刪除當(dāng)前用戶無效的資源
- (void)clearInvalidDiskCache;

//// 以下方法跟服務(wù)器交互,只管調(diào)用即可,無需回調(diào),
/// 清除掉已經(jīng)上傳到服務(wù)器的文件片 fileSection
- (void)deleteUploadedFile:(NSString *)sourceId;

/// 告知草稿頁,某個資源的上傳狀態(tài)改變
/// sourceId -- 資源ID
- (void)postFileUploadStatusDidChangedNotification:(NSString *)sourceId;
/// 告知草稿頁,某個資源不允許點擊
- (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)容,就是筆者在做大文件分片上傳的過程中的心得體會?此坪唵蔚奈募制蟼鞴δ,但其中涵蓋的知識面還是比較廣的,結(jié)合筆者前面談及的必備知識點,大家業(yè)余時間可以系統(tǒng)去學(xué)習(xí)和掌握,最后筆者還是建議大家把多線程的相關(guān)知識惡補一下和實踐起來。當(dāng)然這其中肯定還有一些細小的邏輯和細節(jié)問題還未暴露出來,如果大家在使用和查看過程中發(fā)現(xiàn)問題或者不理解的地方,以及如果有好的建議或意見都可以指出。

期待

  1. 文章若對您有點幫助,請給個喜歡:heart:,畢竟碼字不易;若對您沒啥幫助,請給點建議,切記學(xué)無止境。

  2. 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。

  3. GitHub地址: https://github.com/CoderMikeHe

  4. 源碼地址:

    MHDevelopExample目錄中的Architecture/Contacts/FileUpload文件夾中 <特別強調(diào) 0="" cmhdebug="">

 

來自:http://www.cocoachina.com/ios/20180822/24662.html

 

標(biāo)簽: idc isp seo ssd 安全 代碼 服務(wù)器 數(shù)據(jù)庫 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:SpringBoot | 第十三章:測試相關(guān)(單元測試、性能測試)

下一篇:數(shù)據(jù)埋點太難!知乎的做法有何可借鑒之處?