objective c - Conservar el ciclo en `self` con bloques
objective-c memory-management (9)
Me temo que esta pregunta es bastante básica, pero creo que es relevante para muchos programadores de Objective-C que se están metiendo en bloques.
Lo que he escuchado es que, dado que los bloques capturan las variables locales a las que se hace referencia como copias de const
, usar self
dentro de un bloque puede dar como resultado un ciclo de retención, en caso de que ese bloque se copie. Entonces, se supone que __block
usar __block
para forzar al bloque a tratar directamente con self
lugar de copiarlo.
__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];
en lugar de simplemente
[someObject messageWithBlock:^{ [self doSomething]; }];
Lo que me gustaría saber es lo siguiente: si esto es cierto, ¿hay alguna manera de evitar la fealdad (además de usar GC)?
¿Qué tal esto?
- (void) foo {
__weak __block me = self;
myBlock = ^ {
[[me someProp] someMessage];
}
...
}
Ya no recibo la advertencia del compilador.
Bloque: se producirá un ciclo de retención porque contiene un bloque al que se hace referencia en el bloque; Si haces que el bloque copie y uses una variable miembro, el self retendrá.
Esto puede ser obvio, pero solo tiene que hacer el feo self
alias cuando sabe que obtendrá un ciclo de retención. Si el bloqueo es solo una acción de un solo intento, entonces creo que puedes ignorar el retenerlo con seguridad. El caso malo es cuando tienes el bloque como una interfaz de devolución de llamada, por ejemplo. Como aquí:
typedef void (^BufferCallback)(FullBuffer* buffer);
@interface AudioProcessor : NSObject {…}
@property(copy) BufferCallback bufferHandler;
@end
@implementation AudioProcessor
- (id) init {
…
[self setBufferCallback:^(FullBuffer* buffer) {
[self whatever];
}];
…
}
Aquí la API no tiene mucho sentido, pero tendría sentido cuando se comunica con una superclase, por ejemplo. Retenemos el controlador de búfer, el controlador de búfer nos retiene. Compare con algo como esto:
typedef void (^Callback)(void);
@interface VideoEncoder : NSObject {…}
- (void) encodeVideoAndCall: (Callback) block;
@end
@interface Foo : NSObject {…}
@property(retain) VideoEncoder *encoder;
@end
@implementation Foo
- (void) somewhere {
[encoder encodeVideoAndCall:^{
[self doSomething];
}];
}
En estas situaciones, no hago el self
aliasing. Obtienes un ciclo de retención, pero la operación dura poco y el bloque se quedará sin memoria al final, rompiendo el ciclo. Pero mi experiencia con los bloques es muy pequeña y podría ser que el self
alias salga como una mejor práctica a largo plazo.
Estrictamente hablando, el hecho de que sea una copia exacta no tiene nada que ver con este problema. Los bloques retendrán los valores obj-c que se capturan cuando se crean. Simplemente sucede que la solución para el problema const-copy es idéntica a la solución para el problema de retención; es decir, utilizando la clase de almacenamiento __block
para la variable.
En cualquier caso, para responder a su pregunta, no hay una alternativa real aquí. Si está diseñando su propia API basada en bloques, y tiene sentido hacerlo, puede hacer que el bloque pase el valor de self
como argumento. Desafortunadamente, esto no tiene sentido para la mayoría de las API.
Tenga en cuenta que hacer referencia a un ivar tiene exactamente el mismo problema. Si necesita hacer referencia a un ivar en su bloque, use una propiedad en su lugar o use bself->ivar
.
Adición: Al compilar como ARC, __block
ya no rompe los ciclos de retención. Si está compilando para ARC, necesita usar __weak
o __unsafe_unretained
en __unsafe_unretained
lugar.
Publicar otra respuesta porque esto también fue un problema para mí. Originalmente pensé que tenía que usar BlockSelf en cualquier lugar donde hubiera una referencia propia dentro de un bloque. Este no es el caso, es solo cuando el objeto mismo tiene un bloque. Y de hecho, si utiliza blockSelf en estos casos, el objeto puede ser desasignado antes de obtener el resultado del bloque y luego se bloqueará cuando intente llamarlo, por lo que es evidente que desea conservar el auto hasta la respuesta Vuelve.
El primer caso demuestra cuándo se producirá un ciclo de retención porque contiene un bloque al que se hace referencia en el bloque:
#import <Foundation/Foundation.h>
typedef void (^MyBlock)(void);
@interface ContainsBlock : NSObject
@property (nonatomic, copy) MyBlock block;
- (void)callblock;
@end
@implementation ContainsBlock
@synthesize block = _block;
- (id)init {
if ((self = [super init])) {
//__block ContainsBlock *blockSelf = self; // to fix use this.
self.block = ^{
NSLog(@"object is %@", self); // self retain cycle
};
}
return self;
}
- (void)dealloc {
self.block = nil;
NSLog (@"ContainsBlock"); // never called.
[super dealloc];
}
- (void)callblock {
self.block();
}
@end
int main() {
ContainsBlock *leaks = [[ContainsBlock alloc] init];
[leaks callblock];
[leaks release];
}
No necesita BlockSelf en el segundo caso porque el objeto que llama no tiene un bloque que provocará un ciclo de retención cuando hace referencia a uno mismo:
#import <Foundation/Foundation.h>
typedef void (^MyBlock)(void);
@interface BlockCallingObject : NSObject
@property (copy, nonatomic) MyBlock block;
@end
@implementation BlockCallingObject
@synthesize block = _block;
- (void)dealloc {
self.block = nil;
NSLog(@"BlockCallingObject dealloc");
[super dealloc];
}
- (void)callblock {
self.block();
}
@end
@interface ObjectCallingBlockCallingObject : NSObject
@end
@implementation ObjectCallingBlockCallingObject
- (void)doneblock {
NSLog(@"block call complete");
}
- (void)dealloc {
NSLog(@"ObjectCallingBlockCallingObject dealloc");
[super dealloc];
}
- (id)init {
if ((self = [super init])) {
BlockCallingObject *myobj = [[BlockCallingObject alloc] init];
myobj.block = ^() {
[self doneblock]; // block in different object than this object, no retain cycle
};
[myobj callblock];
[myobj release];
}
return self;
}
@end
int main() {
ObjectCallingBlockCallingObject *myObj = [[ObjectCallingBlockCallingObject alloc] init];
[myObj release];
return 0;
}
Puede usar la biblioteca libextobjc. Es bastante popular, se usa en ReactiveCocoa, por ejemplo. https://github.com/jspahrsummers/libextobjc
Proporciona 2 macros @weakify y @strongify, por lo que puede tener:
@weakify(self)
[someObject messageWithBlock:^{
@strongify(self)
[self doSomething];
}];
Esto evita una referencia fuerte directa, por lo que no entramos en un ciclo de retención para sí mismo. Y también, evita que el yo se vuelva nulo a mitad de camino, pero aún así disminuye apropiadamente el conteo de retención. Más en este enlace: http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html
Recuerde también que los ciclos de retención pueden ocurrir si su bloque se refiere a otro objeto que luego se conserva.
No estoy seguro de que Garbage Collection pueda ayudar en estos ciclos de retención. Si el objeto que retiene el bloque (que llamaré el objeto del servidor) supera a self
(el objeto del cliente), la referencia al self
dentro del bloque no se considerará cíclica hasta que se libere el objeto de retención. Si el objeto del servidor es mucho mayor que sus clientes, es posible que tenga una pérdida de memoria considerable.
Como no hay soluciones limpias, recomendaría las siguientes soluciones provisionales. Siéntase libre de elegir uno o más de ellos para solucionar su problema.
- Use bloques solo para completar y no para eventos abiertos. Por ejemplo, use bloques para métodos como
doSomethingAndWhenDoneExecuteThisBlock:
y no métodos comosetNotificationHandlerBlock:
Los bloques utilizados para la finalización tienen un final definido de vidas, y deben ser liberados por los objetos del servidor después de ser evaluados. Esto evita que el ciclo de retención dure demasiado tiempo, incluso si ocurre. - Haz ese baile de referencia débil que describes.
- Proporcione un método para limpiar su objeto antes de liberarlo, lo que "desconecta" el objeto de los objetos del servidor que pueden contener referencias al mismo; y llame a este método antes de invocar la liberación del objeto. Si bien este método está perfectamente bien si su objeto solo tiene un cliente (o es un singleton dentro de algún contexto), se descompondrá si tiene varios clientes. Básicamente estás derrotando el mecanismo de retención de conteo aquí; esto es similar a llamar a
dealloc
lugar derelease
.
Si está escribiendo un objeto de servidor, tome los argumentos de bloque solo para completar. No acepte argumentos de bloque para devoluciones de llamadas, como setEventHandlerBlock:
En su lugar, vuelva al patrón de delegado clásico: cree un protocolo formal y anuncie un método setEventDelegate:
. No retenga el delegado. Si ni siquiera desea crear un protocolo formal, acepte un selector como devolución de llamada delegada.
Y, por último, este patrón debería sonar alarmas:
- (void)dealloc { [myServerObject releaseCallbackBlocksForObject:self]; ... }
Si estás tratando de desenganchar bloques que pueden referirse a dealloc
self
desde dentro de dealloc
, ya estás en problemas. dealloc
nunca se puede llamar debido al ciclo de retención causado por las referencias en el bloque, lo que significa que su objeto simplemente va a perder hasta que el objeto del servidor se desasigne.
Solo usa:
__weak id weakSelf = self;
[someObject someMethodWithBlock:^{
[weakSelf someOtherMethod];
}];
Para más información: WWDC 2011 - Bloques y Grand Central Dispatch in Practice .
https://developer.apple.com/videos/wwdc/2011/?id=308
Nota: si eso no funciona, puedes intentar
__weak typeof(self)weakSelf = self;
__block __unsafe_unretained
modificadores sugeridos en la publicación de Kevin puede causar la excepción de acceso incorrecto en caso de bloque ejecutado en un hilo diferente. Es mejor usar solo el modificador __block para la variable temp y hacerlo nil después del uso.
__block SomeType* this = self;
[someObject messageWithBlock:^{
[this doSomething]; // here would be BAD_ACCESS in case of __unsafe_unretained with
// multithreading and self was already released
this = nil;
}];