iOS开发攻城狮的集散地iOS开发你需要知道的iOS Developer

iOS 利用AFNetworking实现大文件分片上传

2018-08-16  本文已影响176人  CoderMikeHe
概述

一说到文件上传,想必大家都并不陌生,更何况是利用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
知识点

虽然分片上传的原理看似非常简单,但是落实到具体的实现,其中还是具有非常多的细节分析和逻辑处理,而且都是我们开发中不常用到的知识点,这里笔者就总结了一下分片上传所用到的知识点和使用场景,以及借助一些第三方框架,来达到分片上传的目的。

模块

关于笔者在Demo中提供的文件分片上传的示例程序,虽然不够华丽,但麻雀虽小,五脏俱全,大家凑合着看咯。但总的来说,可以简单分为以下几个模块:

资源新建

资源新建模块的UI搭建,笔者这里就不过多赘述,这里更多讨论的是功能逻辑和细节处理。具体内容还请查看CMHCreateSourceController.h/m

从上图👆明显可知,只有两种场景才会去执行第二步、第三步处理,且都是由于不存在磁盘中导致的。这里有一个比较细节的地方:缓存相对路径。千万不要缓存绝对路径,因为随着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];
  });
}
后台接口

这里分享一下笔者在实际项目中用到的后台提供断点续传的接口,因为项目中部分逻辑处理是根据后台提供的数据来的。这里笔者简单分析一下各个接口的使用场景。

分片上传

分片上传是本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
总结

以上内容,就是笔者在做大文件分片上传的过程中的心得体会。看似简单的文件分片上传功能,但其中涵盖的知识面还是比较广的,结合笔者前面谈及的必备知识点,大家业余时间可以系统去学习和掌握,最后笔者还是建议大家把多线程的相关知识恶补一下和实践起来。当然这其中肯定还有一些细小的逻辑和细节问题还未暴露出来,如果大家在使用和查看过程中发现问题或者不理解的地方,以及如果有好的建议或意见都可以在底部👇评论区指出。

期待
  1. 文章若对您有点帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
  2. 针对文章所述内容,阅读期间任何疑问;请在文章底部批评指正,我会火速解决和修正问题。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源码地址:
    MHDevelopExample目录中的Architecture/Contacts/FileUpload文件夹中 <特别强调: 使用前请全局搜索 CMHDEBUG 字段并将该置为 1即可,默认是0 >
上一篇下一篇

猜你喜欢

热点阅读