Retener el ciclo en `self` con bloques

167

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 variables locales a las que se hace referencia como constcopias, el uso selfdentro de un bloque puede dar como resultado un ciclo de retención, en caso de que ese bloque se copie. Por lo tanto, se supone que debemos usar __blockpara obligar al bloque a tratar directamente en selflugar de copiarlo.

__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];

en lugar de solo

[someObject messageWithBlock:^{ [self doSomething]; }];

Lo que me gustaría saber es lo siguiente: si esto es cierto, ¿hay alguna manera de evitar la fealdad (aparte de usar GC)?

Jonathan Sterling
fuente
3
Me gusta llamar a mis selfrepresentantes thissolo para cambiar las cosas. En JavaScript llamo a mis thiscierres self, por lo que se siente agradable y equilibrado. :)
devios1
Me pregunto si es necesario realizar alguna acción equivalente si estoy usando bloques Swift
Ben Lu
@BenLu absolutamente! en los cierres Swift (y las funciones que se pasan que mencionan self implícita o explícitamente) retendrán self. A veces esto es deseable, y otras veces crea un ciclo (porque el cierre en sí mismo es propiedad de uno mismo (o propiedad de algo propio). La razón principal por la que esto sucede es por ARC.
Ethan
1
Para evitar problemas, la forma adecuada de definir 'self' para usar en un bloque es '__typeof (self) __weak weakSelf = self;' para tener una referencia débil.
XLE_22

Respuestas:

169

Estrictamente hablando, el hecho de que sea una copia constante no tiene nada que ver con este problema. Los bloques retendrán cualquier valor obj-c que se capture cuando se creen. Sucede que la solución para el problema de copia constante es idéntica a la solución para el problema de retención; a saber, usar la __blockclase de almacenamiento 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 selfin 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.


Anexo: Al compilar como ARC, __blockya no interrumpe los ciclos de retención. Si está compilando para ARC, debe usar __weako en su __unsafe_unretainedlugar.

Lily Ballard
fuente
¡No hay problema! Si esto respondiera la pregunta a su satisfacción, le agradecería si pudiera elegir esta como la respuesta correcta a su pregunta. Si no es así, hágame saber cómo puedo responder mejor a su pregunta.
Lily Ballard
44
No hay problema, Kevin. SO demora la selección de una respuesta a una pregunta de inmediato, por lo que tuve que regresar un poco más tarde. Salud.
Jonathan Sterling
__unsafe_unretained id bself = self;
caleb
44
@JKLaiho: Claro, también __weakestá bien. Si sabe con certeza que el objeto no puede estar fuera del alcance cuando se invoca el bloque, entonces __unsafe_unretainedes un poco más rápido, pero en general eso no hará la diferencia. Si lo usa __weak, asegúrese de incluirlo en una __strongvariable local y probarlo nilantes de hacer algo con él.
Lily Ballard
2
@Rpranata: Sí. __blockEl efecto secundario de no retener y liberar se debió únicamente a la incapacidad de razonar adecuadamente sobre eso. Con ARC, el compilador ganó esa habilidad, y __blockahora retiene y lanza. Si necesita evitar eso, debe usarlo __unsafe_unretained, lo que le indica al compilador que no realice ninguna retención o liberación en el valor de la variable.
Lily Ballard
66

Solo usa:

__weak id weakSelf = self;

[someObject someMethodWithBlock:^{
    [weakSelf someOtherMethod];
}];

Para más información: WWDC 2011 - Bloques y Grand Central Dispatch en la práctica .

https://developer.apple.com/videos/wwdc/2011/?id=308

Nota: si eso no funciona, puedes probar

__weak typeof(self)weakSelf = self;
3lvis
fuente
2
¿Y por casualidad lo encontraste :)?
Tieme
2
Puede consultar el video aquí: developer.apple.com/videos/wwdc/2011/…
nanjunda
¿Puede hacer referencia a sí mismo dentro de "someOtherMethod"? ¿Se auto referenciaría al debilitador en ese punto o eso también crearía un ciclo de retención?
Oren
Hola @Oren, si intentas hacer referencia a ti mismo dentro de "someOtherMethod", recibirás una advertencia de Xcode. Mi enfoque solo hace una referencia débil de sí mismo.
3lvis
1
Solo recibí una advertencia cuando me refería a mí mismo directamente dentro del bloque. Ponerse dentro de someOtherMethod no causó ninguna advertencia. ¿Es porque xcode no es lo suficientemente inteligente o no es un problema? ¿Hacer referencia a uno mismo dentro de algún otro Método ya se referiría a débil Sí mismo ya que es a eso a lo que está llamando el método?
Oren
22

Esto puede ser obvio, pero solo tiene que hacer el selfalias feo cuando sabe que obtendrá un ciclo de retención. Si el bloqueo es solo de una sola vez, creo que puede ignorar con seguridad la retención self. El mal caso es cuando tiene el bloque como 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 selfaliasing. Obtendrá un ciclo de retención, pero la operación es de corta duración y el bloque eventualmente se quedará sin memoria, rompiendo el ciclo. Pero mi experiencia con los bloques es muy pequeña y podría ser que el selfalias salga como una mejor práctica a largo plazo.

