Подтвердить что ты не робот

Управление связкой NSOperation с зависимостями

Я работаю над приложением, которое создает содержимое и отправляет его на существующий бэкэнд. Контент - это заголовок, изображение и местоположение. Ничего необычного.

Бэкэнд немного сложный, поэтому вот что мне нужно сделать:

  • Позвольте пользователю сделать снимок, ввести заголовок и разрешить карте использовать его расположение
  • Создайте уникальный идентификатор сообщения
  • Создать сообщение на бэкэнд
  • Загрузить изображение
  • Обновить пользовательский интерфейс

Я использовал несколько подклассов NSOperation для выполнения этой работы, но я не горжусь своим кодом, вот образец.

NSOperation *process = [NSBlockOperation blockOperationWithBlock:^{
    // Process image before upload
}];

NSOperation *filename = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(generateFilename) object: nil];

NSOperation *generateEntry = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(createEntry) object: nil];

NSOperation *uploadImage = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(uploadImageToCreatedEntry) object: nil];

NSOperation *refresh = [NSBlockOperation blockOperationWithBlock:^{
    // Update UI
    [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
}];

[refresh addDependency: uploadImage];

[uploadImage addDependency: generateEntry];
[generateEntry addDependency: filename];
[generateEntry addDependency: process];

[[NSOperationQueue mainQueue] addOperation: refresh];
[_queue addOperations: @[uploadImage, generateEntry, filename, process] waitUntilFinished: NO];

Вот что мне не нравится:

  • в моей createEntry: например, я храню сгенерированное имя файла в свойстве, которое соответствует глобальной области моего класса
  • в методе uploadImageToCreatedEntry: я использую dispatch_async + dispatch_get_main_queue() для обновления сообщения в HUD
  • и др.

Как бы вы справились с таким рабочим процессом? Я бы хотел избежать внедрения нескольких блоков завершения, и я чувствую, что NSOperation действительно является способом выхода, но я также чувствую, что где-то лучше реализована реализация.

Спасибо!

4b9b3361

Ответ 1

Вы можете использовать ReactiveCocoa для выполнить это довольно легко. Одна из его больших целей - сделать такой вид композиция тривиальная.

Если вы еще не слышали об ReactiveCocoa или не знакомы с ним, проверьте Introduction для быстрого объяснения.

Я избегу дублирования всего обзора структуры здесь, но достаточно сказать, что RAC фактически предлагает надмножество promises/фьючерсов. Это позволяет вам составлять и трансформировать события совершенно другого происхождения (пользовательский интерфейс, сеть, база данных, KVO, уведомления и т.д.), что невероятно мощно.

Чтобы начать работу с этим кодом, первое и самое простое, что мы можем сделать, это положить эти отдельные операции в методы и гарантировать, что каждый возвращается a RACSignal. Это не является строго необходимым (все они могут быть определены внутри одна область), но делает код более модульным и читаемым.

Например, создайте пару сигналов, соответствующих process и generateFilename:

- (RACSignal *)processImage:(UIImage *)image {
    return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
        // Process image before upload

        UIImage *processedImage = …;
        [subscriber sendNext:processedImage];
        [subscriber sendCompleted];
    }];
}

- (RACSignal *)generateFilename {
    return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
        NSString *filename = [self generateFilename];
        [subscriber sendNext:filename];
        [subscriber sendCompleted];
    }];
}

Другие операции (createEntry и uploadImageToCreatedEntry) будут очень похожими.

Как только мы получим их, их очень легко составить и выразить (хотя комментарии делают это немного плотным):

