AFNetworking 3 批量上傳圖片的 3 種方法

AFNetworking 在去年年底升級(jí)到了 3.0。這個(gè)版本更新想必有很多好處,然而讓我吃驚的是,它并沒(méi)有 batch request 接口。之前的 1.x 版本、2.x 版本都實(shí)現(xiàn)了這個(gè)很常見的需求,不知道作者為何選擇在 3.x 中去掉它。

在 AFNetworking 2 中,我們只需一行代碼就能解決批量上傳的問(wèn)題:

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

但 AFNetworking 3 用的是NSURLSession,而不是用NSOperation來(lái)包裝NSURLConnection,所以把整個(gè)AFURLConnectionOperation類都干掉了。上面的方法不能再用,并且也沒(méi)有給出替代品。因此,我們只能自己動(dòng)手了。

實(shí)現(xiàn)這個(gè)功能,有幾個(gè)要點(diǎn):

  1. 異步上傳。批量請(qǐng)求里的每個(gè)請(qǐng)求都應(yīng)該在不同線程,可以同時(shí)上傳。
  2. 在所有請(qǐng)求都完成之后,再通知回調(diào)。
  3. 盡管異步請(qǐng)求的返回先后順序沒(méi)有一定,很可能后發(fā)出的請(qǐng)求先返回;但是最后回調(diào)的時(shí)候,請(qǐng)求返回的結(jié)果必須要按請(qǐng)求發(fā)出的順序排列。比如,一個(gè)很常見的處理是,上傳圖片的接口返回該圖片的 url;那么回調(diào)結(jié)果里的 url 順序顯然需要跟上傳的圖片順序一一對(duì)應(yīng)上。
  4. 最好傳完每張圖片也能有一個(gè)回調(diào),方便我們告訴用戶上傳的進(jìn)度。

同時(shí)滿足以上要點(diǎn),主要有3種思路:GCD、NSOperation 以及 promise。這個(gè)需求也是示例多線程用法的一個(gè)很好的例子,所以我寫了這篇比較詳細(xì)的文章供大家參考。

下面的代碼以圖片上傳為例。測(cè)試數(shù)據(jù)配置了 3 張圖片,其中第 2 張圖片尺寸最小,往往先上傳完畢,用來(lái)測(cè)試請(qǐng)求發(fā)出順序與返回順序不一致的情況。

測(cè)試圖片之一

方法一:GCD dispatch group

我們知道,GCD dispatch 是多線程處理最簡(jiǎn)單的方法。全部請(qǐng)求完成后再通知回調(diào)的需求,很適合利用 dispatch group 來(lái)完成。至于保證返回結(jié)果的順序,我們只好自己來(lái)做了。

首先需要一個(gè)方法,對(duì)于每張圖片生成一個(gè)上傳請(qǐng)求。

- (NSURLSessionUploadTask*)uploadTaskWithImage:(UIImage*)image completion:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionBlock {
    // 構(gòu)造 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];
    
    // 可在此處配置驗(yàn)證信息

    // 將 NSURLRequest 與 completionBlock 包裝為 NSURLSessionUploadTask
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:^(NSProgress * _Nonnull uploadProgress) {
    } completionHandler:completionBlock];
    
    return uploadTask;
}

在這個(gè)方法里,我們首先用UIImageJPEGRepresentationUIImage變?yōu)?code>NSData。然后用AFHTTPRequestSerializer來(lái)生成NSMutableURLRequest[self uploadUrl]是上傳接口的地址。為安全考慮,一般上傳的接口都有身份驗(yàn)證的需求,比如在請(qǐng)求 header 中加入 auth 信息,可以在此配置NSMutableURLRequest的 header。最后,我們用 AFURLSessionManagerNSURLRequest 和回調(diào) block 包裝成一個(gè)NSURLSessionUploadTask。

有了生成請(qǐng)求的方法,批量發(fā)出請(qǐng)求的方法如下:


- (IBAction)runDispatchTest:(id)sender {
    // 需要上傳的數(shù)據(jù)
    NSArray* images = [self images];
    
    // 準(zhǔn)備保存結(jié)果的數(shù)組,元素個(gè)數(shù)與上傳的圖片個(gè)數(shù)相同,先用 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 是線程不安全的,所以加個(gè)同步鎖
                    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);
        }
    });
}

