Cómo evitar pérdidas de memoria causadas por Picasso
LeakCanary me notificó que se ha producido una pérdida de memoria:
* Hilo ROOT de GC com.squareup.picasso.Dispatcher.DispatcherThread.lt Java Localgt; android. os.Message.obj
* Referencias com.example.MyActivity$MyDialogClickListener.this$0
* Fugas de instancia de com.example.MyActivity.MainActivity
Simplemente diga: El hilo de Picasso contiene las variables locales de la instancia de Mensaje en el sitio. Instancia MyActivity.MainActivity
En resumen: el hilo de Picasso contiene variables locales de la instancia de Message en el sitio, Message contiene referencias a DialogInterface.OnClickListener y OnClickListener, y DialogInterface.OnClickListener contiene una referencia a la Activity destruida.
Las variables locales suelen ser de corta duración porque solo existen en la pila, y cuando un hilo llama a un método, el sistema le asigna un marco de pila. Cuando el método regrese, el marco de la pila se destruirá y todas las variables locales de la pila se reciclarán. Si una variable local provoca una pérdida de memoria, normalmente significa que el subproceso ha muerto o se ha bloqueado y, en este estado, el subproceso mantiene una referencia a la instancia del Mensaje.
Entonces, Dimitris y yo abrimos el código fuente de Picasso y encontramos:
Dispatcher.DispatcherThread es un HandlerThread simple:
clase estática DispatcherThread extends HandlerThread {
DispatcherThread() {
super(Utils.THREAD_PREFIX DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND
}
}12345
< p); > Este hilo recibe mensajes a través de controladores de la forma estándar:clase estática privada DispatcherHandler extiende el controlador {
despachador final privado de Dispatcher;
DispatcherHandler público (Looper looper , Despachador despachador) {
super(looper);
this.dispatcher = despachador
}
@Override public void handleMessage; (mensaje de mensaje final) {
switch (msg.what) {
case REQUEST_ SUBMIT: {
Acción acción = (Acción) msg.obj; /p>
despachador.performSubmit(action);
break;
}
// ...procesar otros tipos de mensajes
p>
}
}
}12345678910111213141516171819
Obviamente, despachador.
Consejos para hacer cola
Veamos cómo funciona HandlerThread:
Más adelante, más y más pérdidas de memoria de estas notificaciones no provinieron sólo de Picasso, sino de una variedad de subprocesos con pérdidas de memoria en variables locales. a menudo relacionado con OnClickListener del diálogo. Los subprocesos que pierden memoria tienen una característica en común: todos son subprocesos de trabajo que reciben trabajo a través de algún tipo de cola de bloqueo.
for (;;) {
Mensaje msg = queue.next(); // Puede bloquear
if (msg == null) {< / p>
retorno;
}
msg.target.dispatchMessage(msg);
msg.target.recycleUnchecked();
}12345678
El código fuente muestra que debe haber una variable local para contener una referencia al mensaje, pero debe ser de corta duración y borrarse al final del ciclo.
Intentemos reproducir el error implementando un hilo de trabajo simple usando una cola de bloqueo que solo envía un mensaje:
clase estática MyMessage {
mensaje de cadena final ;
MiMensaje(Mensaje de cadena) {
this.message = mensaje;
}
}
static void startThread() {
final BlockingQueuelt; MyMessagelt.gt; cola = new LinkedBlockingQueuelt ();
MyMessage message = new MyMessage("Hola mundo filtrado");
Cola. oferta(mensaje);
nuevo hilo() {
@Override public void run() {
prueba {
bucle( cola);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}.start();
}
bucle vacío estático (BlockingQueuelt; MyMessagegt; cola) lanza InterruptedException {
mientras (verdadero) {
Mi mensaje mensaje = queue.take();
System.out.println("Recibido: " mensaje
}
}
1234567891011121314151617181920212223242526272829
Una vez que el mensaje se imprimió en el registro, se suponía que la instancia MyMessage debía reciclarse, pero se produjo una pérdida de memoria:
* GC Hilo ROOT com.example.MyActivity$2.lt; Java local gt; (llamado "Thread-110")
*Fugas com.example.MyActivity$MyMessage instancia
Cuando bloqueamos Cuando la cola envía un mensaje nuevo, el mensaje anterior se reciclará y el mensaje nuevo se filtrará.
En una máquina virtual, cada marco de pila es una colección de variables locales y el recolector de basura es conservador: mientras haya una referencia superviviente, no se reciclará.
Al final del ciclo, la variable local ya no es accesible, pero la variable local todavía contiene una referencia al mensaje. En teoría, el intérprete/JIT debería invalidar la referencia a la variable local cuando lo es. inaccesible, pero no lo hacen; la referencia sigue viva y no invalidada, lo que impide que se recicle.
Para verificar nuestra conclusión, configuramos manualmente la referencia como nula y la imprimimos, por lo que nula no es el mejor enfoque:
static void loop(BlockingQueuelt; MyMessagegt; queue) throws InterruptedException {
while (true) {
Mi mensaje mensaje = queue.take();
System.out.println("Recibido: " mensaje);
mensaje = null;
System.out.println("Ahora nulo: " mensaje); /p>
Al probar el código anterior, descubrimos que cuando Mensaje se establece en nulo, la instancia de MyMessage se reciclará inmediatamente. Esto significa que nuestra conclusión parece ser correcta.
Dado que este tipo de pérdida de memoria ocurre en varias implementaciones de subprocesos y colas de bloqueo, ahora hemos determinado que se trata de un error en la VM y, según esta conclusión, solo podemos reproducirlo en la VM Dalvik. El error se produce y no se puede reproducir en ART VM o JVM.
Mensaje (reciclaje) en una botella
Encontramos un error que causa una pérdida de memoria, pero ¿causará una pérdida de memoria grave?
* Referencia com.example.MyActivity$MyDialogClickListener.this$0
* Fuga com.example.MyActivity.instance
Luego lo enviamos al Picasso Dispatcher hilo ¿Qué pasa con el mensaje? Nunca configuramos Message.obj en DialogInterface.OnClickListener, entonces, ¿cómo termina?
Además, cuando se procesa un mensaje, el mensaje debe reciclarse inmediatamente y Message.obj debe configurarse como vacío. Sólo entonces el hilo del controlador esperará el siguiente mensaje y filtrará temporalmente el mensaje anterior:
for (;;) {
Message msg = queue.next(); bloquear
if (msg == null) {
return;
}
msg.target.dispatchMessage(msg ); /p>
msg.recycleUnchecked();
}12345678
Entonces sabemos que el mensaje filtrado se reciclará, por lo que el contenido anterior no se conservará.