Как отменить NSBlockOperation

#objective-c-blocks #nsoperation #nsoperationqueue

#objective-c-блоки #nsoperation #nsoperationqueue

Вопрос:

У меня есть длительный цикл, который я хочу запустить в фоновом режиме с помощью NSOperation . Я хотел бы использовать блок:

 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
   while(/* not canceled*/){
      //do something...
   }
}];
  

Вопрос в том, как мне проверить, отменено ли оно. Блок не принимает никаких аргументов и operation равен нулю в момент его захвата блоком. Нет ли способа отменить операции с блоками?

Ответ №1:

Doh. Уважаемые будущие пользователи Google: конечно, при копировании блоком operation равно нулю, но это не обязательно копировать. Это может быть определено с помощью __block примерно так:

 //THIS MIGHT LEAK! See the update below.
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
   while( ! [operation isCancelled]){
      //do something...
   }
}];
  

Обновить:

После дальнейших размышлений мне приходит в голову, что это создаст цикл сохранения под ARC. Я полагаю, что в ARC __block хранилище сохранено. Если это так, у нас проблемы, потому что NSBlockOperation также сохраняются сильные ссылки на переданный блок in, который теперь имеет сильную ссылку на операцию, которая имеет сильную ссылку на переданный блок in, который…

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

 NSBlockOperation *operation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOperation = operation;
[operation addExecutionBlock:^{
   while( ! [weakOperation isCancelled]){
      //do something...
   }
}];
  

У кого есть идеи для более элегантного решения, пожалуйста, прокомментируйте!

Комментарии:

1. Очень полезно! Однако у вас опечатка: isCanceled должно быть isCancelled

2. Исправлено! Спасибо. Теперь у меня есть CodeRunner, который избавит меня от подобных затруднений в будущем 😉

3. Нет ли ошибки в этой реализации? Когда weakOperation становится нулевым, не будет ли он пытаться продолжить цикл? т.е. !nil == true. Не должно ли условие цикла быть while (weakOperation amp;amp; ! [Слабая операция отменена])?

4. @MarcPalmer Учитывая, что этот блок выполняется в операции, на которую weakOperation ссылается операция, я не думаю, что weakOperation возможно, чтобы внутри него было nil. Конечно, я не смог создать такую ситуацию в тестовом коде. У вас есть пример?

5. @asma22 __block в любом случае не понадобился бы. Используется для сигнализации о том, что переменная, импортируемая в блок, должна быть изменяемой . Нам не нужно weakOperation быть изменяемыми, потому что: А) это указатель. Мы можем изменить то, на что он указывает, не изменяя его. Он должен быть изменяемым, только если мы хотим, чтобы он указывал на что-то другое. И Б) даже если бы это было не так, мы только читаем из него, поэтому ему все равно не нужно было бы изменять. Смотрите документы Apple

Ответ №2:

Чтобы подкрепить ответ Джеммонса. Сессия 211 WWDC 2012 — Построение одновременных пользовательских интерфейсов (33 минуты)

 NSOperationQueue* myQueue = [[NSOperationQueue alloc] init];
NSBlockOperation* myOp = [[NSBlockOperation alloc] init];

// Make a weak reference to avoid a retain cycle
__weak NSBlockOperation* myWeakOp = myOp;

[myOp addExecutionBlock:^{
    for (int i = 0; i < 10000; i  ) {
        if ([myWeakOp isCancelled]) break;
        precessData(i);
    }
}];
[myQueue addOperation:myOp];
  

Комментарии:

1. Просто хочу убедиться, что вы не можете выполнить blockOperationWithBlock на этом, не так ли?

2. blockOperationWithBlock обычно это очень удобно, но, к сожалению, вы не можете получить ссылку на операцию при использовании этого метода (ну, на самом деле вы можете получить ее после ее объявления, но вы не можете использовать эту ссылку в фактическом блоке). Вам нужна ссылка, чтобы проверить, отменена ли операция.

3. Мне удалось извлечь это, но тогда операция блока должна быть объявлена как __weak __block, чтобы блок сохранял ссылку на него, а не копировал фактический указатель.

4. и результат такой же сложный.

5. Вероятно, безопаснее писать if (!myWeakOp || [myWeakOp isCancelled]) , чтобы предотвратить выполнение тела, если блок фактически равен нулю 🙂

Ответ №3:

В Swift 5 вы можете создать отменяемый BlockOperation с addExecutionBlock(_:) помощью. addExecutionBlock(_:) имеет следующее объявление:

 func addExecutionBlock(_ block: @escaping () -> Void)
  

Добавляет указанный блок в список блоков получателя для выполнения.


Приведенный ниже пример показывает, как реализовать addExecutionBlock(_:) :

 let blockOperation = BlockOperation()

blockOperation.addExecutionBlock({ [unowned blockOperation] in
    for i in 0 ..< 10000 {
        if blockOperation.isCancelled {
            print("Cancelled")
            return // or break
        }
        print(i)
    }
})
  

Обратите внимание, что для предотвращения цикла сохранения между BlockOperation экземпляром и его блоком выполнения вы должны использовать список захвата со ссылкой на weak или unowned на blockOperation внутри блока выполнения.


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

 import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class TestBlockOperation: BlockOperation {
    deinit {
        print("No retain cycle")
    }
}

do {
    let queue = OperationQueue()

    let blockOperation = TestBlockOperation()
    blockOperation.addExecutionBlock({ [unowned blockOperation] in
        for i in 0 ..< 10000 {
            if blockOperation.isCancelled {
                print("Cancelled")
                return // or break
            }
            print(i)
        }
    })

    queue.addOperation(blockOperation)

    Thread.sleep(forTimeInterval: 0.5)
    blockOperation.cancel()
}
  

Это выводит:

 0
1
2
3
...
Cancelled
No retain cycle
  

Ответ №4:

Я хотел иметь отменяемые блоки, которые мой UICollectionViewController мог бы легко отменить, как только ячейки были прокручены с экрана. Блоки не выполняют сетевые операции, они выполняют операции с изображениями (изменение размера, обрезка и т.д.). Сами блоки должны иметь ссылку, чтобы проверить, была ли отменена их операция, и ни один из других ответов (на момент написания этого) этого не предусматривал.

Вот что сработало у меня (Swift 3) — создание блоков, которые принимают слабую ссылку на BlockOperation , затем оборачивают их в BlockOperation сам блок:

     public extension OperationQueue {
        func addCancellableBlock(_ block: @escaping (BlockOperation?)->Void) -> BlockOperation {
            let op = BlockOperation.init()
            weak var opWeak = op
            op.addExecutionBlock {
                block(opWeak)
            }
            self.addOperation(op)
            return op
        }
    }
  

Используя это в моем UICollectionViewController :

 var ops = [IndexPath:Weak<BlockOperation>]()

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        ...
        ops[indexPath] = Weak(value: DispatchQueues.concurrentQueue.addCancellableBlock({ (op) in
            cell.setup(obj: photoObj, cellsize: cellsize)
        }))
    }

    func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if let weakOp = ops[indexPath], let op: BlockOperation = weakOp.value {
            NSLog("GCV: CANCELLING OP FOR INDEXPATH (indexPath)")
            op.cancel()
        }
    }
  

Завершаем картину:

     class Weak<T: AnyObject> {
        weak var value : T?
        init (value: T) {
            self.value = value
        }
    }