如何处理异步完成处理程序以编程方式捕获静态图像并按顺序使用 AVFoundation

How to deal with asynchronous completion handler to capture still images programmatically AND sequentially with AVFoundation

在我正在编写的 iOS 应用程序中,我需要在没有用户交互的情况下自动捕获图像。应用程序启动后,它依次需要使用设备相机拍照,将其发送到外部 Web 服务,等待 Web 服务响应,然后拍摄第二张图像,发送它,等待响应,拍摄第三张照片,发送它...等等。 到目前为止,我正在使用 AVFoundation 和 captureStillImageAsynchronouslyFromConnection.

我很难用我的 'sequential' 算法来适应异步图像捕获。我的 ViewController 有一个 captureImage,应该用捕获的图像数据更新 属性 (currentImage):

-(void)captureImage {  
    AVCaptureConnection *videoConnection = .... 
    // .... code here to get the connection, not relevant here .... //

    __block ViewController *weakSelf = self;

    [self.output captureStillImageAsynchronouslyFromConnection:videoConnection
        completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error) {
            weakSelf.currentImage = [[NSData alloc] initWithData:
                [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer]];

        NSLog(@"end of completion handler");         
    }];

    NSLog(@"end of captureImage");
}

然后我从实现算法的方法 doStuff 中调用 captureImage。请注意,从视图中的按钮调用 doStuff 以启动自动捕获过程。

-(void)doStuff {
    [self captureImage];
    if (self.currentImage == nil) { NSLog(@"NIL"); }
    .... send the currentImage to web service .... (not relevant here)
    // get second image after web service result is available
    [self captureImage];
    .... send the currentImage .... (not relevant here)
}

正如您可能猜到的那样,问题在于虽然调用了 captureImage returns,但尚未执行对给定 captureStillImageAsynchronously 的完成处理程序的异步调用。因此 self.currentImage 保持为零。我注意到完成处理程序似乎在 doStuff 完成后 运行 (我对 iOS 应用程序线程的理解仍然很模糊,但我猜想主线程在完成时执行完成处理程序代码没有更多的我的代码可以执行...为时已晚)。

我应该如何在我的 iOS 应用程序中实现这样的图像顺序捕获?我的用例确实需要能够在没有用户交互的情况下控制排序。我没有很多 iOS 开发经验,所以我想我错过了一些重要的事情。欢迎任何建议!

一种方法是将块参数传递给您的 captureImage 方法。

- (void)captureImage:(void(^)(NSData*))completion {
  AVCaptureConnection *videoConnection = ....
  // .... code here to get the connection, not relevant here .... //

  __block ViewController *weakSelf = self;

  [self.output captureStillImageAsynchronouslyFromConnection:videoConnection
                                           completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error) {
                                             NSData *data = [[NSData alloc] initWithData:
                                                                      [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer]];
                                             completion(data);
                                             NSLog(@"end of completion handler");         
                                           }];

  NSLog(@"end of captureImage");
}

然后更新你的 doStuff 方法:

-(void)doStuff {
  [self captureImage:^(NSData* data){
    // Do things with your data.
    [self captureImage:^(NSData *data){
      // Do things with the next data.
    }];
  }];
}

现在,如果您要拍摄很多图像,嵌套块回调会变得很多(即丑陋)。

但是您可以将处理程序块定义为局部变量。

  void (^handler)(NSData*) = ^(NSData*data) {
    self.currentImage = [UIImage imageWithData:data];
    // Do things with your current image.
  };

  int maxImages = 42;
  for (int i = 0; i < maxImages; i++) {
    [self captureImage:handler];
  }

此外,如果你想确保你的图片是按照拍摄的顺序上传的,你可以看一下captureStillImageAsynchronouslyWithCompletionHandler:的completionBlock中传回的数据,其中包含一个EXIF字典。相关值将键入 kCGImagePropertyExifDateTimeOriginal.

因此,根据您的描述,您有两个异步方法,并且您希望在循环中顺序调用它们。

您可以按以下方式解决此问题:

首先定义这些异步方法:

typedef void (^completion_t)(id result, NSError* error);

- (void)captureImage:(completion_t)completion {
    // ...
}

- (void)uploadImage:(UIImage*)image params:(NSDictionary*)params completion:(completion_t)completion {
    // ...
}

然后定义一个私有辅助方法 captureAndUpload,它基本上为调用 uploadImage 的方法 captureImage 设置一个延续。 uploadImage 的继续测试它是否应该重复,如果结果是真的,则再次调用 captureAndUpload

- (void)captureAndUpload {
    [self captureImage:^(UIImage* image, NSError *error) {
        if (image != nil) {
            [self uploadImage:image params:someParams
                   completion:^(id result, NSError *error) {
                if (result != nil) {
                    if (repeat) {
                        [self captureAndUpload];
                    }
                    else {
                        // ..
                    }
                }
                else {
                    // ...
                }
            }];
        }
        else {
            // ...
        }
    }];
}

处理并发问题的更完整示例:

@interface Foo : NSObject

-(instancetype)init;

- (void) resume;
- (void) suspend;

@end


typedef void (^completion_t)(id result, NSError* error);

@implementation Foo {
    int _resumed;
    BOOL _running;
    dispatch_queue_t _sync_queue;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _resumed = 0;
        _running = NO;
        _sync_queue = dispatch_queue_create("imageuploader.sync_queue", DISPATCH_QUEUE_SERIAL);

    }
    return self;
}

- (void) resume {
    dispatch_async(_sync_queue, ^{
        if (++_resumed == 1) {
            [self captureAndUpload];
        }
    });
}

- (void) suspend {
    dispatch_async(_sync_queue, ^{
        --_resumed;
    });
}


- (void)captureAndUpload {
    _running = YES;
    [self captureImage:^(UIImage* image, NSError *error) {
        if (image != nil) {
            [self uploadImage:image params:nil
                   completion:^(id result, NSError *error) {
                if (result != nil) {
                    if (_resumed > 0) {
                        dispatch_async(_sync_queue, ^{
                            [self captureAndUpload];
                        });
                    }
                    else {
                        dispatch_async(_sync_queue, ^{
                            _running = NO;
                        });
                    }
                }
                else {
                    // ...
                }
            }];
        }
        else {
            // ...
        }
    }];
}

- (void)captureImage:(completion_t)completion {
    // ...
}

- (void)uploadImage:(UIImage*)image params:(NSDictionary*)params completion:(completion_t)completion {
    // ...
}

@end

不过这个例子还不完整:它不处理错误,没有办法取消底层网络请求,并且不清楚 "uploader" 的状态(Foo) 目前是。也就是说,我们可能想知道暂停上传器后最后一次上传是什么时候完成的。