Esta clase de controlador debe ser estática o pueden producirse fugas: IncomingHandler

297

Estoy desarrollando una aplicación Android 2.3.3 con un servicio. Tengo esto dentro de ese servicio para comunicarme con la actividad principal:

public class UDPListenerService extends Service
{
    private static final String TAG = "UDPListenerService";
    //private ThreadGroup myThreads = new ThreadGroup("UDPListenerServiceWorker");
    private UDPListenerThread myThread;
    /**
     * Handler to communicate from WorkerThread to service.
     */
    private Handler mServiceHandler;

    // Used to receive messages from the Activity
    final Messenger inMessenger = new Messenger(new IncomingHandler());
    // Use to send message to the Activity
    private Messenger outMessenger;

    class IncomingHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
        }
    }

    /**
     * Target we publish for clients to send messages to Incoming Handler.
     */
    final Messenger mMessenger = new Messenger(new IncomingHandler());
    [ ... ]
}

Y aquí, final Messenger mMessenger = new Messenger(new IncomingHandler());recibo la siguiente advertencia de pelusa:

This Handler class should be static or leaks might occur: IncomingHandler

Qué significa eso?

VansFannel
fuente
24
¡Mira esta publicación de blog para obtener más información sobre este tema!
Adrian Monk
1
Pérdidas de memoria causadas por la recolección de basura ... Esto es suficiente para demostrar cómo Java es inconsistente y está mal diseñado
Gojir4

Respuestas:

392

Si la IncomingHandlerclase no es estática, tendrá una referencia a su Serviceobjeto.

Handler Todos los objetos para el mismo hilo comparten un objeto Looper común, en el que publican mensajes y leen.

Como los mensajes contienen destino Handler, siempre que haya mensajes con un controlador de destino en la cola de mensajes, el controlador no puede ser recolectado como basura. Si manejador no es estática, el Serviceo Activityno puede ser basura recogida, incluso después de ser destruido.

Esto puede provocar pérdidas de memoria, al menos durante un tiempo, siempre y cuando los mensajes permanezcan en la cola. Esto no es un gran problema a menos que publique mensajes muy retrasados.

Puede hacer IncomingHandlerestática y tener un WeakReferencea su servicio:

static class IncomingHandler extends Handler {
    private final WeakReference<UDPListenerService> mService; 

    IncomingHandler(UDPListenerService service) {
        mService = new WeakReference<UDPListenerService>(service);
    }
    @Override
    public void handleMessage(Message msg)
    {
         UDPListenerService service = mService.get();
         if (service != null) {
              service.handleMessage(msg);
         }
    }
}

Vea esta publicación de Romain Guy para más referencias

Tomasz Niedabylski
fuente
3
Romain muestra que la WeakReference a la clase externa es todo lo que se necesita: una clase anidada estática no es necesaria. Creo que preferiré el enfoque WeakReference porque, de lo contrario, toda la clase externa cambia drásticamente debido a todas las variables 'estáticas' que necesitaría.
Alguien en algún lugar
35
Si desea utilizar una clase anidada, debe ser estática. De lo contrario, WeakReference no cambia nada. La clase interna (anidada pero no estática) siempre tiene una fuerte referencia a la clase externa. Sin embargo, no hay necesidad de ninguna variable estática.
Tomasz Niedabylski
2
@SomeoneSomewhere mSerivce es una referencia débil. get()devolverá nulo cuando el objeto al que se hizo referencia fue gc-ed. En este caso, cuando el servicio está muerto.
Tomasz Niedabylski
1
Nota: después de hacer el IncomingHandler estático, recibí el error "El constructor MyActivity.IncomingHandler () no está definido". en la línea "final Messenger inMessenger = new Messenger (new IncomingHandler ());". La solución es cambiar esa línea a "final Messenger inMessenger = new Messenger (new IncomingHandler (this));".
Lance Lefebure
44
@Someone Somewhere Sí, la publicación de Romain está equivocada, ya que se perdió al declarar la estática de la clase interna que pierde todo el punto. A menos que tenga un compilador ultra genial que convierta automáticamente las clases internas en clases estáticas cuando no usan variables de clase.
Sogger
67

Como otros han mencionado, la advertencia Lint se debe a la posible pérdida de memoria. Puede evitar la advertencia de pelusa pasando un Handler.Callbackal construir Handler(es decir, no subclase Handlery no hay Handleruna clase interna no estática):

Handler mIncomingHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        // todo
        return true;
    }
});

Según tengo entendido, esto no evitará la pérdida potencial de memoria. Messagelos objetos contienen una referencia al mIncomingHandlerobjeto que contiene una referencia al Handler.Callbackobjeto que contiene una referencia al Serviceobjeto. Mientras haya mensajes en la Loopercola de mensajes, Serviceno será GC. Sin embargo, no será un problema grave a menos que tenga mensajes de gran retraso en la cola de mensajes.

Miguel
fuente
10
@Braj No creo que evitar la advertencia de pelusa, pero aún así mantener el error sea una buena solución. A menos que, como indica la advertencia de pelusa, si el controlador no se coloca en su bucle principal (y puede asegurarse de que todos los mensajes pendientes en él se destruyen cuando se destruye la clase), se mitiga la filtración de la referencia.
Sogger
33