[[[[[[self
    generateFilename]
    flattenMap:^(NSString *filename) {
        // Returns a signal representing the entry creation.
        // We assume that this will eventually send an `Entry` object.
        return [self createEntryWithFilename:filename];
    }]
    // Combine the value with that returned by `-processImage:`.
    zipWith:[self processImage:startingImage]]
    flattenMap:^(RACTuple *entryAndImage) {
        // Here, we unpack the zipped values then return a single object,
        // which is just a signal representing the upload.
        return [self uploadImage:entryAndImage[1] toCreatedEntry:entryAndImage[0]];
    }]
    // Make sure that the next code runs on the main thread.
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeError:^(NSError *error) {
        // Any errors will trickle down into this block, where we can
        // display them.
        [self presentError:error];
    } completed:^{
        // Update UI
        [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
    }];

Обратите внимание, что я переименовал некоторые из ваших методов, чтобы они могли принимать входы от их зависимости, давая нам более естественный способ накопления значений из одного к следующему.

Здесь есть огромные преимущества:

  • Вы можете прочитать его сверху вниз, поэтому очень легко понять порядок, который все происходит, и где лежат зависимости.
  • Очень легко перемещать работу между различными потоками, о чем свидетельствует использование -deliverOn:.
  • Любые ошибки, отправленные любым из этих методов, автоматически отменяют все остальной части работы, и в конечном итоге достичь блока subscribeError: для легкого обработки.
  • Вы также можете составить это с другими потоками событий (т.е. не только операции). Например, вы можете настроить это для запуска только тогда, когда пользовательский интерфейс сигнал (например, нажатие кнопки) срабатывает.

ReactiveCocoa - огромная инфраструктура, и, к сожалению, трудно перегонять преимущества в небольшой образец кода. Я настоятельно рекомендую проверить примеры для когда использовать ReactiveCocoa чтобы узнать больше о том, как это может помочь.

Ответ 2

Несколько мыслей:

  • Я был бы склонен использовать блоки завершения, потому что вы, вероятно, только хотите начать следующую операцию, если предыдущий преуспел. Вы хотите, чтобы вы правильно обрабатывали ошибки и могли легко вырваться из своей цепочки операций, если это не удается.

  • Если бы я хотел передать данные из операции в другую и не хотел использовать какое-либо свойство класса вызывающего, я бы, вероятно, определил свой собственный блок завершения как свойство моей пользовательской операции, у которой был параметр, который включал поле, которое я хотел передать из одной операции в другую. Это предполагает, однако, что вы выполняете подклассы NSOperation.

    Например, у меня может быть FilenameOperation.h, который определяет интерфейс для моего подкласса операции:

    #import <Foundation/Foundation.h>
    
    typedef void (^FilenameOperationSuccessFailureBlock)(NSString *filename, NSError *error);
    
    @interface FilenameOperation : NSOperation
    
    @property (nonatomic, copy) FilenameOperationSuccessFailureBlock successFailureBlock;
    
    @end
    

    и если это была не параллельная операция, реализация может выглядеть так:

    #import "FilenameOperation.h"
    
    @implementation FilenameOperation
    
    - (void)main
    {
        if (self.isCancelled)
            return;
    
        NSString *filename = ...;
        BOOL failure = ...
    
        if (failure)
        {
            NSError *error = [NSError errorWithDomain:... code:... userInfo:...];
            if (self.successFailureBlock)
                self.successFailureBlock(nil, error);                                                    
        }
        else
        {
            if (self.successFailureBlock)
                self.successFailureBlock(filename, nil);
        }
    }
    
    @end
    

    Очевидно, что если у вас есть параллельная операция, вы реализуете все стандартные логики isConcurrent, isFinished и isExecuting, но идея одинаков. В стороне, иногда люди отправляют эти успехи или неудачи обратно в основную очередь, поэтому вы тоже можете это сделать, если хотите.

    Несмотря на это, это иллюстрирует идею настраиваемого свойства с моим собственным блоком завершения, который передает соответствующие данные. Вы можете повторить этот процесс для каждого из соответствующих типов операций, затем вы можете связать их все вместе с чем-то вроде:

    FilenameOperation *filenameOperation = [[FilenameOperation alloc] init];
    GenerateOperation *generateOperation = [[GenerateOperation alloc] init];
    UploadOperation   *uploadOperation   = [[UploadOperation alloc] init];
    
    filenameOperation.successFailureBlock = ^(NSString *filename, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            generateOperation.filename = filename;
            [queue addOperation:generateOperation];
        }
    };
    
    generateOperation.successFailureBlock = ^(NSString *filename, NSData *data, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            uploadOperation.filename = filename;
            uploadOperation.data     = data;
            [queue addOperation:uploadOperation];
        }
    };
    
    uploadOperation.successFailureBlock = ^(NSString *result, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // update UI here
                NSLog(@"%@", result);
            }];
        }
    };
    
    [queue addOperation:filenameOperation];
    
  • Другой подход в более сложных сценариях заключается в том, чтобы ваш подкласс NSOperation использовал метод, аналогичный тому, как работает стандартный метод addDependency, в котором NSOperation устанавливает состояние isReady на основе KVO on isFinished на другой операции. Это позволяет не только устанавливать более сложные зависимости между операциями, но также передавать базу данных между ними. Это, вероятно, выходит за рамки этого вопроса (и я уже страдаю от tl: dr), но дайте мне знать, если вам нужно больше здесь.

  • Я не был бы слишком обеспокоен тем, что uploadImageToCreatedEntry отправляется обратно в основной поток. В сложных проектах у вас могут быть разные очереди, предназначенные для определенных типов операций, а тот факт, что обновления пользовательского интерфейса добавляются в основную очередь, полностью соответствует этому режиму. Но вместо dispatch_async я мог бы склониться к эквиваленту NSOperationQueue:

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // do my UI update here
    }];
    
  • Интересно, нужны ли вам все эти операции. Например, мне трудно представить, что filename достаточно сложна для оправдания собственной операции (но если вы получаете имя файла из какого-то удаленного источника, то отдельная операция имеет смысл). Я предполагаю, что вы делаете что-то достаточно сложное, что оправдывает его, но имена этих операций заставляют меня задуматься.

  • Если вы хотите, вы можете взглянуть на класс couchdeveloper RXPromise, который использует promises: (a) контролировать логическую взаимосвязь между отдельными операциями; и (б) упростить передачу данных от одного к другому. У Майка Эша есть старый MAFuture класс, который делает то же самое.

    Я не уверен, что кто-то из них достаточно зрелый, что я буду рассматривать их в своем собственном коде, но это интересная идея.

