¿Cómo evito capturarme en bloques al implementar una API?


222

Tengo una aplicación que funciona y estoy trabajando para convertirla a ARC en Xcode 4.2. Una de las advertencias previas a la verificación implica capturar selffuertemente en un bloque que conduce a un ciclo de retención. Hice una muestra de código simple para ilustrar el problema. Creo que entiendo lo que esto significa, pero no estoy seguro de la forma "correcta" o recomendada para implementar este tipo de escenario.

  • self es una instancia de la clase MyAPI
  • El siguiente código está simplificado para mostrar solo las interacciones con los objetos y bloques relevantes para mi pregunta
  • suponga que MyAPI obtiene datos de una fuente remota y MyDataProcessor trabaja en esos datos y produce una salida
  • el procesador está configurado con bloques para comunicar el progreso y el estado

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

Pregunta: ¿qué estoy haciendo "mal" y / o cómo debería modificarse esto para cumplir con las convenciones ARC?

Respuestas:


509

Respuesta corta

En lugar de acceder selfdirectamente, debe acceder indirectamente, desde una referencia que no se retendrá. Si no está utilizando el conteo automático de referencia (ARC) , puede hacer esto:

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

La __blockpalabra clave marca las variables que se pueden modificar dentro del bloque (no lo estamos haciendo), pero tampoco se retienen automáticamente cuando se retiene el bloque (a menos que esté utilizando ARC). Si hace esto, debe estar seguro de que nada más intentará ejecutar el bloque después de que se libere la instancia de MyDataProcessor. (Dada la estructura de su código, eso no debería ser un problema). Lea más sobre__block .

Si está utilizando ARC , se conservará la semántica de los __blockcambios y la referencia, en cuyo caso deberá declararla __weak.

Respuesta larga

Digamos que tienes un código como este:

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

El problema aquí es que self retiene una referencia al bloque; mientras tanto, el bloque debe retener una referencia a sí mismo para obtener su propiedad de delegado y enviarle un método. Si todo lo demás en su aplicación libera su referencia a este objeto, su recuento de retención no será cero (porque el bloque lo señala) y el bloque no está haciendo nada malo (porque el objeto lo señala) y así el par de objetos se filtrará en el montón, ocupando memoria pero inalcanzable para siempre sin un depurador. Trágico, de verdad.

Ese caso podría solucionarse fácilmente haciendo esto en su lugar:

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

En este código, self retiene el bloque, el bloque retiene al delegado y no hay ciclos (visibles desde aquí; el delegado puede retener nuestro objeto, pero eso está fuera de nuestras manos en este momento). Este código no correrá el riesgo de una fuga de la misma manera, porque el valor de la propiedad delegada se captura cuando se crea el bloque, en lugar de buscarlo cuando se ejecuta. Un efecto secundario es que, si cambia el delegado después de crear este bloque, el bloque seguirá enviando mensajes de actualización al antiguo delegado. Si eso puede suceder o no depende de su aplicación.

Incluso si fue genial con ese comportamiento, aún no puede usar ese truco en su caso:

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

Aquí está pasando selfdirectamente al delegado en la llamada al método, por lo que debe ingresarlo en algún lugar. Si tiene control sobre la definición del tipo de bloque, lo mejor sería pasar el delegado al bloque como parámetro:

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

Esta solución evita el ciclo de retención y siempre llama al delegado actual.

Si no puede cambiar el bloqueo, puede lidiar con él . La razón por la que un ciclo de retención es una advertencia, no un error, es que no necesariamente deletrean la fatalidad para su aplicación. SiMyDataProcessor es capaz de liberar los bloques cuando se completa la operación, antes de que su padre intente liberarlo, el ciclo se interrumpirá y todo se limpiará correctamente. Si puede estar seguro de esto, lo correcto sería utilizar a #pragmapara suprimir las advertencias para ese bloque de código. (O utilice un indicador del compilador por archivo. Pero no desactive la advertencia para todo el proyecto).

También puede considerar usar un truco similar arriba, declarar una referencia débil o no retenida y usarla en el bloque. Por ejemplo:

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

Los tres anteriores le darán una referencia sin retener el resultado, aunque todos se comportan de manera un poco diferente: __weakintentará poner a cero la referencia cuando se libere el objeto; __unsafe_unretainedte dejará con un puntero no válido; __blocken realidad agregará otro nivel de indirección y le permitirá cambiar el valor de la referencia desde dentro del bloque (irrelevante en este caso, ya quedp que no se usa en ningún otro lugar).

Lo mejor dependerá de qué código puede cambiar y qué no puede. Pero espero que esto te haya dado algunas ideas sobre cómo proceder.


1
Respuesta impresionante! Gracias, entiendo mucho mejor lo que está sucediendo y cómo funciona todo esto. En este caso, tengo control sobre todo, así que rediseñaré algunos de los objetos según sea necesario.
XJones

18
O_O Estaba pasando con un problema ligeramente diferente, me quedé atascado leyendo y ahora salgo de esta página sintiéndome bien informado y genial. ¡Gracias!
Orc JMR

