Como evito me capturar em blocos ao implementar uma API?

222

Eu tenho um aplicativo em funcionamento e estou trabalhando para convertê-lo em ARC no Xcode 4.2. Um dos avisos de pré-verificação envolve capturar selffortemente em um bloco, levando a um ciclo de retenção. Fiz um exemplo de código simples para ilustrar o problema. Acredito entender o que isso significa, mas não tenho certeza da maneira "correta" ou recomendada de implementar esse tipo de cenário.

  • self é uma instância da classe MyAPI
  • o código abaixo é simplificado para mostrar apenas as interações com os objetos e os blocos relevantes para minha pergunta
  • suponha que MyAPI obtenha dados de uma fonte remota e MyDataProcessor trabalhe nesses dados e produza uma saída
  • o processador está configurado com blocos para comunicar progresso e estado

amostra de código:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Pergunta: o que estou fazendo de "errado" e / ou como isso deve ser modificado para estar em conformidade com as convenções da ARC?

XJones
fonte

Respostas:

509

Resposta curta

Em vez de acessar selfdiretamente, você deve acessá-lo indiretamente, a partir de uma referência que não será mantida. Se você não estiver usando a Contagem automática de referência (ARC) , poderá fazer o seguinte:

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

A __blockpalavra-chave marca variáveis ​​que podem ser modificadas dentro do bloco (não estamos fazendo isso), mas também não são retidas automaticamente quando o bloco é retido (a menos que você esteja usando o ARC). Se você fizer isso, deve ter certeza de que nada mais tentará executar o bloco após o lançamento da instância MyDataProcessor. (Dada a estrutura do seu código, isso não deve ser um problema.) Leia mais sobre__block .

Se você estiver usando o ARC , a semântica das __blockalterações e a referência serão mantidas; nesse caso, você deve declará-lo __weak.

Resposta longa

Digamos que você tenha um código como este:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

O problema aqui é que o eu está mantendo uma referência ao bloco; enquanto isso, o bloco deve manter uma referência a si próprio para buscar sua propriedade de delegado e enviar um método para o delegado. Se todo o resto do seu aplicativo lançar sua referência a esse objeto, sua contagem de retenção não será zero (porque o bloco está apontando para ele) e o bloco não fará nada de errado (porque o objeto está apontando para ele) e, portanto, o par de objetos vazará para a pilha, ocupando memória, mas inacessível para sempre sem um depurador. Trágico, sério.

Esse caso pode ser facilmente corrigido fazendo isso:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

Nesse código, o self está retendo o bloco, o bloco está retendo o delegado e não há ciclos (visíveis a partir daqui; o delegado pode reter nosso objeto, mas está fora de nossas mãos agora). Esse código não corre o risco de vazar da mesma maneira, porque o valor da propriedade delegate é capturado quando o bloco é criado, em vez de procurado quando é executado. Um efeito colateral é que, se você alterar o delegado após a criação desse bloco, o bloco ainda enviará mensagens de atualização ao antigo delegado. A probabilidade de isso acontecer ou não depende da sua aplicação.

Mesmo se você foi legal com esse comportamento, ainda não pode usar esse truque no seu caso:

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

Aqui você está passando selfdiretamente para o delegado na chamada de método, então você precisa colocá-lo em algum lugar. Se você tiver controle sobre a definição do tipo de bloco, o melhor seria passar o delegado para o bloco como um parâmetro:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

Essa solução evita o ciclo de retenção e sempre chama o delegado atual.

Se você não pode mudar o bloco, você pode lidar com isso . A razão pela qual um ciclo de retenção é um aviso, não um erro, é que eles não significam necessariamente desgraça para o seu aplicativo. Se MyDataProcessorfor capaz de liberar os blocos quando a operação estiver concluída, antes que seu pai tente liberá-lo, o ciclo será interrompido e tudo será limpo corretamente. Se você pudesse ter certeza disso, a coisa certa a fazer seria usar a #pragmapara suprimir os avisos para esse bloco de código. (Ou use um sinalizador de compilador por arquivo. Mas não desative o aviso para todo o projeto.)

Você também pode usar um truque semelhante acima, declarando uma referência fraca ou sem retenção e usando-a no bloco. Por exemplo:

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Todas as três opções acima fornecerão uma referência sem reter o resultado, embora todas se comportem de maneira um pouco diferente: __weaktentará zerar a referência quando o objeto for liberado; __unsafe_unretaineddeixará você com um ponteiro inválido; __blockna verdade, adicionará outro nível de indireção e permitirá que você altere o valor da referência de dentro do bloco (neste caso, irrelevante, pois dpnão é usado em nenhum outro lugar).

O melhor depende do código que você pode alterar e do que não pode. Mas espero que isso tenha lhe dado algumas idéias sobre como proceder.