zoul
fuente
66
Buen punto. Es solo un ciclo de retención si uno mismo mantiene vivo el bloqueo. En el caso de bloques que nunca se copian, o bloques con una duración limitada garantizada (por ejemplo, el bloque de finalización para una animación UIView), no tiene que preocuparse por eso.
Lily Ballard
66
En principio, tienes razón. Sin embargo, si tuviera que ejecutar el código en el ejemplo, se bloquearía. Las propiedades de bloque siempre deben declararse como copy, no retain. Si son justos retain, entonces no hay garantía de que se moverán de la pila, lo que significa que cuando vayas a ejecutarlo, ya no estará allí. (y la copia y el bloque ya copiado está optimizado para retener)
Dave DeLong
Ah, claro, un error tipográfico. Pasé por la retainfase hace un tiempo y rápidamente me di cuenta de lo que estás diciendo :) ¡Gracias!
zoul
Estoy bastante seguro de que retainse ignora por completo para los bloques (a menos que ya se hayan movido de la pila con copy).
Steven Fisher
@Dave DeLong, No, no se bloqueará ya que @property (retener) se usa solo para una referencia de objeto, no para el bloque. No hay necesidad de utilizar una copia aquí ...
DeniziOS
20

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 autorreferencia dentro de un bloque. Este no es el caso, es solo cuando el objeto en sí tiene un bloque. Y, de hecho, si usa blockSelf en estos casos, el objeto puede ser desasignado antes de recuperar el resultado del bloque y luego se bloqueará cuando intente llamarlo, por lo que claramente desea que se retenga hasta la respuesta Vuelve.

El primer caso demuestra cuándo ocurrirá 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 realiza la llamada no tiene un bloque que causará un ciclo de retención cuando haga referencia a self:

#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;
} 
poseer
fuente
Esa es una idea errónea común y puede ser peligrosa, porque los bloques que deberían retener selfpueden no deberse a que las personas apliquen en exceso esta solución. Este es un buen ejemplo de cómo evitar los ciclos de retención en el código que no es ARC, gracias por publicar.
Carl Veazey
9

Recuerde también que los ciclos de retención pueden ocurrir si su bloqueo se refiere a otro objeto que luego retieneself .

No estoy seguro de que la recolección de basura pueda ayudar en estos ciclos de retención. Si el objeto que retiene el bloque (que llamaré el objeto del servidor) sobrevive self(el objeto del cliente), la referencia aself interior del bloque no se considerará cíclica hasta que se libere el objeto de retención. Si el objeto del servidor supera a sus clientes, es posible que tenga una pérdida de memoria significativa.

Como no hay soluciones limpias, recomendaría las siguientes soluciones. 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 como setNotificationHandlerBlock:. Los bloques utilizados para la finalización tienen un final definitivo de vida útil, y los objetos del servidor deben liberarlos después de evaluarlos. Esto evita que el ciclo de retención viva demasiado tiempo, incluso si ocurre.
  • Haz ese baile de referencia débil que describiste.
  • Proporcione un método para limpiar su objeto antes de liberarlo, que "desconecta" el objeto de los objetos del servidor que pueden contener referencias a él; y llame a este método antes de llamar a release en el 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), pero se descompondrá si tiene varios clientes. Básicamente estás derrotando el mecanismo de conteo de retención aquí; Esto es similar a llamar en dealloclugar de release.

Si está escribiendo un objeto de servidor, tome argumentos de bloque solo para completar. No acepte argumentos de bloque para devoluciones de llamada, como setEventHandlerBlock:. En cambio, recurra al patrón de delegado clásico: cree un protocolo formal y anuncie un setEventDelegate:método. No retener al 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 hacer sonar las alarmas:

- (nulo) dealloc {
    [myServerObject releaseCallbackBlocksForObject: self];
    ...
}

Si está tratando de desenganchar bloques que pueden referirse selfdesde adentro dealloc, ya está en problemas. deallocEs posible que nunca se llame debido al ciclo de retención causado por las referencias en el bloque, lo que significa que su objeto simplemente se filtrará hasta que el objeto del servidor se desasigne.

Dave R
fuente
GC ayuda si lo usa __weakadecuadamente.
tc.
El seguimiento de la recolección de basura puede, por supuesto, tratar con ciclos de retención. Los ciclos de retención son solo un problema para los entornos de recuento de referencia
newacct
Para que todos lo sepan, la recolección de basura se desactivó en OS X v10.8 a favor del Conteo automático de referencias (ARC) y está programado para eliminarse en una versión futura de OS X ( developer.apple.com/library/mac/#releasenotes / ObjectiveC / ... ).
Ricardo Sanchez-Saez
1

__block __unsafe_unretainedlos modificadores sugeridos en la publicación de Kevin pueden causar una excepción de acceso incorrecto en caso de que el bloque se ejecute en un hilo diferente. Es mejor usar solo el modificador __block para la variable temporal y hacerlo nulo 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;
}];
b1gbr0
fuente
¿No sería más seguro usar __weak en lugar de __block para evitar la necesidad de anular la variable después de usarla? Quiero decir, esta solución es excelente si quieres romper otros tipos de ciclos, pero ciertamente no veo ninguna ventaja particular para los ciclos de retención "propios".
ale0xB
No puede usar __weak si el objetivo de su plataforma es iOS 4.x. También a veces necesita que el código en el bloque se haya ejecutado para el objeto válido, no para cero.
b1gbr0
1

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 directa fuerte para que no entremos en un ciclo de retención para uno mismo. Y también, evita que el yo se vuelva nulo a mitad de camino, pero aún así disminuye adecuadamente 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

Yuri Solodkin
fuente
1
Antes de mostrar el código simplificado, sería mejor saber qué hay detrás, todos deberían conocer las dos líneas de código reales.
Alex Cio
0

¿Qué tal esto?

- (void) foo {
     __weak __block me = self;

     myBlock = ^ {
        [[me someProp] someMessage];
     }
     ...
 }

Ya no recibo la advertencia del compilador.

Καrτhικ
fuente
-1

Bloque: se producirá un ciclo de retención porque contiene un bloque al que se hace referencia en el bloque; Si realiza la copia en bloque y utiliza una variable miembro, self retendrá.

yijiankaka
fuente