AFNetworking 在去年年底升級到了 3.0。這個版本更新想必有很多好處,然而讓我吃驚的是,它並沒有 batch request 介面。之前的 1.x 版本、2.x 版本都實現了這個很常見的需求,不知道作者為何選擇在 3.x 中去掉它。
在 AFNetworking 2 中,我們只需一行程式碼就能解決批量上傳的問題:
1 2 3 4 5 |
[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
類都幹掉了。上面的方法不能再用,並且也沒有給出替代品。因此,我們只能自己動手了。
實現這個功能,有幾個要點:
- 非同步上傳。批量請求裡的每個請求都應該在不同執行緒,可以同時上傳。
- 在所有請求都完成之後,再通知回撥。
- 儘管非同步請求的返回先後順序沒有一定,很可能後發出的請求先返回;但是最後回撥的時候,請求返回的結果必須要按請求發出的順序排列。比如,一個很常見的處理是,上傳圖片的介面返回該圖片的 url;那麼回撥結果裡的 url 順序顯然需要跟上傳的圖片順序一一對應上。
- 最好傳完每張圖片也能有一個回撥,方便我們告訴使用者上傳的進度。
同時滿足以上要點,主要有3種思路:GCD、NSOperation 以及 promise。這個需求也是示例多執行緒用法的一個很好的例子,所以我寫了這篇比較詳細的文章供大家參考。
下面的程式碼以圖片上傳為例。測試資料配置了 3 張圖片,其中第 2 張圖片尺寸最小,往往先上傳完畢,用來測試請求發出順序與返回順序不一致的情況。
方法一:GCD dispatch group
我們知道,GCD dispatch 是多執行緒處理最簡單的方法。全部請求完成後再通知回撥的需求,很適合利用 dispatch group 來完成。至於保證返回結果的順序,我們只好自己來做了。
首先需要一個方法,對於每張圖片生成一個上傳請求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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; } |
在這個方法裡,我們首先用UIImageJPEGRepresentation
把UIImage
變為NSData
。然後用AFHTTPRequestSerializer
來生成NSMutableURLRequest
,[self uploadUrl]
是上傳介面的地址。為安全考慮,一般上傳的介面都有身份驗證的需求,比如在請求 header 中加入 auth 資訊,可以在此配置NSMutableURLRequest
的 header。最後,我們用 AFURLSessionManager
把 NSURLRequest
和回撥 block 包裝成一個NSURLSessionUploadTask
。
有了生成請求的方法,批量發出請求的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
- (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
把運算元組的程式碼鎖住了,鎖的物件就用這個陣列即可。這樣保證所有執行緒執行到這一句都得序列,避免執行緒安全問題。
一次測試結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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
能用 dispatch 實現的功能,自然也可以用NSOperationQueue
。NSOperation 這一套比 dispatch 寫起來要麻煩一些,不過有幾個優點:
NSOperation
是物件,不像 dispatch 是 c 函式。這就意味著你可以繼承它,可以給它加 category,在執行過程中也可以始終管理它,訪問到它,檢視它的狀態等,不像 dispatch 是一撒手就夠不著了。- 用
NSOperation
執行的任務,執行過程中可以隨時取消。dispatch 一經發出是無法取消的。 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
1 2 3 4 5 6 7 |
#import <Foundation/Foundation.h> @interface HAMURLSessionWrapperOperation : NSOperation + (instancetype)operationWithURLSessionTask:(NSURLSessionTask*)task; @end |
HAMURLSessionWrapperOperation.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#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 { <a href="http://www.jobbole.com/members/xyz937134366">@try</a> { [self startObservingTask]; [self.task resume]; } <a href="http://www.jobbole.com/members/wx895846013">@catch</a> (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
這個任務結束。
系統NSOperation
的finished
屬性是隻讀的,不能修改;為了記錄值和發出 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
方法,根據圖片生成請求。發出請求的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
- (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 裡就開始跑了。
一次測試結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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 的程式碼怎麼寫呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (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()
就能幫我們搞定一切了——是不是很神奇呢?每個任務單開執行緒、等待全部任務執行完、結果正確排序等諸多工序,全都由這一行程式碼搞定了。看看測試結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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,感謝這位回答的大神~