Aquí hay un ejemplo genérico de uso de una referencia débil y una clase de controlador estático para resolver el problema (como se recomienda en la documentación de Lint):

public class MyClass{

  //static inner class doesn't hold an implicit reference to the outer class
  private static class MyHandler extends Handler {
    //Using a weak reference means you won't prevent garbage collection
    private final WeakReference<MyClass> myClassWeakReference; 

    public MyHandler(MyClass myClassInstance) {
      myClassWeakReference = new WeakReference<MyClass>(myClassInstance);
    }

    @Override
    public void handleMessage(Message msg) {
      MyClass myClass = myClassWeakReference.get();
      if (myClass != null) {
        ...do work here...
      }
    }
  }

  /**
   * An example getter to provide it to some external class
   * or just use 'new MyHandler(this)' if you are using it internally.
   * If you only use it internally you might even want it as final member:
   * private final MyHandler mHandler = new MyHandler(this);
   */
  public Handler getHandler() {
    return new MyHandler(this);
  }
}
Sogger
fuente
2
El ejemplo de Sogger es genial. Sin embargo, el último método Myclassdebe declararse como en public Handler getHandler()lugar depublic void
Jason Porter
Es similar a una respuesta de Tomasz Niedabylski
CoolMind
24

De esta manera funcionó bien para mí, mantiene el código limpio al mantener donde maneja el mensaje en su propia clase interna.

El manejador que deseas usar

Handler mIncomingHandler = new Handler(new IncomingHandlerCallback());

La clase interna

class IncomingHandlerCallback implements Handler.Callback{

        @Override
        public boolean handleMessage(Message message) {

            // Handle message code

            return true;
        }
}
Stuart Campbell
fuente
2
Aquí el método handleMessage devuelve verdadero al final. ¿Podría explicar qué significa exactamente esto (el valor de retorno verdadero / falso)? Gracias.
JibW
2
Entiendo que devolver verdadero es para indicar que ha manejado el mensaje y, por lo tanto, el mensaje no debe pasarse a ningún otro lado, por ejemplo, el controlador subyacente. Dicho esto, no pude encontrar ninguna documentación y sería felizmente corregido.
Stuart Campbell
1
Javadoc dice: Constructor asocia este controlador con el Looper para el hilo actual y toma una interfaz de devolución de llamada en la que puede manejar los mensajes. Si este hilo no tiene un bucle, este controlador no podrá recibir mensajes, por lo que se genera una excepción. <- Creo que el nuevo Handler (nuevo IncomingHandlerCallback ()) no funcionará cuando no haya un Looper conectado al hilo, y ese PUEDE ser el caso. No digo que sea incorrecto hacerlo en algunos casos, solo digo que no siempre funciona como cabría esperar.
user504342
1
@StuartCampbell: Tienes razón. Ver: groups.google.com/forum/#!topic/android-developers/L_xYM0yS6z8 .
MDTech.us_MAN
2

Con la ayuda de la respuesta de @ Sogger, creé un controlador genérico:

public class MainThreadHandler<T extends MessageHandler> extends Handler {

    private final WeakReference<T> mInstance;

    public MainThreadHandler(T clazz) {
        // Remove the following line to use the current thread.
        super(Looper.getMainLooper());
        mInstance = new WeakReference<>(clazz);
    }

    @Override
    public void handleMessage(Message msg) {
        T clazz = mInstance.get();
        if (clazz != null) {
            clazz.handleMessage(msg);
        }
    }
}

La interfaz:

public interface MessageHandler {

    void handleMessage(Message msg);

}

Lo estoy usando de la siguiente manera. Pero no estoy 100% seguro de si esto es a prueba de fugas. Tal vez alguien podría comentar sobre esto:

public class MyClass implements MessageHandler {

    private static final int DO_IT_MSG = 123;

    private MainThreadHandler<MyClass> mHandler = new MainThreadHandler<>(this);

    private void start() {
        // Do it in 5 seconds.
        mHandler.sendEmptyMessageDelayed(DO_IT_MSG, 5 * 1000);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case DO_IT_MSG:
                doIt();
                break;
        }
    }

    ...

}
Mario
fuente
0

No estoy seguro, pero puedes probar el controlador de inicialización para anular en onDestroy ()

Chaitanya
fuente
1
Todos los objetos de controlador para el mismo hilo comparten un objeto Looper común, en el que publican mensajes y leen. Como los mensajes contienen un controlador de destino, siempre que haya mensajes con el controlador de destino en la cola de mensajes, el controlador no se puede recolectar basura.
msysmilu
0

Estoy confundido. El ejemplo que encontré evita la propiedad estática por completo y usa el hilo de la interfaz de usuario:

    public class example extends Activity {
        final int HANDLE_FIX_SCREEN = 1000;
        public Handler DBthreadHandler = new Handler(Looper.getMainLooper()){
            @Override
            public void handleMessage(Message msg) {
                int imsg;
                imsg = msg.what;
                if (imsg == HANDLE_FIX_SCREEN) {
                    doSomething();
                }
            }
        };
    }

Lo que me gusta de esta solución es que no hay problema al intentar mezclar variables de clase y método.

usuario2515235
fuente