benzado
fonte
1
Resposta incrível! Obrigado, compreendo muito melhor o que está acontecendo e como tudo isso funciona. Nesse caso, eu tenho controle sobre tudo, então vou re-arquitetar alguns dos objetos, conforme necessário.
XJones 21/10
18
O_O Eu estava passando por um problema um pouco diferente, fiquei com uma leitura paralisada e agora deixo esta página com um conhecimento e um bom conhecimento. Obrigado!
perfil completo de Orc JMR
está correto, que, se por algum motivo no momento da execução do bloco dpfor liberado (por exemplo, se foi um controlador de exibição e foi populado), a linha [dp.delegate ...causará EXC_BADACCESS?
peetonn
A propriedade que contém o bloco (por exemplo, dataProcess.progress) deve ser strongou weak?
djskinner
1
Você pode dar uma olhada no libextobjc, que fornece duas macros úteis chamadas @weakify(..)e @strongify(...)que permite usar selfem bloco de maneira não retida.
25

Há também a opção de suprimir o aviso quando tiver certeza de que o ciclo será interrompido no futuro:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

Dessa forma, você não precisa se preocupar com __weak, selfaliasing e prefixo ivar explícito.

zoul
fonte
8
Parece uma prática muito ruim que requer mais de 3 linhas de código que podem ser substituídas por __weak id weakSelf = self;
Ben Sinclair
3
Geralmente, há um bloco de código maior que pode se beneficiar dos avisos suprimidos.
zoul 3/09/13
2
Exceto que o __weak id weakSelf = self;comportamento é fundamentalmente diferente do que suprimir o aviso. A pergunta começou com "... se você está certo de que o ciclo de retenção será interrompido"
Tim
Muitas vezes as pessoas cegamente tornam as variáveis ​​fracas, sem realmente entender as ramificações. Por exemplo, eu vi pessoas enfraquecerem um objeto e, em seguida, no bloco que fazem: [array addObject:weakObject];Se o fracoObjeto foi lançado, isso causa uma falha. Claramente, isso não é preferido em relação a um ciclo de retenção. Você precisa entender se o seu bloco realmente dura o tempo suficiente para justificar o enfraquecimento e também se você deseja que a ação no bloco dependa se o objeto fraco ainda é válido.
Mahboudz 27/03/19
14

Para uma solução comum, eu os defino no cabeçalho de pré-compilação. Evita a captura e ainda ativa a ajuda do compilador, evitando o usoid

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

Então, no código, você pode fazer:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Damien Pontifex
fonte
Concordado, isso pode causar um problema dentro do bloco. O ReactiveCocoa tem outra solução interessante para esse problema que permite continuar usando selfdentro do seu bloco @weakify (self); bloco de identificação = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex
@dmpontifex é uma macro de libextobjc github.com/jspahrsummers/libextobjc
Elechtron 3/15
11

Acredito que a solução sem o ARC também funcione com o ARC, usando a __blockpalavra-chave:

EDIT: De acordo com a transição para as notas de versão do ARC , um objeto declarado com __blockarmazenamento ainda é mantido. Use __weak(preferencial) ou __unsafe_unretained(para compatibilidade com versões anteriores).

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];
Tony
fonte
Não sabia que a __blockpalavra - chave evitava reter seu referente. Obrigado! Eu atualizei minha resposta monolítica. :-)
benzado 21/10
3
De acordo com os documentos da Apple "No modo de contagem de referência manual, __block id x; tem o efeito de não reter x. No modo ARC, __block id x; o padrão é reter x (assim como todos os outros valores)."
XJones 22/10
11

Combinando algumas outras respostas, é isso que eu uso agora para que um eu fraco digitado use em blocos:

__typeof(self) __weak welf = self;

Defino isso como um snippet de código XCode com um prefixo de conclusão de "welf" nos métodos / funções, que ocorre após digitar apenas "nós".

Kendall Helmstetter Gelner
fonte
Você tem certeza? Esse link e os documentos clang parecem achar que ambos podem e devem ser usados ​​para manter uma referência ao objeto, mas não um link que causará um ciclo de retenção: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner,
Nos documentos clang: clang.llvm.org/docs/BlockLanguageSpec.html "Nos idiomas Objective-C e Objective-C ++, permitimos o especificador __weak para __block variáveis ​​do tipo de objeto. Se a coleta de lixo não estiver ativada, esse qualificador causará essas variáveis ​​devem ser mantidas sem que as mensagens retidas sejam enviadas ".
Kendall Helmstetter Gelner
6

warning => "capturar a si mesmo dentro do bloco provavelmente conduzirá um ciclo de retenção"

quando você se refere a si mesmo ou a sua propriedade dentro de um bloco que é fortemente retido por si mesmo, como mostra acima o aviso.

por isso, para evitá-lo, temos que fazer uma semana ref

__weak typeof(self) weakSelf = self;

então ao invés de usar

blockname=^{
    self.PROPERTY =something;
}

devemos usar

blockname=^{
    weakSelf.PROPERTY =something;
}

note: o ciclo de retenção geralmente ocorre quando, de alguma maneira, dois objetos se referem um ao outro pelo qual ambos têm contagem de referência = 1 e seu método delloc nunca é chamado.

Anurag Bhakuni
fonte
-1

Se você tiver certeza de que seu código não criará um ciclo de retenção ou que o ciclo será interrompido posteriormente, a maneira mais simples de silenciar o aviso é:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

[self dataProcessor].progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

[self dataProcessor].completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

A razão para isso funcionar é que, enquanto o acesso às propriedades por pontos é levado em consideração pela análise do Xcode, e, portanto,

x.y.z = ^{ block that retains x}

é visto como retido por x de y (no lado esquerdo da atribuição) e por y de x (no lado direito), as chamadas de método não estão sujeitas à mesma análise, mesmo quando são chamadas de método de acesso à propriedade equivalentes ao acesso por pontos, mesmo quando esses métodos de acesso à propriedade são gerados pelo compilador, portanto, em

[x y].z = ^{ block that retains x}

somente o lado direito é visto como criando uma retenção (por y de x) e nenhum aviso do ciclo de retenção é gerado.

Ben Artin
fonte