ios - Gestión de memoria/recursos utilizando MonoTouch y MonoTouch.Dialog
memory-management xamarin.ios (1)
He pasado un par de días en el código fuente de MT.D y en el generador de perfiles. Aunque sigo buscando orientación general sobre cuál es el mejor patrón de diseño para implementar DidReceiveMemoryWarning y ViewDidUnload, tengo algunas observaciones generales para compartir que podrían ser útiles para alguien:
- MonoTouch.Dialog se comporta muy bien. No filtra ningún recurso bajo uso ordinario. Mantiene un árbol de control bajo DVC.Root, y cada método de Disposición de los Elementos desecha correctamente el control UIKit subyacente. Ni siquiera tiene que preocuparse por deshacerse de un RootElement antiguo si ha reemplazado DVC.Root: el creador de propiedades lo desecha automáticamente por usted. En general, MT.D no parece sufrir ningún problema de memoria significativo. Hay una excepción - ver más abajo.
- Al crear sus propios elementos personalizados (por ejemplo, MultilineEntryElement), asegúrese de anular el método Dispose (bool), eliminando el control UIKit subyacente (por ejemplo, UITextView), y encadene el método de clase base Dispose (). El código fuente en el proyecto de mithub MT.D de Miguel proporciona muchos buenos ejemplos. Todos los Elementos implementan el patrón de Dispose estándar (aunque omiten un destructor / finalizador que llama Dispose (falso)).
- Al implementar controladores de vista personalizados, generalmente no es necesario implementar las subclases Dispose en UIViewController, ni en las clases TableView DataSource o Delegate. Cuando el controlador de la vista obtiene GC''ed, llamará correctamente a Dispose en sus referencias. Todas las celdas que asigne en el DataSource serán eliminadas adecuadamente.
- Como excepción a (3): encontré un problema desagradable al agregar mi propia subvista a la celda de TableView. Esta subvista es un control que creé llamado "UICheckbox" que finalmente se hereda de UIImageView, que tiene dos UIImages (activado y desactivado) y un evento público llamado Clicked. Solo tengo un problema cuando un controlador de eventos que hace referencia a miembros de DataSource está conectado a este evento (si el controlador de eventos no hace referencia a DataSource o al controlador en sí, todo está bien). Sin embargo, cuando se cumplen las condiciones anteriores y se despide el controlador, parece que hay algún ciclo que el GC no puede resolver, y cada UICheckbox que coloco en el TableView se filtra (junto con sus imágenes). La única forma de solucionar esto fue agregar código a ViewDidDisappear para deshacerse del ViewController y limpiar su estado IFF, ya no está en ninguna parte de la pila de navegación. Es hacky pero funciona.
En general, me adhiero a la siguiente plantilla para asignar objetos en mis controladores de vista:
- no asigne nada en el constructor (utilícelo solo para pasar el estado)
- cree un árbol de control en ViewDidLoad (y deséchelo en ViewDidUnload). piense "InitializeComponent" en XAML (si eso ayuda). Si UIViewController va a colocar un DialogViewController en la pila de navegación, ViewDidLoad es un buen lugar para crear el DVC.
- Inicialice los valores en el árbol de control en ViewDidAppear. Por ejemplo, puede agregar / eliminar / reemplazar elementos, secciones e incluso la raíz del DVC en este método. Pero no cree un nuevo DVC.
Hay un problema general con la pérdida de ViewControllers cuando el usuario navega por la pila de navegación (hago referencia al enlace bugzilla en la "Actualización" en la pregunta). Esto también afecta a MT.D. Hay una solución bastante sencilla: agregue la siguiente línea de código en ViewDidAppear del controlador de vista principal:
// HACK: touch the ViewControllers array to refresh it (in case the user popped the nav stack) // this is to work around a bug in monotouch (https://bugzilla.xamarin.com/show_bug.cgi?id=1889) // where the UINavigationController leaks UIViewControllers when the user pops the nav stack int count = this.NavigationController.ViewControllers.Length;
Rolf hace un gran trabajo explicando por qué ocurre este error y por qué la solución funciona en el enlace bugzilla, así que no lo repetiré.
Espero que alguien encuentre esto útil. También espero que alguien más inteligente que yo tenga alguna guía sobre cómo manejar DidReceiveMemoryWarning y cómo dividir el trabajo entre ese método y ViewDidUnload.
Actualización : Un par de notas más:
- Ahora me doy cuenta del protocolo para DidReceiveMemoryWarning y ViewDidUnload: el primero siempre se entrega a cada controlador de vista, mientras que el último solo se envía a los controladores de vista que no se muestran actualmente, Y no son más profundos que la raíz de la pila de navegación. Al final, decidí ignorar DidReceiveMemoryWarning porque realmente no tengo imágenes que almacene en la memoria caché y que pueda volcar (según la guía de iOS). En ViewDidUnload, libero todos los recursos que asigné en ViewDidLoad.
Mi aplicación tiene un TabBar donde cada pestaña alberga un UINavigationController, la mayoría de los cuales empujan un DialogViewController. Un problema con el que estaba tratando era filtrar el DialogViewController después de que ViewDidUnload dejara de lado la referencia a él. Intenté Desechar el DVC en ViewDidUnload, pero iOS seguía deseando volver a invocarlo y estaba recibiendo una excepción por invocar un selector en un objeto GC''ed. Descubrí el motivo: el controlador de navegación se aferraba al DVC en su matriz ViewControllers. La solución es liberar la matriz mediante la creación de una matriz de longitud cero en su lugar, en ViewDidUnload:
this.ViewControllers = new UIViewController[0];
La matriz antigua ahora será GC''ed, y también lo será el DVC porque ya nada lo apunta. Y iOS nunca volverá a invocar el objeto. Nota: no es necesario llamar a Dispose en el DVC.
Tengo una aplicación MonoTouch que tiene un UITabBarController, y cada una de las pestañas es un UINavigationController. Algunos de estos envuelven un UIViewController que agrega un UITableView y una UIToolbar, y otros envuelven un DialogViewController.
No he prestado mucha atención a la gestión de memoria / vista hasta ahora (he estado ejecutando principalmente en el simulador), pero cuando comencé a probar en un dispositivo real, he notado algunas fallas debido a las bajas condiciones de memoria ( por ejemplo, la aplicación se termina, y descubro en mi registro que DidReceiveMemoryWarning fue llamado antes de esto). Otras veces, observo pausas prolongadas en la capacidad de respuesta de la aplicación que supongo que se deben a un ciclo de GC.
Hasta ahora he estado asumiendo que cada DialogViewController que presiono en la pila de navegación limpiará sus vistas y otras cosas que se asignan cuando lo abro. Pero estoy empezando a darme cuenta de que probablemente no sea tan fácil, y que necesito comenzar a llamar a Dispose () sobre las cosas.
¿Existen prácticas recomendadas sobre cómo manejar la administración de recursos y la memoria con MonoTouch y MT.D? Específicamente:
- ¿Es necesario llamar a Dispose en un DialogViewController después de que se abra? Si es así, ¿dónde es mejor hacer esto? (ViewDidUnload? DidReceiveMemoryWarning? Destructor?)
- ¿El DVC desecha automáticamente objetos como el RootElement que se le pasa o tengo que preocuparme por esto? ¿Qué hay de los UIImages que se cargan como parte de la representación de una celda de tabla (por ejemplo, StyledStringElement)?
- ¿Hay lugares donde debería llamar a GC.Collect () para espaciar mejor las colecciones a fin de no afectar la capacidad de respuesta cuando ocurre una GC?
- ¿El recolector de basura generacional ayuda con los problemas de interactividad y es lo suficientemente estable como para usarlo en una aplicación de producción? (Creo que todavía se considera "experimental" en MonoDevelop 3.0.2 / MT 4.3.3)
- ¿Qué debo hacer en DidReceiveMemoryWarning para reducir la probabilidad de que iOS lance mi aplicación? Ya que cada controlador de vista no visible parece recibir esta llamada, asumo que debo limpiar los recursos de ese controlador de vista ... ¿Debo hacer el mismo tipo de cosas que hago en ViewDidUnload?
- Parece que no recibo mi ViewDidUnload (incluso después de recibir DidReceiveMemoryWarning). De hecho, no recuerdo haberlo visto nunca en mi registro. Si iOS siempre llamó a mi ViewDidUnload después de DidReceiveMemoryWarning, podría hacer toda la limpieza en ViewDidUnload ... ¿Cuál es la mejor manera de dividir la responsabilidad de limpieza entre ViewDidUnload y DidReceiveMemoryWarning?
Pido disculpas por la naturaleza general de esta pregunta; parece un buen tema para un documento técnico, pero no pude encontrar ningún ...
Actualización : para que la pregunta sea más concreta: después de usar Instruments y el perfilador Xamarin Heapshot, tengo claro que pierdo UIViewControllers cuando el usuario abre la pila de navegación. Rolf presentó un bug por esto y tiene dos dups, por lo que este es un problema real para más que solo yo. Desafortunadamente, no he encontrado una buena solución para los controladores de UIView filtrados; no he encontrado un buen lugar para llamar a Dispose () sobre ellos. El lugar natural para liberar recursos asignados por ViewDidLoad está en el mensaje ViewDidUnload, pero nunca se llama en el simulador, por lo que mi huella de memoria sigue creciendo. En el dispositivo, veo DidReceiveMemoryWarning, pero me resisto a usar esto como un lugar para liberar mi controlador de vista y sus recursos, ya que no tengo la garantía de que iOS realmente descargará mi vista y, por lo tanto, no estoy seguro de que se vuelva a llamar a mi ViewDidLoad cualquiera de los dos (lo que lleva a un ViewDidAppear que tendría que codificar de manera defensiva frente a situaciones en las que se disponían los recursos subyacentes) Me encantaría obtener algunos consejos sobre cómo salir de este lío ...