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?

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.


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.


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;
};

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];

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".


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.



-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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.