可以看到,我們把所有請(qǐng)求放在一個(gè) dispatch_group 里。首先用dispatch_group_create()來(lái)創(chuàng)建這個(gè) group。然后,對(duì)于每一個(gè) uploadTask,在創(chuàng)建之前先執(zhí)行dispatch_group_enter(group),在結(jié)束回調(diào)的 block里執(zhí)行dispatch_group_leave(group)。結(jié)束回調(diào)的代碼放在dispatch_group_notify里即可。

實(shí)際執(zhí)行中,首先是所有 task 都進(jìn)入 group,同時(shí)開始上傳;上傳完成之后依次離開 group;最后 group 空了會(huì)自動(dòng)調(diào)用傳入group_notify的回調(diào),整個(gè)過(guò)程完成。

那么如何把回調(diào)數(shù)據(jù)排成正確的順序呢?借助 block 會(huì)保存自動(dòng)變量的特點(diǎn),我們讓每個(gè) task 的回調(diào) block 都自動(dòng)帶上標(biāo)志請(qǐng)求次序的變量 i,只需把返回結(jié)果填入數(shù)組的第 i 位即可。所以在開始請(qǐng)求之前,先創(chuàng)建好保存返回結(jié)果的數(shù)組,元素個(gè)數(shù)與請(qǐng)求個(gè)數(shù)相等,每個(gè)位置上用[NSNull null]占位。每個(gè)請(qǐng)求返回之后,把自己那個(gè)位置上的[NSNull null]替換成返回結(jié)果。全部請(qǐng)求返回之后,數(shù)組里保存的自然是按請(qǐng)求順序排列的回調(diào)數(shù)據(jù)。

這里注意,因?yàn)?NSMutableArray 是線程不安全的,而每個(gè)請(qǐng)求返回時(shí)是在不同線程操作同一個(gè)數(shù)組,所以我用@synchronized把操作數(shù)組的代碼鎖住了,鎖的對(duì)象就用這個(gè)數(shù)組即可。這樣保證所有線程執(zhí)行到這一句都得串行,避免線程安全問(wèn)題。

一次測(cè)試結(jié)果如下:

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 張圖片后傳完,但最后的結(jié)果順序還是正確的。


方法二:NSOperationQueue

注意:這個(gè)方法有點(diǎn)問(wèn)題,出在用 KVO 監(jiān)聽 task 的 state 的部分:看 AFN 源碼可以看到它是在 task 的 didComplete 的 delegate 方法里執(zhí)行 completionHandler 的,此時(shí) task 的 state 已經(jīng)變成 Completed。所以 KVO 有可能會(huì)略先一點(diǎn)執(zhí)行,此時(shí)最后一個(gè)請(qǐng)求的 success block 可能還沒(méi)執(zhí)行。加一點(diǎn)點(diǎn)延時(shí)應(yīng)該能解決問(wèn)題……但這樣不太嚴(yán)謹(jǐn)。我再想想有沒(méi)有更合適的監(jiān)聽的東西。非常感謝王銀博的 demo,幫我發(fā)現(xiàn)這個(gè)問(wèn)題~

能用 dispatch 實(shí)現(xiàn)的功能,自然也可以用NSOperationQueue。NSOperation 這一套比 dispatch 寫起來(lái)要麻煩一些,不過(guò)有幾個(gè)優(yōu)點(diǎn):

  1. NSOperation是對(duì)象,不像 dispatch 是 c 函數(shù)。這就意味著你可以繼承它,可以給它加 category,在執(zhí)行過(guò)程中也可以始終管理它,訪問(wèn)到它,查看它的狀態(tài)等,不像 dispatch 是一撒手就夠不著了。
  2. NSOperation執(zhí)行的任務(wù),執(zhí)行過(guò)程中可以隨時(shí)取消。dispatch 一經(jīng)發(fā)出是無(wú)法取消的。
  3. NSOperationQueue可以限制最大并發(fā)數(shù)。假如隊(duì)列里真有 100 個(gè)文件要傳,開出 100 個(gè)線程反而會(huì)嚴(yán)重影響性能。NSOperationQueue可以很方便地設(shè)置maxConcurrentOperationCount。dispatch 也可以限制最大并發(fā)數(shù)(參考蘋果的文檔)不過(guò)寫起來(lái)麻煩很多。

