objective c - ¿Siempre está bien tener una referencia ''fuerte'' para un delegado?
objective-c delegates (3)
Tengo una clase que recupera JSON de una URL y devuelve los datos a través del patrón de protocolo / delegado.
MRDelegateClass.h
#import <Foundation/Foundation.h>
@protocol MRDelegateClassProtocol
@optional
- (void)dataRetrieved:(NSDictionary *)json;
- (void)dataFailed:(NSError *)error;
@end
@interface MRDelegateClass : NSObject
@property (strong) id <MRDelegateClassProtocol> delegate;
- (void)getJSONData;
@end
Tenga en cuenta que estoy usando strong
para mi propiedad de delegado. Más sobre eso más tarde ...
Estoy intentando escribir una clase ''wrapper'' que implementa getJSONData en un formato basado en bloques.
MRBlockWrapperClassForDelegate.h
#import <Foundation/Foundation.h>
typedef void(^SuccessBlock)(NSDictionary *json);
typedef void(^ErrorBlock)(NSError *error);
@interface MRBlockWrapperClassForDelegate : NSObject
+ (void)getJSONWithSuccess:(SuccessBlock)success orError:(ErrorBlock)error;
@end
MRBlockWrapperClassForDelegate.m
#import "MRBlockWrapperClassForDelegate.h"
#import "MRDelegateClass.h"
@interface DelegateBlock:NSObject <MRDelegateClassProtocol>
@property (nonatomic, copy) SuccessBlock successBlock;
@property (nonatomic, copy) ErrorBlock errorBlock;
@end
@implementation DelegateBlock
- (id)initWithSuccessBlock:(SuccessBlock)aSuccessBlock andErrorBlock:(ErrorBlock)aErrorBlock {
self = [super init];
if (self) {
_successBlock = aSuccessBlock;
_errorBlock = aErrorBlock;
}
return self;
}
#pragma mark - <MRDelegateClass> protocols
- (void)dataRetrieved:(NSDictionary *)json {
self.successBlock(json);
}
- (void)dataFailed:(NSError *)error {
self.errorBlock(error);
}
@end
// main class
@interface MRBlockWrapperClassForDelegate()
@end
@implementation MRBlockWrapperClassForDelegate
+ (void)getJSONWithSuccess:(SuccessBlock)success orError:(ErrorBlock)error {
MRDelegateClass *delegateClassInstance = [MRDelegateClass new];
DelegateBlock *delegateBlock = [[DelegateBlock alloc] initWithSuccessBlock:success andErrorBlock:error];
delegateClassInstance.delegate = delegateBlock; // set the delegate as the new delegate block
[delegateClassInstance getJSONData];
}
@end
He llegado al mundo del objetivo-c relativamente recientemente (solo viví en tiempos de ARC, y todavía estoy llegando a un acuerdo con los bloques) y debo admitir que mi comprensión de la gestión de la memoria está en el lado más delgado de las cosas.
Este código parece funcionar bien, pero solo si mi delegado es tan strong
. Entiendo que mi delegado debe ser weak
para evitar posibles ciclos de retención. En cuanto a los instrumentos, encuentro que las asignaciones no continúan creciendo con las llamadas continuadas. Sin embargo, creo que la "mejor práctica" es tener delegados weak
.
Preguntas
P1) ¿Está siempre "bien" tener delegados strong
P2) ¿cómo podría implementar el contenedor basado en bloques dejando al delegado de la clase subyacente como delegado weak
(es decir, evitar que el * delegateBlock se desasigne antes de que reciba los métodos de protocolo)?
Depende totalmente de la arquitectura de tus objetos.
Cuando las personas usan delegados débiles, es porque el delegado suele ser algún tipo de objeto "padre", que retiene lo que tiene el delegado (llamémoslo "delegador"). ¿Por qué tiene que ser un objeto padre? No tiene que ser; sin embargo, en la mayoría de los casos de uso, resulta ser el patrón más conveniente. Dado que el delegado es un objeto principal que retiene al delegador, el delegador no puede retener al delegado o tendrá un ciclo de retención, por lo que tiene una referencia débil al delegado.
Sin embargo, esa no es la única situación de uso. Tomemos, por ejemplo, UIAlertView
y UIActionSheet
en iOS. La forma habitual en que se usan es: dentro de una función, cree una vista de alerta con un mensaje y agregue botones a ella, configure su delegado, realice cualquier otra personalización, haga una llamada de -show
y luego olvídelo (no se almacena en cualquier sitio). Es un tipo de mecanismo de "disparar y olvidar". Una vez que lo show
, no necesitas retenerlo ni nada y aún se mostrará en la pantalla. En algunos casos, es posible que desee almacenar la vista de alerta para poder descartarla mediante programación, pero eso es raro; en la gran mayoría de los casos de uso, simplemente lo muestra y lo olvida, y solo maneja las llamadas de delegado.
Entonces, en este caso, el estilo adecuado sería un delegado fuerte, porque 1) el objeto principal no conserva la vista de alerta, por lo que no hay problema con un ciclo de retención, y 2) el delegado debe mantenerse cerca, por lo que cuando se presiona algún botón en la vista de alerta, alguien estará alrededor para responderle. Ahora, muchas veces, el # 2 no es un problema porque el delegado (objeto principal) es algún tipo de controlador de vista o algo que de otra manera es retenido por otra cosa. Pero este no es siempre el caso. Por ejemplo, simplemente puedo tener un método que no forma parte de ningún controlador de vista, al que cualquiera puede llamar para mostrar una vista de alerta, y si el usuario presiona Sí, carga algo en el servidor. Dado que no es parte de ningún controlador, es probable que no quede retenido por nada. Pero debe permanecer el tiempo suficiente hasta que se complete la vista de alerta. Así que, idealmente, la vista de alerta debería tener una fuerte referencia a ella.
Pero como he mencionado antes, esto no siempre es lo que desea para una vista de alerta; A veces quieres mantenerlo cerca y descartarlo programáticamente. En este caso, desea un delegado débil o causará un ciclo de retención. Entonces, ¿debería una vista de alerta tener un delegado fuerte o débil? Bueno, la persona que llama debe decidir! En algunas situaciones, la persona que llama quiere fuerte; en otros, la persona que llama quiere débil. Pero, ¿cómo es esto posible? El delegado de vista de alerta está declarado por la clase de vista de alerta y debe declararse como fuerte o débil.
Afortunadamente, hay una solución que permite que la persona que llama decida: una devolución de llamada basada en bloques . En una API basada en bloques, el bloque se convierte esencialmente en el delegado; pero el bloque no es el objeto padre. Por lo general, el bloque se crea en la clase de llamada y se captura a self
para que pueda realizar acciones en el "objeto principal". El delegador (vista de alerta en este caso) siempre tiene una fuerte referencia al bloque. Sin embargo, el bloque puede tener una referencia fuerte o débil al objeto principal, dependiendo de cómo se escriba el bloque en el código de llamada (para capturar una referencia débil al objeto principal, no se utilice self
directamente en el bloque, y en su lugar , cree una versión débil de self
fuera del bloque, y deje que el bloque use eso en su lugar). De esta manera, el código que llama controla completamente si el delegador tiene una referencia fuerte o débil a él.
Q1 - Sí. Como se señala, tener propiedades de delegado débiles es una recomendación para ayudar a evitar los ciclos de retención. Por lo tanto, no hay nada de malo en tener un delegado fuerte, pero si los clientes de su clase esperan que sea débil, puede causarles sorpresas. El mejor enfoque es mantener al delegado débil y para el lado del servidor (la clase con la propiedad del delegado) mantener internamente una referencia sólida para los períodos en que la necesita. Como @Scott señala los documentos de Apple que hacen esto para NSURLConnection
. Por supuesto, ese enfoque no resuelve su problema, donde desea que el servidor conserve al delegado para usted ...
P2: desde el lado del cliente, el problema es cómo mantener vivo a un delegado siempre que un servidor con una referencia débil lo requiera. Hay una solución estándar para este problema llamada objetos asociados . En resumen, el tiempo de ejecución de Objective-C esencialmente permite que una colección de objetos clave se asocie con otro objeto, junto con una política de asociación que indique cuánto debe durar esa asociación. Para usar este mecanismo, solo tiene que elegir su propia clave única, que es de tipo void *
, es decir, una dirección . El siguiente esquema de código muestra cómo usar esto usando NSOpenPanel
como ejemplo:
#import <objc/runtime.h> // import associated object functions
static char myUniqueKey; // the address of this variable is going to be unique
NSOpenPanel *panel = [NSOpenPanel openPanel];
MyOpenPanelDelegate *myDelegate = [MyOpenPanelDelegate new];
// associate the delegate with the panel so it lives just as long as the panel itself
objc_setAssociatedObject(panel, &myUniqueKey, myDelegate, OBJC_ASSOCIATION_RETAIN);
// assign as the panel delegate
[panel setDelegate:myDelegate];
La política de asociación OBJC_ASSOCIATION_RETAIN
conservará el objeto pasado ( myDelegate
) mientras el objeto con el que esté asociado ( panel
) y luego lo libere.
La adopción de esta solución evita que la propiedad del delegado sea fuerte y le permite al cliente controlar si se retiene el delegado. Por supuesto, si también está implementando el servidor, puede proporcionar un método para hacer esto, tal vez, objc_setAssociatedObject
associatedDelegate:
?, Para evitar que el cliente tenga que definir la clave y llamar a objc_setAssociatedObject
. (O puede agregarlo a una clase existente usando una categoría).
HTH.
Tienes razón en que los delegados suelen tener una referencia débil. Sin embargo, hay casos de uso donde se prefiere una referencia fuerte, o incluso necesaria. Apple usa esto en NSURLConnection :
Durante una descarga, la conexión mantiene una fuerte referencia al delegado. Libera esa referencia fuerte cuando la conexión termina de cargarse, falla o se cancela.
Una instancia de NSURLConnection
solo puede usarse una vez. Una vez que finaliza (ya sea con error o éxito), libera al delegado y, dado que el delegado es de readonly
, no puede ser reutilizado (de manera segura).
Puedes hacer algo similar. En sus métodos dataRetrieved
y dataFailed
, establezca su delegado en nil
. Es probable que no necesite hacer que su delegado sea de readonly
si desea reutilizar su objeto, pero tendrá que asignarlo nuevamente.