美文网首页iOS知识收录PerhapYs的OC学习日记iOS技术点
AFNetworking 3 批量上传图片的 3 种方法

AFNetworking 3 批量上传图片的 3 种方法

作者: 戴仓薯 | 来源:发表于2016-05-14 19:08 被阅读16246次

    AFNetworking 在去年年底升级到了 3.0。这个版本更新想必有很多好处,然而让我吃惊的是,它并没有 batch request 接口。之前的 1.x 版本、2.x 版本都实现了这个很常见的需求,不知道作者为何选择在 3.x 中去掉它。

    在 AFNetworking 2 中,我们只需一行代码就能解决批量上传的问题:

    [AFURLConnectionOperation batchOfRequestOperations:operations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
        NSLog(@"%lu 上传完成,共 %lu", (long)numberOfFinishedOperations, (long)totalNumberOfOperations);
    } completionBlock:^(NSArray *operations) {
        NSLog(@"上传完毕");   
    }];
    

    但 AFNetworking 3 用的是NSURLSession,而不是用NSOperation来包装NSURLConnection,所以把整个AFURLConnectionOperation类都干掉了。上面的方法不能再用,并且也没有给出替代品。因此,我们只能自己动手了。

    实现这个功能,有几个要点:

    1. 异步上传。批量请求里的每个请求都应该在不同线程,可以同时上传。
    2. 在所有请求都完成之后,再通知回调。
    3. 尽管异步请求的返回先后顺序没有一定,很可能后发出的请求先返回;但是最后回调的时候,请求返回的结果必须要按请求发出的顺序排列。比如,一个很常见的处理是,上传图片的接口返回该图片的 url;那么回调结果里的 url 顺序显然需要跟上传的图片顺序一一对应上。
    4. 最好传完每张图片也能有一个回调,方便我们告诉用户上传的进度。

    同时满足以上要点,主要有3种思路:GCD、NSOperation 以及 promise。这个需求也是示例多线程用法的一个很好的例子,所以我写了这篇比较详细的文章供大家参考。

    下面的代码以图片上传为例。测试数据配置了 3 张图片,其中第 2 张图片尺寸最小,往往先上传完毕,用来测试请求发出顺序与返回顺序不一致的情况。

    测试图片之一

    方法一:GCD dispatch group

    我们知道,GCD dispatch 是多线程处理最简单的方法。全部请求完成后再通知回调的需求,很适合利用 dispatch group 来完成。至于保证返回结果的顺序,我们只好自己来做了。

    首先需要一个方法,对于每张图片生成一个上传请求。

    - (NSURLSessionUploadTask*)uploadTaskWithImage:(UIImage*)image completion:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionBlock {
        // 构造 NSURLRequest
        NSError* error = NULL;
        NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[self uploadUrl] parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
            NSData* imageData = UIImageJPEGRepresentation(image, 1.0);
            [formData appendPartWithFileData:imageData name:@"file" fileName:@"someFileName" mimeType:@"multipart/form-data"];
        } error:&error];
        
        // 可在此处配置验证信息
    
        // 将 NSURLRequest 与 completionBlock 包装为 NSURLSessionUploadTask
        AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:^(NSProgress * _Nonnull uploadProgress) {
        } completionHandler:completionBlock];
        
        return uploadTask;
    }
    

    在这个方法里,我们首先用UIImageJPEGRepresentationUIImage变为NSData。然后用AFHTTPRequestSerializer来生成NSMutableURLRequest[self uploadUrl]是上传接口的地址。为安全考虑,一般上传的接口都有身份验证的需求,比如在请求 header 中加入 auth 信息,可以在此配置NSMutableURLRequest的 header。最后,我们用 AFURLSessionManagerNSURLRequest 和回调 block 包装成一个NSURLSessionUploadTask

    有了生成请求的方法,批量发出请求的方法如下:

    
    - (IBAction)runDispatchTest:(id)sender {
        // 需要上传的数据
        NSArray* images = [self images];
        
        // 准备保存结果的数组,元素个数与上传的图片个数相同,先用 NSNull 占位
        NSMutableArray* result = [NSMutableArray array];
        for (UIImage* image in images) {
            [result addObject:[NSNull null]];
        }
        
        dispatch_group_t group = dispatch_group_create();
        
        for (NSInteger i = 0; i < images.count; i++) {
            
            dispatch_group_enter(group);
    
            NSURLSessionUploadTask* uploadTask = [self uploadTaskWithImage:images[i] completion:^(NSURLResponse *response, NSDictionary* responseObject, NSError *error) {
                if (error) {
                    NSLog(@"第 %d 张图片上传失败: %@", (int)i + 1, error);
                    dispatch_group_leave(group);
                } else {
                    NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, responseObject);
                    @synchronized (result) { // NSMutableArray 是线程不安全的,所以加个同步锁
                        result[i] = responseObject;
                    }
                    dispatch_group_leave(group);
                }
            }];
            [uploadTask resume];
        }
    
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"上传完成!");
            for (id response in result) {
                NSLog(@"%@", response);
            }
        });
    }
    

    可以看到,我们把所有请求放在一个 dispatch_group 里。首先用dispatch_group_create()来创建这个 group。然后,对于每一个 uploadTask,在创建之前先执行dispatch_group_enter(group),在结束回调的 block里执行dispatch_group_leave(group)。结束回调的代码放在dispatch_group_notify里即可。

    实际执行中,首先是所有 task 都进入 group,同时开始上传;上传完成之后依次离开 group;最后 group 空了会自动调用传入group_notify的回调,整个过程完成。

    那么如何把回调数据排成正确的顺序呢?借助 block 会保存自动变量的特点,我们让每个 task 的回调 block 都自动带上标志请求次序的变量 i,只需把返回结果填入数组的第 i 位即可。所以在开始请求之前,先创建好保存返回结果的数组,元素个数与请求个数相等,每个位置上用[NSNull null]占位。每个请求返回之后,把自己那个位置上的[NSNull null]替换成返回结果。全部请求返回之后,数组里保存的自然是按请求顺序排列的回调数据。

    这里注意,因为 NSMutableArray 是线程不安全的,而每个请求返回时是在不同线程操作同一个数组,所以我用@synchronized把操作数组的代码锁住了,锁的对象就用这个数组即可。这样保证所有线程执行到这一句都得串行,避免线程安全问题。

    一次测试结果如下:

    2016-05-13 15:49:43.042 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 2 张图片上传成功: {
        imageBucket = test;
        imageKey = "331eb245-741f-4fdc-8769-fdfb9e646da7";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/331eb245-741f-4fdc-8769-fdfb9e646da7?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:49:43.098 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 1 张图片上传成功: {
        imageBucket = test;
        imageKey = "d08f5370-c8b6-4912-b4e5-c73ea3134637";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/d08f5370-c8b6-4912-b4e5-c73ea3134637?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:49:43.120 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 3 张图片上传成功: {
        imageBucket = test;
        imageKey = "bdf13097-8128-4f04-bcbc-462bd2a728ab";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/bdf13097-8128-4f04-bcbc-462bd2a728ab?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:49:43.120 HAMAFNetworkingBatchRequestDemo[23102:5717076] 上传完成!
    2016-05-13 15:49:43.121 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "d08f5370-c8b6-4912-b4e5-c73ea3134637";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/d08f5370-c8b6-4912-b4e5-c73ea3134637?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:49:43.121 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "331eb245-741f-4fdc-8769-fdfb9e646da7";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/331eb245-741f-4fdc-8769-fdfb9e646da7?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:49:43.124 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "bdf13097-8128-4f04-bcbc-462bd2a728ab";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/bdf13097-8128-4f04-bcbc-462bd2a728ab?imageMogr2/thumbnail/640x";
    }
    

    可以看到,尽管第 2 张图片尺寸最小、最先传完,第 1 张图片后传完,但最后的结果顺序还是正确的。


    方法二:NSOperationQueue

    注意:这个方法有点问题,出在用 KVO 监听 task 的 state 的部分:看 AFN 源码可以看到它是在 task 的 didComplete 的 delegate 方法里执行 completionHandler 的,此时 task 的 state 已经变成 Completed。所以 KVO 有可能会略先一点执行,此时最后一个请求的 success block 可能还没执行。加一点点延时应该能解决问题……但这样不太严谨。我再想想有没有更合适的监听的东西。非常感谢王银博的 demo,帮我发现这个问题~

    能用 dispatch 实现的功能,自然也可以用NSOperationQueue。NSOperation 这一套比 dispatch 写起来要麻烦一些,不过有几个优点:

    1. NSOperation是对象,不像 dispatch 是 c 函数。这就意味着你可以继承它,可以给它加 category,在执行过程中也可以始终管理它,访问到它,查看它的状态等,不像 dispatch 是一撒手就够不着了。
    2. NSOperation执行的任务,执行过程中可以随时取消。dispatch 一经发出是无法取消的。
    3. NSOperationQueue可以限制最大并发数。假如队列里真有 100 个文件要传,开出 100 个线程反而会严重影响性能。NSOperationQueue可以很方便地设置maxConcurrentOperationCount。dispatch 也可以限制最大并发数(参考苹果的文档)不过写起来麻烦很多。

    就我们的需求而言,用 NSOperation 有一个很方便的特点:dispatch 里的任务各自为政,而NSOperation之前是可以有依赖关系的。我们就可以利用这一点,来发起所有任务上传完成后的回调:把这个完成回调也做成一个NSOperation,让这个NSOperation前置依赖所有上传的NSOperation,这样等到所有上传的NSOperation完成之后,这个回调NSOperation才会开始执行。

    然而,用NSOperation也有一个很不方便的特点:NSOperationQueue是用 KVO 观察NSOperation状态来判断任务是否已结束的。而我们请求用的NSURLSessionTask,它长得很像一个NSOperation,但却并不是NSOperation的子类。所以,这一套方法最麻烦的地方就在于我们需要写一个自定义的NSOperation子类,只是为了跟踪NSURLSessionTask的状态。

    自定义的NSOperation代码如下:

    HAMURLSessionWrapperOperation.h

    #import <Foundation/Foundation.h>
    
    @interface HAMURLSessionWrapperOperation : NSOperation
    
    + (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task;
    
    @end
    

    HAMURLSessionWrapperOperation.m

    #import "HAMURLSessionWrapperOperation.h"
    
    @interface HAMURLSessionWrapperOperation () {
        BOOL executing;  // 系统的 finished 是只读的,不能修改,所以只能重写一个。
        BOOL finished;
    }
    
    @property (nonatomic, strong) NSURLSessionTask* task;
    
    @property (nonatomic, assign) BOOL isObserving;
    
    @end
    
    @implementation HAMURLSessionWrapperOperation
    
    #pragma mark - Observe Task
    
    + (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task {
        HAMURLSessionWrapperOperation* operation = [HAMURLSessionWrapperOperation new];
        operation.task = task;
        return operation;
    }
    
    - (void)dealloc {
        [self stopObservingTask];
    }
    
    - (void)startObservingTask {
        @synchronized (self) {
            if (_isObserving) {
                return;
            }
            
            [_task addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew context:nil];
            _isObserving = YES;
        }
    }
    
    - (void)stopObservingTask { // 因为要在 dealloc 调,所以用下划线不用点语法
        @synchronized (self) {
            if (!_isObserving) {
                return;
            }
            
            _isObserving = NO;
            [_task removeObserver:self forKeyPath:@"state"];
        }
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if (self.task.state == NSURLSessionTaskStateCanceling || self.task.state == NSURLSessionTaskStateCompleted) {
            [self stopObservingTask];
            [self completeOperation];
        }
    }
    
    #pragma mark - NSOperation methods
    
    - (void)start {
        // Always check for cancellation before launching the task.
        if ([self isCancelled])
        {
            // Must move the operation to the finished state if it is canceled.
            [self willChangeValueForKey:@"isFinished"];
            finished = YES;
            [self didChangeValueForKey:@"isFinished"];
            return;
        }
        
        // If the operation is not canceled, begin executing the task.
        [self willChangeValueForKey:@"isExecuting"];
        [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
        executing = YES;
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)main {
        @try {
            [self startObservingTask];
            [self.task resume];
        }
        @catch (NSException * e) {
            NSLog(@"Exception %@", e);
        }
    }
    
    - (void)completeOperation {
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isExecuting"];
        
        executing = NO;
        finished = YES;
        
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
    }
    
    - (BOOL)isAsynchronous {
        return YES;
    }
    
    - (BOOL)isExecuting {
        return executing;
    }
    
    - (BOOL)isFinished {
        return finished;
    }
    
    @end
    

    代码有点长,但没办法。我们的目标是对每个NSURLSessionTask都包装出一个HAMURLSessionWrapperOperation,这个NSOperation完全随着NSURLSessionTask的状态而动,在 Task 结束之后发出 KVO 的通知,通知NSOperationQueue这个任务结束。

    系统NSOperationfinished属性是只读的,不能修改;为了记录值和发出 KVO 的通知,我们只能在旁再定义一个finished的成员变量,通过重写- (BOOL)isFinished等 getter 方法,盖掉原来的finished属性。现在几乎全用 property,这种成员变量的写法好久没看见过了,没想到还有这种用处,这种特殊的写法还是从苹果文档学来的(参考这里)。

    这里 start 方法照抄苹果文档,在新线程调起 main 方法。main 方法里就两件事:开始 KVO 观察上传 task 的 state 属性,然后启动 task。一旦 task 完成(或失败),接到 KVO 的通知,我们停止对 task 的观察,然后发出自己的 KVO 通知去通知NSOperationQueue。这里我们手动调起了[self willChangeValueForKey:@"isFinished"];[self didChangeValueForKey:@"isFinished"];,又重写了- (BOOL)isFinished方法,就把只读的finished属性偷天换日变成我们自己定义的finished成员变量了。

    自定义NSOperation说完了,下面我们来看看怎么使用这个类。我们同样要利用上面 dispatch 一节写的那个uploadTaskWithImage:completion方法,根据图片生成请求。发出请求的代码如下:

    - (IBAction)runNSOperationTest:(id)sender {
        // 需要上传的数据
        NSArray* images = [self images];
        
        // 准备保存结果的数组,元素个数与上传的图片个数相同,先用 NSNull 占位
        NSMutableArray* result = [NSMutableArray array];
        for (UIImage* image in images) {
            [result addObject:[NSNull null]];
        }
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        queue.maxConcurrentOperationCount = 5;
        
        NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 回到主线程执行,方便更新 UI 等
                NSLog(@"上传完成!");
                for (id response in result) {
                    NSLog(@"%@", response);
                }
            }];
        }];
        
        for (NSInteger i = 0; i < images.count; i++) {
            
            NSURLSessionUploadTask* uploadTask = [self uploadTaskWithImage:images[i] completion:^(NSURLResponse *response, NSDictionary* responseObject, NSError *error) {
                if (error) {
                    NSLog(@"第 %d 张图片上传失败: %@", (int)i + 1, error);
                } else {
                    NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, responseObject);
                    @synchronized (result) { // NSMutableArray 是线程不安全的,所以加个同步锁
                        result[i] = responseObject;
                    }
                }
            }];
            
            HAMURLSessionWrapperOperation *uploadOperation = [HAMURLSessionWrapperOperation operationWithURLSessionTask:uploadTask];
            
            [completionOperation addDependency:uploadOperation];
            [queue addOperation:uploadOperation];
        }
    
        [queue addOperation:completionOperation];
    }
    

    保持结果顺序的方法与 dispatch 相同,都是我们自己完成的。我们把maxConcurrentOperationCount定成 5,避免并发过多竞争资源。先创建结束回调的 operation,再让它依赖后面创建的每一个上传 operation。因为一般回调都要涉及到更新 UI,所以让它回到主线程执行。后面根据每张图片逐一创建 task、包装成 operation。创建好之后,加进 operationQueue 里就开始跑了。

    一次测试结果如下:

    2016-05-13 15:50:06.269 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 2 张图片上传成功: {
        imageBucket = test;
        imageKey = "cc60ab02-7745-4c60-8697-8bae1501768b";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/cc60ab02-7745-4c60-8697-8bae1501768b?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:50:06.365 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 1 张图片上传成功: {
        imageBucket = test;
        imageKey = "ee9c1492-a8f1-441c-9bd4-c90756841266";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ee9c1492-a8f1-441c-9bd4-c90756841266?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:50:06.413 HAMAFNetworkingBatchRequestDemo[23102:5717076] 第 3 张图片上传成功: {
        imageBucket = test;
        imageKey = "6fe8197a-4638-4706-afe1-3aca203cf73f";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/6fe8197a-4638-4706-afe1-3aca203cf73f?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:50:06.414 HAMAFNetworkingBatchRequestDemo[23102:5717076] 上传完成!
    2016-05-13 15:50:06.414 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "ee9c1492-a8f1-441c-9bd4-c90756841266";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ee9c1492-a8f1-441c-9bd4-c90756841266?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:50:06.415 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "cc60ab02-7745-4c60-8697-8bae1501768b";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/cc60ab02-7745-4c60-8697-8bae1501768b?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:50:06.415 HAMAFNetworkingBatchRequestDemo[23102:5717076] {
        imageBucket = test;
        imageKey = "6fe8197a-4638-4706-afe1-3aca203cf73f";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/6fe8197a-4638-4706-afe1-3aca203cf73f?imageMogr2/thumbnail/640x";
    }
    

    结果也是正确的。


    方法三:promise

    上面的两种方法,我们都是自己用数组、占位、逐位替换的方法,自己写代码保证返回数据顺序正确的。其实这种需要多个线程执行、全部结束后回调、结果顺序保证正确的需求,一般最适合用 promise 来做。各个语言都有自己的 promise 实现,iOS 也有好几种。这里我们试用一下 iOS 最著名的实现 PromiseKit

    在 github 上 5000 多个 star,这个 lib 是 Objective-C 、Swift 通用的,两套代码都有。在网络请求方面,它要依赖同一个作者写的另一个库 OMGHTTPURLRQ,导入的时候小费周折。PromiseKit 这一套方法与 AFNetworking 库就没关系了,可能有些离题,但是用起来是最为方便的。

    这里我们不再需要上面那个生成NSURLSessionTask的方法了,现在我们需要把NSURLRequest包装成AnyPromise

    - (AnyPromise *)uploadPromiseWithImage:(UIImage *)image completion:(id (^)(id))completionBlock {
        NSString* url = [self uploadUrl];
        
        NSData* imageData = UIImageJPEGRepresentation(image, 1.0);
        
        OMGMultipartFormData *multipartFormData = [OMGMultipartFormData new];
        [multipartFormData addFile:imageData parameterName:@"file" filename:@"someFileName" contentType:@"multipart/form-data"];
        NSMutableURLRequest* request = [OMGHTTPURLRQ POST:url :multipartFormData error:nil];
        
        // 可在此处配置验证信息
        
        if (completionBlock) {
            return [NSURLConnection promise:request].then(completionBlock);
        } else {
            return [NSURLConnection promise:request];
        }
    }
    

    这里可以看到 promise 的.then语法。它是一个 C 函数,传进的参数是这项 promise 完成之后下一步需要执行的 block,返回值仍然是AnyPromise,所以可以一直.then().then()……这样链式调用下去。我们在这里让它上传完单张图片之后执行单张图片的回调,把回调 block『附身』在上传的 promise 之后。

    上面就是创建 promise 的过程。那么执行 promise 的代码怎么写呢?

    - (IBAction)runPromiseTest:(id)sender {
        // 需要上传的数据
        NSArray* images = [self images];
        
        NSMutableArray* promises = [NSMutableArray array];
        for (NSInteger i = 0; i < images.count; i++) {
            UIImage* image = images[i];
            
            [promises addObject:[self uploadPromiseWithImage:image completion:^(id resultImageUrl){
                NSLog(@"第 %d 张图片上传成功: %@", (int)i + 1, resultImageUrl);
                return resultImageUrl;
            }]];
        }
        
        PMKWhen(promises).then(^(NSArray *results) {
            NSLog(@"上传完成!");
            NSLog(@"%@", results);
        }).catch(^{
            NSLog(@"图片上传失败");
        });
    }
    

    可以看到代码非常简洁,可读性又好,比前两种方法都省去不少代码,这是 promise 的一大优势。我们只需把针对每张图片创建一个 promise ,放进一个 promises 数组,然后PMKWhen(promises).then()就能帮我们搞定一切了——是不是很神奇呢?每个任务单开线程、等待全部任务执行完、结果正确排序等诸多工序,全都由这一行代码搞定了。看看测试结果:

    2016-05-13 15:30:45.447 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 2 张图片上传成功: {
        imageBucket = test;
        imageKey = "5d50cdd3-2272-4d3b-bbb1-054d1d08e682";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/5d50cdd3-2272-4d3b-bbb1-054d1d08e682?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:30:45.595 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 1 张图片上传成功: {
        imageBucket = test;
        imageKey = "ff3874d2-8477-4ceb-a49f-1938168b0456";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ff3874d2-8477-4ceb-a49f-1938168b0456?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:30:46.127 HAMAFNetworkingBatchRequestDemo[23093:5713564] 第 3 张图片上传成功: {
        imageBucket = test;
        imageKey = "2b8b0175-1274-4de9-b809-7d88809ef606";
        imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/2b8b0175-1274-4de9-b809-7d88809ef606?imageMogr2/thumbnail/640x";
    }
    2016-05-13 15:30:46.130 HAMAFNetworkingBatchRequestDemo[23093:5713564] 上传完成!
    2016-05-13 15:30:46.130 HAMAFNetworkingBatchRequestDemo[23093:5713564] (
            {
            imageBucket = test;
            imageKey = "ff3874d2-8477-4ceb-a49f-1938168b0456";
            imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/ff3874d2-8477-4ceb-a49f-1938168b0456?imageMogr2/thumbnail/640x";
        },
            {
            imageBucket = test;
            imageKey = "5d50cdd3-2272-4d3b-bbb1-054d1d08e682";
            imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/5d50cdd3-2272-4d3b-bbb1-054d1d08e682?imageMogr2/thumbnail/640x";
        },
            {
            imageBucket = test;
            imageKey = "2b8b0175-1274-4de9-b809-7d88809ef606";
            imageUrl = "http://7xnpgs.com1.z0.glb.clouddn.com/2b8b0175-1274-4de9-b809-7d88809ef606?imageMogr2/thumbnail/640x";
        }
    )
    

    同样是正确的。

    所以看起来用 promise 还是非常方便的。不过这是我第一次尝试用它,还不知道在工程中实际应用会有什么缺点。

    以上就是多线程批量上传图片的 3 种方法。思路最初来自 stackoverflow 的这个问题How to send batch request by using AFNetworking 3.0,感谢这位回答的大神~

    相关文章

      网友评论

      • 薰衣草儿:我用的你的 NSOperationQueue方法上传了50多张图片 好想创建task没有释放 上传完之后 内存很大
      • Link913:写得很不错,我一般都是用afn那个方法直接干的
      • 神秘嘉宾yzj:陆陆续续收到不少私信,针对25楼下面遇到的问题 发个demo。https://github.com/yzjzmn/UploadImages
      • f52f11462a9e:学习了 多谢楼主分享
      • 720841baa847:使用第一种方法,图片重复了。就是返回的url是不同,但是对应的图片是重复的。这个怎么解决?
      • 杨老师红烧肉:楼主有swift版本的demo吗能发一个吗,最近遇见这个问题很苦恼啊
      • HelloWorld_1986:楼主你好,我用了GCD的方法实现图片批量上传,但是上传失败的时候同一张图片会执行多次呢
      • linfantasy:还是不能保证 按照顺序发送的
      • 53c469a10b6e:session 自带operationqueue只要确保一个manager去调用upload函数应该就可以,我没试,只是源码那么写来着。:sweat:
      • Cedric_Jc:perfect~
      • 21750a9ece6a:刚好希望顺序上传数据 ,解决了我的需求 谢谢
      • hhgvg:请问下上传失败重新上传怎么处理呢
        戴仓薯:@hhgvg 我没想过这个问题。感觉用 GCD 应该希望不大,改造 NSOperation 应该可以,毕竟自定义的 NSOperation 想指定什么时候结束都行。如果失败的话,按自己的策略重新传,直到成功再改 state 让 operation 结束就可以了。
      • 酷酷的哀殿:可以封装到分类里面,让大家通过 CocoaPods 快速使用
      • 神秘嘉宾yzj:只有我一个人遇到bug吗,operation的方法可能会少一张图啊,意思就是 有可能出现 完成回调走完之后,某张图片才上传成功。uploadTask会慢于completion,本身completion是依赖于uploadTask的,但是会出现偶然bug
        神秘嘉宾yzj:@戴仓薯 这个问题不是100%必现的,调试这个废了我好长时间,最后实在找不到原因,然后换成GCD就好了
        神秘嘉宾yzj:@戴仓薯 难道只有我自己遇到这问题么,举个栗子,9张图上传,有8张成功了然后就直接completion了 然后那边最后一张图的uploadOperation也成功了
        戴仓薯:@神秘嘉宾yzj = = 我从来没遇到过呀。包装 uploadTask 的 operation 是 KVO 监听 task state 的。打印下 state 已经变成了 NSURLSessionTaskStateCompleted?
      • 437aff410919:作者想请教下,如果好像做相册这样的功能,一次传10张图片。用dispatch_group上传会10个线程并发上传,而且上传的时候还要能继续添加上传内容。内存会爆炸.目前想到的就是限制dispatch_group这种并发的线程数量,但是网上说用dispatch_semaphore_t这种的好像会把主线程也卡住了?对GCD这方面不是很熟,能请作者提供下思路吗?
        戴仓薯:@Tosaka乐园 啊,加我的群吧 546948320
        437aff410919:@戴仓薯 可以qq交流下吗?我现在用第二种方法,但是遇到一些很蛋疼的问题。希望能讨论下 :joy:
        戴仓薯:@Tosaka乐园 嗯用第二种方法 OperationQueue 可以限制最大并发数哈。
      • 微笑刺客Fly:您好,我现在使用了您的第一种方法,现在有个问题,只要是group里面的不按顺序上传就会报错,返回的responseObject就是为,请问一下您遇到过这种情况吗
      • f02559bba37a:貌似AFHTTPSessionManager里有方法可以解决批量上传 ,但与您第一种方法不同的是它应该不是多线程的。

        - (NSURLSessionDataTask *)POST:(NSString *)URLString
        parameters:(id)parameters
        constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
        success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
        failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
      • aeffb2f7ec39:第二种方法里面
        uploadOperation 不 start 能执行completionOperation么?
      • 中秋梧桐语:// 准备保存结果的数组,元素个数与上传的图片个数相同,先用 NSNull 占位
        NSMutableArray* result = [NSMutableArray array];
        for (UIImage* image in images) {
        [result addObject:[NSNull null]];
        }

        请问这里为什么要用NSNull来占位,有什么好处吗???
        中秋梧桐语:@戴仓薯 嗯 是为了请求成功后排序不变,起到占位的作用。
        戴仓薯:@夜聆纞尐珊 比如顺序应该排在第 2 位的请求先回来了,我怎么把它放在数组的第 2 位呢? iOS 里的数组,第 1 位上如果没有东西,是不能放到第 2 位上的。必须第 1 位上有东西占位,才能往第 2 位上放东西。所以我就用 NSNull 把所有位置都占上了。
      • adcd2641c136:刚刚是是github上的原话。。。
        戴仓薯:@重剑无锋_大巧不工 那要看后台接口怎么写的了。我的后台调的是七牛的接口,应该是比较常见的,一个 request 就是一张图片。而且像我文章开头说的,多个 request 可以多线程同步传呀。
        adcd2641c136:@戴仓薯 在这个地方,可以,
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"file://path/to/image.jpg"] name:@"file" fileName:@"filename.jpg" mimeType:@"image/jpeg" error:nil];
        多张图片,在这里for循环就行。
        戴仓薯:@重剑无锋_大巧不工 如果我没看错的话…… 这不是上传单张图片吗?这是一个 request 吧。
      • adcd2641c136:3.0可以批量上传啊,“Creating an Upload Task for a Multi-Part Request, with Progress

        NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload&quot; parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"file://path/to/image.jpg"] name:@"file" fileName:@"filename.jpg" mimeType:@"image/jpeg" error:nil];
        } error:nil];

        AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

        NSURLSessionUploadTask *uploadTask;
        uploadTask = [manager
        uploadTaskWithStreamedRequest:request
        progress:^(NSProgress * _Nonnull uploadProgress) {
        // This is not called back on the main queue.
        // You are responsible for dispatching to the main queue for UI updates
        dispatch_async(dispatch_get_main_queue(), ^{
        //Update the progress view
        [progressView setProgress:uploadProgress.fractionCompleted];
        });
        }
        completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
        if (error) {
        NSLog(@"Error: %@", error);
        } else {
        NSLog(@"%@ %@", response, responseObject);
        }
        }];

        [uploadTask resume];”
        linfantasy:@重剑无锋_大巧不工 你这个是请求一个接口 批量上传是指批量请求多个接口
      • GeekDmm:RAC 也阔以
        戴仓薯:@GeekDmm 噢噢对呀~ 忘了加上> <
      • newbiecoder:好好学习
      • 谢kun:赞!
      • 萧城x:第三种才是最好的
      • 我系哆啦:其实GCD还是最简单的
        戴仓薯:@我系哆啦 是的~~
      • 云逸枫林:最近正好有这个需求,:+1:,不错的思路
      • visual_:cool:+1:

      本文标题:AFNetworking 3 批量上传图片的 3 种方法

      本文链接:https://www.haomeiwen.com/subject/jpozkttx.html