就我們的需求而言,用 NSOperation 有一個(gè)很方便的特點(diǎn):dispatch 里的任務(wù)各自為政,而NSOperation之前是可以有依賴關(guān)系的。我們就可以利用這一點(diǎn),來(lái)發(fā)起所有任務(wù)上傳完成后的回調(diào):把這個(gè)完成回調(diào)也做成一個(gè)NSOperation,讓這個(gè)NSOperation前置依賴所有上傳的NSOperation,這樣等到所有上傳的NSOperation完成之后,這個(gè)回調(diào)NSOperation才會(huì)開始執(zhí)行。

然而,用NSOperation也有一個(gè)很不方便的特點(diǎn):NSOperationQueue是用 KVO 觀察NSOperation狀態(tài)來(lái)判斷任務(wù)是否已結(jié)束的。而我們請(qǐng)求用的NSURLSessionTask,它長(zhǎng)得很像一個(gè)NSOperation,但卻并不是NSOperation的子類。所以,這一套方法最麻煩的地方就在于我們需要寫一個(gè)自定義的NSOperation子類,只是為了跟蹤NSURLSessionTask的狀態(tài)。

自定義的NSOperation代碼如下:

HAMURLSessionWrapperOperation.h

#import <Foundation/Foundation.h>

@interface HAMURLSessionWrapperOperation : NSOperation

+ (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task;

@end

HAMURLSessionWrapperOperation.m

#import "HAMURLSessionWrapperOperation.h"

@interface HAMURLSessionWrapperOperation () {
    BOOL executing;  // 系統(tǒng)的 finished 是只讀的,不能修改,所以只能重寫一個(gè)。
    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 { // 因?yàn)橐?dealloc 調(diào),所以用下劃線不用點(diǎn)語(yǔ)法
    @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

代碼有點(diǎn)長(zhǎng),但沒(méi)辦法。我們的目標(biāo)是對(duì)每個(gè)NSURLSessionTask都包裝出一個(gè)HAMURLSessionWrapperOperation,這個(gè)NSOperation完全隨著NSURLSessionTask的狀態(tài)而動(dòng),在 Task 結(jié)束之后發(fā)出 KVO 的通知,通知NSOperationQueue這個(gè)任務(wù)結(jié)束。

系統(tǒng)NSOperationfinished屬性是只讀的,不能修改;為了記錄值和發(fā)出 KVO 的通知,我們只能在旁再定義一個(gè)finished的成員變量,通過(guò)重寫- (BOOL)isFinished等 getter 方法,蓋掉原來(lái)的finished屬性?,F(xiàn)在幾乎全用 property,這種成員變量的寫法好久沒(méi)看見過(guò)了,沒(méi)想到還有這種用處,這種特殊的寫法還是從蘋果文檔學(xué)來(lái)的(參考這里)。

這里 start 方法照抄蘋果文檔,在新線程調(diào)起 main 方法。main 方法里就兩件事:開始 KVO 觀察上傳 task 的 state 屬性,然后啟動(dòng) task。一旦 task 完成(或失?。?,接到 KVO 的通知,我們停止對(duì) task 的觀察,然后發(fā)出自己的 KVO 通知去通知NSOperationQueue。這里我們手動(dòng)調(diào)起了[self willChangeValueForKey:@"isFinished"];[self didChangeValueForKey:@"isFinished"];,又重寫了- (BOOL)isFinished方法,就把只讀的finished屬性偷天換日變成我們自己定義的finished成員變量了。

自定義NSOperation說(shuō)完了,下面我們來(lái)看看怎么使用這個(gè)類。我們同樣要利用上面 dispatch 一節(jié)寫的那個(gè)uploadTaskWithImage:completion方法,根據(jù)圖片生成請(qǐng)求。發(fā)出請(qǐng)求的代碼如下:

- (IBAction)runNSOperationTest:(id)sender {
    // 需要上傳的數(shù)據(jù)
    NSArray* images = [self images];
    
    // 準(zhǔn)備保存結(jié)果的數(shù)組,元素個(gè)數(shù)與上傳的圖片個(gè)數(shù)相同,先用 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:^{ // 回到主線程執(zhí)行,方便更新 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 是線程不安全的,所以加個(gè)同步鎖
                    result[i] = responseObject;
                }
            }
        }];
        
        HAMURLSessionWrapperOperation *uploadOperation = [HAMURLSessionWrapperOperation operationWithURLSessionTask:uploadTask];
        
        [completionOperation addDependency:uploadOperation];
        [queue addOperation:uploadOperation];
    }

    [queue addOperation:completionOperation];
}

