objective c - Pruebas unitarias para la gestión de la memoria en Cocoa/Objective-C
unit-testing memory-management (3)
¿Cómo escribiría un OCUnit prueba de unidad, por ejemplo, para garantizar que los objetos se liberan / conservan correctamente en Cocoa / Objective-C?
Una forma ingenua de hacer esto sería verificar el valor de retainCount
, pero por supuesto nunca deberías usar retainCount
. ¿Puedes simplemente verificar si a la referencia de un objeto se le asigna un valor nil
para indicar que se ha liberado? Además, ¿qué garantías tiene sobre el momento en que los objetos se desasignan?
Estoy esperando una solución concisa de solo unas pocas líneas de código, ya que probablemente la use extensamente. En realidad, puede haber dos respuestas: una que usa el grupo de autorrelease y otra que no.
Para aclarar, no estoy buscando una manera de probar exhaustivamente cada objeto que creo. Es imposible probar de forma unitaria cualquier comportamiento de manera integral, y mucho menos manejar la memoria. Sin embargo, como mínimo, sería bueno comprobar el comportamiento de los objetos liberados para las pruebas de regresión (y asegurarse de que el mismo error relacionado con la memoria no ocurra dos veces).
Sobre las respuestas
Acepté la answer BJ Homer porque me pareció la forma más fácil y concisa de lograr lo que tenía en mente, dado que era necesario que los punteros débiles proporcionados con Conteo automático de referencias no estuvieran disponibles en las versiones de producción de XCode (anterior a 4.2?) a partir del 23 de julio de 2011. También me impresionó saber que
ARC se puede habilitar por archivo; no requiere que todo su proyecto lo use. Puede compilar sus pruebas de unidad con ARC y dejar su proyecto principal en relanzamiento manual, y esta prueba aún funcionaría.
Dicho esto, para una exploración mucho más detallada de los posibles problemas relacionados con la administración de la memoria de pruebas unitarias en Objective-C, recomiendo encarecidamente la respuesta en profundidad de Peter Hosey .
¿Puedes simplemente verificar si a la referencia de un objeto se le asigna un valor
nil
para indicar que se ha liberado?
No, porque enviar un mensaje de release
a un objeto y asignarle nil
a una variable son dos cosas diferentes y no relacionadas.
Lo más cercano que puede obtener es que la asignación de cualquier elemento a una propiedad fuerte / de retención o de copia, que se traduce en un mensaje de acceso, hace que se libere el valor anterior de la propiedad (lo que hace el usuario). Aun así, observar el valor de la propiedad -utilizando KVO, por ejemplo- no significa que sabrá cuándo se libera el objeto; más especialmente, cuando el objeto propietario es desasignado, no recibirá una notificación cuando envíe el release
directamente al objeto propiedad. También recibirá un mensaje de advertencia en su consola (porque el objeto propietario murió mientras lo estaba observando), y no quiere mensajes de advertencia ruidosos de una prueba unitaria. Además, tendrías que observar específicamente cada propiedad de cada objeto para sacarlo de la cuenta, y es posible que te falte un error.
Un mensaje de release
a un objeto no tiene efecto sobre las variables que apuntan a ese objeto. Tampoco lo hace la desasignación.
Esto cambia ligeramente en ARC : las variables de referencia débil se asignarán automáticamente nil
cuando el objeto al que se hace referencia desaparezca. Eso no te ayuda mucho, sin embargo, porque las variables de referencia fuerte, por definición, no: Si hay una fuerte referencia al objeto, el objeto no desaparecerá (bueno, no debería ), porque la referencia fuerte lo mantendré (debería) con vida. Un objeto que muere antes que debería es uno de los problemas que estás buscando, no algo que desees utilizar como herramienta.
En teoría, podría crear una referencia débil a cada objeto que cree, pero debería referirse a cada objeto específicamente, creando una variable para él manualmente en su código. Como se puede imaginar, un dolor tremendo y seguro de perder objetos.
Además, ¿qué garantías tienes sobre el momento en que se lanzan los objetos?
Un objeto se libera enviándole un mensaje de release
, por lo que el objeto se libera cuando recibe ese mensaje.
Quizás quisiste decir "desasignado". La liberación simplemente lo acerca a ese punto; un objeto puede liberarse muchas veces y aún tener una larga vida por delante si cada lanzamiento simplemente equilibra un retener anterior.
Un objeto es desasignado cuando es lanzado por última vez. Esto sucede inmediatamente. El infame retainCount
ni siquiera baja a 0, como una persona inteligente que intentó escribir while ([obj retainCount] > 0) [obj release];
ha descubierto.
En realidad, puede haber dos respuestas: una que usa el grupo de autorrelease y otra que no.
Una solución que usa el grupo de autorrelease solo funciona para objetos que se liberan automáticamente; por definición, los objetos que no se liberan automáticamente no entran en el grupo. Es completamente válido, y en ocasiones deseable, nunca liberar automáticamente ciertos objetos (especialmente aquellos que crea muchos miles). Además, no puedes mirar dentro de la agrupación para ver qué contiene y qué no, o intentar golpear cada objeto para ver si está muerto.
¿Cómo escribiría un OCUnit de prueba de unidad, por ejemplo, para garantizar que los objetos se liberan / conservan correctamente en Cocoa / Objective-C?
Lo mejor que puedes hacer es configurar NSZombieEnabled
en YES
en setUp
y restablecer su valor anterior en tearDown
. Esto atrapará liberaciones excesivas / sub-retenciones, pero no fugas de ningún tipo.
Incluso si pudiera escribir una prueba unitaria que pruebe exhaustivamente la administración de la memoria, aún sería imperfecta porque solo puede probar los objetos del modelo de código comprobables y tal vez ciertos controladores. Todavía podría tener fugas y bloqueos en su aplicación causados por el código de vista, las referencias a base de nib y ciertas opciones ("Release When Closed" viene a la mente), y así sucesivamente.
No hay pruebas fuera de aplicación que pueda escribir que garanticen que su aplicación esté libre de errores de memoria.
Dicho esto, una prueba como la que estás imaginando, si fuera autónoma y automática, sería genial, incluso si no pudiera probar todo. Así que espero estar equivocado y hay una manera.
Posiblemente esto no es lo que estás buscando, pero como experimento mental, me pregunté si esto podría hacer algo parecido a lo que deseas: ¿qué pasaría si creas un mecanismo para rastrear el comportamiento de retención / liberación de objetos particulares que deseas probar? Trabaja algo como esto:
- crear una anulación de NSObject dealloc
- cree un
CFMutableSetRef
y configure una función personalizada de retención / liberación para no hacer nada - hacer una rutina de prueba unitaria como
registerForRRTracking: (id) object
- realice una rutina de prueba de unidad como
clearRRTrackingReportingLeaks: (BOOL) report
que informará cualquier objeto en el conjunto en ese momento. - llame al
[tracker clearRRTrackignReportingLeaks: NO];
al comienzo de su prueba de unidad - llama al método de registro en tu prueba unitaria para cada objeto que quieras rastrear y se eliminará automáticamente en dealloc.
- Al final de su prueba, llame al
[tracker clearRRTrackingReportingLeaks: YES];
y enumerará todos los objetos que no se eliminaron correctamente.
también puedes anular la NSObject alloc
y rastrear todo, pero imagino que tu conjunto se NSObject alloc
demasiado grande (!!!).
Aún mejor sería poner CFMutableSetRef
en un proceso separado y, por lo tanto, no afectar demasiado a la huella de memoria de tiempo de ejecución del programa. Agrega la complejidad y el tiempo de ejecución de la comunicación entre procesos. Podría usar un montón privado (o zona, ¿todavía existen los que existen?) Para aislarlo en menor grado.
Si puede usar el recuento automático de referencias recientemente introducido (aún no disponible en las versiones de producción de Xcode, pero documentado aquí ), entonces podría utilizar punteros débiles para comprobar si algo se retuvo en exceso.
- (void)testMemory {
__weak id testingPointer = nil;
id someObject = // some object with a ''foo'' property
@autoreleasepool {
// Point the weak pointer to the thing we expect to be dealloc''d
// when we''re done.
id theFoo = [someObject theFoo];
testingPointer = theFoo;
[someObject setTheFoo:somethingElse];
// At this point, we still have a reference to ''theFoo'',
// so ''testingPointer'' is still valid. We need to nil it out.
STAssertNotNil(testingPointer, @"This will never happen, since we''re still holding it.")
theFoo = nil;
}
// Now the last strong reference to ''theFoo'' should be gone, so ''testingPointer'' will revert to nil
STAssertNil(testingPointer, @"Something didn''t release %@ when it should have", testingPointer);
}
Tenga en cuenta que esto funciona bajo ARC debido a este cambio en la semántica del lenguaje:
Un puntero de objeto retenible es un puntero nulo o un puntero a un objeto válido.
Por lo tanto, el acto de establecer un puntero a nil garantiza liberar el objeto al que apunta, y no hay forma (bajo ARC) de liberar un objeto sin quitarle un puntero.
Una cosa a tener en cuenta es que ARC se puede habilitar por archivo; no requiere que todo su proyecto lo use. Puede compilar sus pruebas de unidad con ARC y dejar su proyecto principal en relanzamiento manual, y esta prueba aún funcionaría.
Lo anterior no detecta el exceso de liberación, pero de todos modos es bastante fácil de detectar con NSZombieEnabled
.
Si ARC simplemente no es una opción, es posible que pueda hacer algo similar con MAZeroingWeakRef
Mike Ash. No lo he usado mucho, pero parece proporcionar una funcionalidad similar a los indicadores __weak de una manera compatible con versiones anteriores.