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

AFNetworking 在去年年末升級到了 3.0。這個版本更新想必有不少好處,然而讓我吃驚的是,它並無 batch request 接口。以前的 1.x 版本、2.x 版本都實現了這個很常見的需求,不知道做者爲什麼選擇在 3.x 中去掉它。html

在 AFNetworking 2 中,咱們只需一行代碼就能解決批量上傳的問題:ios

[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類都幹掉了。上面的方法不能再用,而且也沒有給出替代品。所以,咱們只能本身動手了。git

實現這個功能,有幾個要點:github

  1. 異步上傳。批量請求裏的每一個請求都應該在不一樣線程,能夠同時上傳。
  2. 在全部請求都完成以後,再通知回調。
  3. 儘管異步請求的返回前後順序沒有必定,極可能後發出的請求先返回;可是最後回調的時候,請求返回的結果必需要按請求發出的順序排列。好比,一個很常見的處理是,上傳圖片的接口返回該圖片的 url;那麼回調結果裏的 url 順序顯然須要跟上傳的圖片順序一一對應上。
  4. 最好傳完每張圖片也能有一個回調,方便咱們告訴用戶上傳的進度。

同時知足以上要點,主要有3種思路:GCD、NSOperation 以及 promise。這個需求也是示例多線程用法的一個很好的例子,因此我寫了這篇比較詳細的文章供你們參考。數組

下面的代碼以圖片上傳爲例。測試數據配置了 3 張圖片,其中第 2 張圖片尺寸最小,每每先上傳完畢,用來測試請求發出順序與返回順序不一致的狀況。 promise

方法一: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。最後,咱們用 AFURLSessionManager把 NSURLRequest和回調 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 仍是很是方便的。不過這是我第一次嘗試用它,還不知道在工程中實際應用會有什麼缺點。

相關文章
相關標籤/搜索