tipos - ¿Cuáles son las semánticas de propiedad de referencia de ReactiveCocoa?
semantica ejemplos (2)
Estoy tratando de resolver el misterio de gestión de memoria de ReactiveCocoa 2.5
RACSubject* subject = [RACSubject subject];
RACSignal* signal = [RACSignal return:@(1)];
NSLog(@"Retain count of RACSubject %ld", CFGetRetainCount((__bridge CFTypeRef)subject));
NSLog(@"Retain count of RACSignal %ld", CFGetRetainCount((__bridge CFTypeRef)signal));
La primera línea de salida 1
, y la segunda línea de salida 2
. Parece que RACSignal
se RACSignal
algún lugar, mientras que RACSubject
no lo es. Si no retiene RACSubject
explícitamente, será desasignado cuando el programa salga del alcance actual.
Cuando creo una señal y la llevo al alcance de una función, su conteo de retención efectiva es 0 por convenciones de Cocoa:
RACSignal *signal = [self createSignal];
Cuando me suscribo a la señal, retiene al suscriptor y devuelve un desechable que, según las convenciones de Cocoa, también tiene una cuenta de retención de cero.
RACDisposable *disposable = [signal subscribeCompleted:^ {
doSomethingPossiblyInvolving(self);
}];
La mayoría de las veces, el suscriptor se cerrará y hará referencia a self
o a sus ivars o a alguna otra parte del ámbito de aplicación. Así que cuando se suscribe a una señal, la señal tiene una referencia de propiedad del suscriptor y el suscriptor tiene una referencia de propiedad para usted. Y el desechable que recibe a cambio tiene una referencia de propiedad a la señal.
disposable -> signal -> subscriber -> calling scope
Supongamos que mantiene ese desechable para poder cancelar su suscripción en algún momento (por ejemplo, si la señal está recuperando datos de un servicio web y el usuario navega fuera de la pantalla, cancelando su intención de ver los datos que se están recuperando).
self.disposeToCancelWebRequest = disposable;
En este punto tenemos una referencia circular:
calling scope -> disposable -> signal -> subscriber -> calling scope
Lo responsable es asegurarse de que el ciclo se rompa al cancelar una solicitud o después de que una solicitud haya finalizado.
[self.disposeToCancelWebRequest dispose]
self.disposeToCancelWebRequest = nil;
Tenga en cuenta que no puede hacer esto cuando se self
asigna, porque eso nunca sucederá debido al ciclo de retención. Algo también parece sospechoso de romper el ciclo de retención durante una devolución de llamada al suscriptor, ya que la señal podría potencialmente ser desasignada mientras su implementación aún está en la pila de llamadas.
También me doy cuenta de que la implementación conserva una lista global de procesos de señales activas (en el momento en que originalmente formulé esta pregunta).
¿Cómo debo pensar en la propiedad al usar RAC?
Para ser honesto, la administración de la memoria de ReactiveCocoa es bastante compleja, pero el resultado final valioso es que no es necesario retener las señales para procesarlas .
Si el marco requiere que retenga cada señal, sería mucho más difícil de usar, especialmente para señales de un disparo que se usan como futuros (por ejemplo, solicitudes de red). Tendría que guardar cualquier señal de larga duración en una propiedad y luego asegurarse de borrarla cuando haya terminado con ella. No es divertido.
Suscriptores
Antes de continuar, debo señalar que subscribeNext:error:completed:
(y todas sus variantes) crea un suscriptor implícito utilizando los bloques dados. Todos los objetos a los que se hace referencia desde esos bloques se conservarán como parte de la suscripción. Al igual que cualquier otro objeto, el self
no se retendrá sin una referencia directa o indirecta a él.
(Basado en la redacción de su pregunta, creo que ya sabía esto, pero podría ser útil para otros).
Señales finitas o de corta duración
La guía más importante para la administración de la memoria del RAC es que una suscripción se termina automáticamente una vez que se completa o se produce un error, y se elimina el suscriptor . Para usar su ejemplo de referencia circular:
calling scope -> disposable -> signal -> subscriber -> calling scope
... esto significa que la signal -> subscriber
se interrumpe tan pronto como finaliza la signal
, rompiendo el ciclo de retención.
A menudo, esto es todo lo que necesita , porque la vida útil de la RACSignal
en la memoria coincidirá naturalmente con la vida lógica del flujo de eventos.
Señales infinitas
Sin embargo, las señales infinitas (o señales que viven tanto que podrían ser infinitas) nunca se derrumbarán de forma natural. Aquí es donde brillan los desechables.
La eliminación de una suscripción eliminará al suscriptor asociado y, en general, solo eliminará los recursos asociados con esa suscripción. Para ese suscriptor, es como si la señal se hubiera completado o se hubiera producido un error, excepto que no se envía ningún evento final sobre la señal. Todos los demás suscriptores permanecerán intactos.
Sin embargo, como regla general, si tiene que administrar manualmente el ciclo de vida de una suscripción, probablemente haya una mejor manera de hacer lo que quiere. Métodos como -take:
o -takeUntil:
manejarán la eliminación por usted, y terminará con una abstracción de nivel superior.
Señales derivadas de self
Sin embargo, todavía hay un poco de un caso medio complicado aquí. Cada vez que la vida útil de una señal esté vinculada al ámbito de la llamada, tendrá un ciclo mucho más difícil de interrumpir.
Esto ocurre comúnmente cuando se usa RACAble()
o RACAbleWithStart()
en una ruta clave que es relativa a self
, y luego se aplica un bloque que necesita capturarse a self
.
La respuesta más fácil aquí es simplemente capturarse débilmente :
__weak id weakSelf = self;
[RACAble(self.username) subscribeNext:^(NSString *username) {
id strongSelf = weakSelf;
[strongSelf validateUsername];
}];
O, después de importar el encabezado EXTScope.h incluido:
@weakify(self);
[RACAble(self.username) subscribeNext:^(NSString *username) {
@strongify(self);
[self validateUsername];
}];
(Reemplace __weak
o @weakify
con __unsafe_unretained
o @unsafeify
, respectivamente, si el objeto no admite referencias débiles.)
Sin embargo, probablemente hay un mejor patrón que podrías usar en su lugar. Por ejemplo, la muestra anterior tal vez podría escribirse como:
[self rac_liftSelector:@selector(validateUsername:)
withObjects:RACAble(self.username)];
o:
RACSignal *validated = [RACAble(self.username) map:^(NSString *username) {
// Put validation logic here.
return @YES;
}];
Al igual que con las señales infinitas, en general hay formas en que puede evitar hacer referencia a self
(o cualquier objeto) de bloques en una cadena de señales.
La información anterior es realmente todo lo que necesita para utilizar ReactiveCocoa de manera efectiva. Sin embargo, quiero abordar un punto más, solo para los que tengan curiosidad técnica o para cualquier persona interesada en contribuir al RAC:
También me doy cuenta de que la implementación conserva una lista global de procesos de señales activas.
Esto es absolutamente cierto.
El objetivo de diseño de "no es necesario retenerlo" plantea la pregunta: ¿cómo sabemos cuándo se debe desasignar una señal? ¿Qué sucede si se acaba de crear, se escapó de un grupo de autorelease y aún no se ha conservado?
La respuesta real es que no , PERO generalmente podemos suponer que la persona que llama conservará la señal dentro de la iteración del bucle de ejecución actual si desea mantenerla.
Por consiguiente:
- Una señal creada se agrega automáticamente a un conjunto global de señales activas.
- La señal esperará un solo paso del bucle de ejecución principal y luego se eliminará del conjunto activo si no tiene suscriptores . A menos que la señal fuera retenida de alguna manera, se desasignaría en este punto.
- Si algo se suscribió en esa iteración de bucle de ejecución, la señal permanece en el conjunto.
- Más tarde, cuando todos los suscriptores se han ido, el # 2 se dispara de nuevo.
Esto podría ser contraproducente si el bucle de ejecución se realiza de forma recursiva (como en un bucle de evento modal en OS X), pero hace que la vida del consumidor del marco sea mucho más fácil para la mayoría o para todos los demás casos.