dpEsto es correcto, que si por alguna razón en el momento de la ejecución del bloque se liberará (por ejemplo, si se tratara de un controlador de vista y se apilara), ¿la línea [dp.delegate ...causará EXC_BADACCESS?
peetonn

En caso de que la propiedad que sostiene el bloque (por ejemplo dataProcess.progress) ser strongo weak?
djskinner

1
Puede echar un vistazo a libextobjc, que proporciona dos prácticas macros llamadas @weakify(..)y @strongify(...)que le permite usar selfen bloque de una manera no retenida.

25

También existe la opción de suprimir la advertencia cuando esté seguro de que el ciclo se romperá en el futuro:

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

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

#pragma clang diagnostic pop

De esa manera, no tiene que andar con alias __weak, selfalias y prefijos de ivar explícitos.


8
Suena como una práctica muy mala que requiere más de 3 líneas de código que se pueden reemplazar con __weak id weakSelf = self;
Ben Sinclair

3
A menudo hay un bloque de código más grande que puede beneficiarse de las advertencias suprimidas.
zoul

2
Excepto que __weak id weakSelf = self;tiene un comportamiento fundamentalmente diferente a la supresión de la advertencia. La pregunta comenzó con "... si está seguro de que el ciclo de retención se romperá"
Tim

Con demasiada frecuencia, las personas ciegamente debilitan las variables, sin comprender realmente las ramificaciones. Por ejemplo, he visto personas debilitar un objeto y luego, en el bloque que lo hacen: [array addObject:weakObject];si se ha liberado el objeto débil, esto provoca un bloqueo. Claramente, eso no se prefiere sobre un ciclo de retención. Debe comprender si su bloqueo realmente dura lo suficiente como para justificar su debilitamiento, y también si desea que la acción en el bloque dependa de si el objeto débil sigue siendo válido.
mahboudz

14

Para una solución común, tengo estos definir en el encabezado de precompilación. Evita la captura y aún habilita la ayuda del compilador al evitar usarid

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

Luego en código puedes hacer:

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

De acuerdo, esto podría causar un problema dentro del bloque. ReactiveCocoa tiene otra solución interesante para este problema que le permite continuar usando selfdentro de su bloque @weakify (self); id block = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex

@dmpontifex es una macro de libextobjc github.com/jspahrsummers/libextobjc
Elechtron

11

Creo que la solución sin ARC también funciona con ARC, usando la __blockpalabra clave:

EDITAR: según las notas de la versión Transition to ARC , un objeto declarado con __blockalmacenamiento aún se conserva. Use __weak(preferido) o __unsafe_unretained(para compatibilidad con versiones 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];

No me di cuenta de que la __blockpalabra clave evitaba conservar su referencia. ¡Gracias! Actualicé mi respuesta monolítica. :-)
benzado

3
De acuerdo con los documentos de Apple "En el modo de conteo de referencia manual, __block id x; tiene el efecto de no retener x. En el modo ARC, __block id x; el valor predeterminado es retener x (al igual que todos los demás valores)".
XJones

11

Combinando algunas otras respuestas, esto es lo que uso ahora para que un yo débil mecanografiado lo use en bloques:

__typeof(self) __weak welf = self;

Lo configuré como un fragmento de código XCode con un prefijo de finalización de "welf" en métodos / funciones, que aparece después de escribir solo "nosotros".


¿Estás seguro? Este enlace y los documentos de clang parecen pensar que ambos pueden y deben usarse para mantener una referencia al objeto, pero no un enlace que causará un ciclo de retención: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner

De los documentos de clang: clang.llvm.org/docs/BlockLanguageSpec.html "En los lenguajes Objective-C y Objective-C ++, permitimos el especificador __weak para __block variables de tipo de objeto. Si la recolección de basura no está habilitada, este calificador provoca estas variables deben mantenerse sin retener los mensajes que se envían ".
Kendall Helmstetter Gelner


6

warning => "es probable que la captura de uno mismo dentro del bloque conduzca a un ciclo de retención"

cuando se refiere self o su propiedad dentro de un bloque que está fuertemente retenido por self de lo que muestra la advertencia anterior.

así que para evitarlo tenemos que hacerlo una semana ref

__weak typeof(self) weakSelf = self;

así que en lugar de usar

blockname=^{
    self.PROPERTY =something;
}

deberíamos usar

blockname=^{
    weakSelf.PROPERTY =something;
}

nota: el ciclo de retención generalmente ocurre cuando de alguna manera dos objetos que se refieren entre sí por los cuales ambos tienen un recuento de referencia = 1 y su método delloc nunca se llama.



-1

Si está seguro de que su código no creará un ciclo de retención, o que el ciclo se interrumpirá más tarde, entonces la forma más sencilla de silenciar la advertencia es:

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

La razón por la que esto funciona es que si bien el análisis de Xcode tiene en cuenta el acceso de puntos a las propiedades y, por lo tanto,

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

se considera que tiene una retención por x de y (en el lado izquierdo de la asignación) y por y de x (en el lado derecho), las llamadas a métodos no están sujetas al mismo análisis, incluso cuando son llamadas a métodos de acceso a la propiedad que son equivalentes al punto de acceso, incluso cuando esos métodos de acceso a la propiedad son generados por el compilador, así que en

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

solo se ve que el lado derecho crea una retención (por y de x), y no se genera ninguna advertencia de ciclo de retención.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.