Ответ 3

Я, вероятно, полностью, предвзятый, но по какой-то причине - мне нравится подход @Rob № 6;)

Предполагая, что вы создали соответствующие обертки для ваших асинхронных методов и операций, которые возвращают Promise вместо того, чтобы сигнализировать о завершении с блоком завершения, решение выглядит следующим образом:

RXPromise* finalResult = [RXPromise all:@[[self filename], [self process]]]
.then(^id(id filenameAndProcessResult){
    return [self generateEntry];
}, nil)
.then(^id(id generateEntryResult){
    return [self uploadImage];
}, nil)
.thenOn(dispatch_get_main_queue() , ^id(id uploadImageResult){
    [self refreshWithResult:uploadImageResult];
    return nil;
}, nil)
.then(nil, ^id(NSError*error){
    // Something went wrong in any of the operations. Log the error:
    NSLog(@"Error: %@", error);
});

И, если вы хотите отменить целую асинхронную последовательность на любом тине, где бы то ни было и независимо от того, как далеко он был продолжен:

[finalResult.root cancel];

(Небольшое примечание: свойство root пока недоступно в текущей версии RXPromise, но его в основном очень просто реализовать).

Ответ 4

Если вы все еще хотите использовать NSOperation, вы можете положиться на ProcedureKit и использовать свойства впрыска класса Procedure.

Для каждой операции укажите, какой тип она производит, и введите ее в следующую зависимую операцию. Вы также можете завершить весь процесс внутри класса GroupProcedure.