保持結(jié)果順序的方法與 dispatch 相同,都是我們自己完成的。我們把maxConcurrentOperationCount定成 5,避免并發(fā)過(guò)多競(jìng)爭(zhēng)資源。先創(chuàng)建結(jié)束回調(diào)的 operation,再讓它依賴后面創(chuàng)建的每一個(gè)上傳 operation。因?yàn)橐话慊卣{(diào)都要涉及到更新 UI,所以讓它回到主線程執(zhí)行。后面根據(jù)每張圖片逐一創(chuàng)建 task、包裝成 operation。創(chuàng)建好之后,加進(jìn) operationQueue 里就開始跑了。

一次測(cè)試結(jié)果如下:

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

結(jié)果也是正確的。


方法三:promise

上面的兩種方法,我們都是自己用數(shù)組、占位、逐位替換的方法,自己寫代碼保證返回?cái)?shù)據(jù)順序正確的。其實(shí)這種需要多個(gè)線程執(zhí)行、全部結(jié)束后回調(diào)、結(jié)果順序保證正確的需求,一般最適合用 promise 來(lái)做。各個(gè)語(yǔ)言都有自己的 promise 實(shí)現(xiàn),iOS 也有好幾種。這里我們?cè)囉靡幌?iOS 最著名的實(shí)現(xiàn) PromiseKit。

在 github 上 5000 多個(gè) star,這個(gè) lib 是 Objective-C 、Swift 通用的,兩套代碼都有。在網(wǎng)絡(luò)請(qǐng)求方面,它要依賴同一個(gè)作者寫的另一個(gè)庫(kù) OMGHTTPURLRQ,導(dǎo)入的時(shí)候小費(fèi)周折。PromiseKit 這一套方法與 AFNetworking 庫(kù)就沒(méi)關(guān)系了,可能有些離題,但是用起來(lái)是最為方便的。

這里我們不再需要上面那個(gè)生成NSURLSessionTask的方法了,現(xiàn)在我們需要把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];
    
    // 可在此處配置驗(yàn)證信息
    
    if (completionBlock) {
        return [NSURLConnection promise:request].then(completionBlock);
    } else {
        return [NSURLConnection promise:request];
    }
}

這里可以看到 promise 的.then語(yǔ)法。它是一個(gè) C 函數(shù),傳進(jìn)的參數(shù)是這項(xiàng) promise 完成之后下一步需要執(zhí)行的 block,返回值仍然是AnyPromise,所以可以一直.then().then()……這樣鏈?zhǔn)秸{(diào)用下去。我們?cè)谶@里讓它上傳完單張圖片之后執(zhí)行單張圖片的回調(diào),把回調(diào) block『附身』在上傳的 promise 之后。

上面就是創(chuàng)建 promise 的過(guò)程。那么執(zhí)行 promise 的代碼怎么寫呢?

- (IBAction)runPromiseTest:(id)sender {
    // 需要上傳的數(shù)據(jù)
    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(@"圖片上傳失敗");
    });
}

可以看到代碼非常簡(jiǎn)潔,可讀性又好,比前兩種方法都省去不少代碼,這是 promise 的一大優(yōu)勢(shì)。我們只需把針對(duì)每張圖片創(chuàng)建一個(gè) promise ,放進(jìn)一個(gè) promises 數(shù)組,然后PMKWhen(promises).then()就能幫我們搞定一切了——是不是很神奇呢?每個(gè)任務(wù)單開線程、等待全部任務(wù)執(zhí)行完、結(jié)果正確排序等諸多工序,全都由這一行代碼搞定了。看看測(cè)試結(jié)果:

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";
    }
)

同樣是正確的。

所以看起來(lái)用 promise 還是非常方便的。不過(guò)這是我第一次嘗試用它,還不知道在工程中實(shí)際應(yīng)用會(huì)有什么缺點(diǎn)。

以上就是多線程批量上傳圖片的 3 種方法。思路最初來(lái)自 stackoverflow 的這個(gè)問(wèn)題How to send batch request by using AFNetworking 3.0,感謝這位回答的大神~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容