Android Évitez les fuites d'activités avec les auditeurs


Exemple

Si vous implémentez ou créez un écouteur dans une activité, faites toujours attention au cycle de vie de l'objet sur lequel le programme d'écoute est enregistré.

Considérons une application dans laquelle plusieurs activités / fragments différents sont intéressés lorsqu'un utilisateur est connecté ou déconnecté. Une façon de procéder serait d’avoir une instance singleton de UserController laquelle vous pouvez vous abonner pour être averti lorsque l’état de l’utilisateur change:

public class UserController {
    private static UserController instance;
    private List<StateListener> listeners;

    public static synchronized UserController getInstance() {
        if (instance == null) {
            instance = new UserController();
        }
        return instance;
    }

    private UserController() {
        // Init
    }

    public void registerUserStateChangeListener(StateListener listener) {
        listeners.add(listener);
    }

    public void logout() {
        for (StateListener listener : listeners) {
            listener.userLoggedOut();
        }
    }

    public void login() {
        for (StateListener listener : listeners) {
            listener.userLoggedIn();
        }
    }

    public interface StateListener {
        void userLoggedIn();
        void userLoggedOut();
    }
}

Ensuite, il y a deux activités, SignInActivity :

public class SignInActivity extends Activity implements UserController.StateListener{

    UserController userController;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.userController = UserController.getInstance();
        this.userController.registerUserStateChangeListener(this);
    }

    @Override
    public void userLoggedIn() {
        startMainActivity();
    }

    @Override
    public void userLoggedOut() {
        showLoginForm();
    }

    ...

    public void onLoginClicked(View v) {
        userController.login();
    }
}

Et MainActivity :

public class MainActivity extends Activity implements UserController.StateListener{
    UserController userController;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.userController = UserController.getInstance();
        this.userController.registerUserStateChangeListener(this);
    }

    @Override
    public void userLoggedIn() {
        showUserAccount();
    }

    @Override
    public void userLoggedOut() {
        finish();
    }

    ...

    public void onLogoutClicked(View v) {
        userController.logout();
    }
}

Ce qui se passe avec cet exemple, c'est que chaque fois que l'utilisateur se connecte puis se déconnecte, une instance de MainActivity fuit. La fuite se produit car il existe une référence à l'activité dans les UserController#listeners .

S'il vous plaît noter: Même si nous utilisons une classe interne anonyme en tant qu'auditeur, l'activité fuirait toujours:

...
this.userController.registerUserStateChangeListener(new UserController.StateListener() {
    @Override
    public void userLoggedIn() {
        showUserAccount();
    }

    @Override
    public void userLoggedOut() {
        finish();
    }
});    
...

L'activité fuirait toujours, car la classe interne anonyme a une référence implicite à la classe externe (dans ce cas, l'activité). C'est pourquoi il est possible d'appeler des méthodes d'instance dans la classe externe à partir de la classe interne. En fait, les seules classes internes qui n'ont pas de référence à la classe externe sont les classes internes statiques .

En bref, toutes les instances de classes internes non statiques contiennent une référence implicite à l'instance de la classe externe qui les a créées.

Il existe deux approches principales pour résoudre ce problème, soit en ajoutant une méthode pour supprimer un écouteur des écouteurs UserController#listeners ou en utilisant une WeakReference pour contenir la référence des écouteurs.

Alternative 1: Supprimer les auditeurs

Commençons par créer une nouvelle méthode removeUserStateChangeListener(StateListener listener) :

public class UserController {

    ...

    public void registerUserStateChangeListener(StateListener listener) {
        listeners.add(listener);
    }

    public void removeUserStateChangeListener(StateListener listener) {
        listeners.remove(listener);
    }

    ...
}

onDestroy ensuite cette méthode dans la méthode onDestroy l'activité:

public class MainActivity extends Activity implements UserController.StateListener{
    ...

    @Override
    protected void onDestroy() {
        super.onDestroy();
        userController.removeUserStateChangeListener(this);
    }
}

Avec cette modification, les instances de MainActivity ne fuient plus lorsque l'utilisateur se connecte et se déconnecte. Cependant, si la documentation n'est pas claire, il est probable que le prochain développeur qui commence à utiliser UserController risque de ne plus pouvoir enregistrer l’auditeur lorsque l’activité est détruite, ce qui nous amène à la deuxième méthode pour éviter ces types de fuites.

Alternative 2: Utiliser des références faibles

Tout d'abord, commençons par expliquer ce qu'est une référence faible. Une référence faible, comme son nom l'indique, contient une référence faible à un objet. Par rapport à un champ d'instance normal, qui est une référence forte, une référence faible n'arrête pas le ramasse-miettes, GC, de supprimer les objets. Dans l'exemple ci-dessus, cela permettrait à MainActivity d'être récupéré après avoir été détruit si UserController utilisait WeakReference pour référencer les écouteurs.

En bref, une référence faible indique au GC que si personne d'autre ne fait référence à cet objet, retirez-le.

WeakReference le UserController pour utiliser une liste de WeakReference pour suivre ses écouteurs:

public class UserController {

    ...
    private List<WeakReference<StateListener>> listeners;
    ...

    public void registerUserStateChangeListener(StateListener listener) {
        listeners.add(new WeakReference<>(listener));
    }

    public void removeUserStateChangeListener(StateListener listenerToRemove) {
        WeakReference referencesToRemove = null;
        for (WeakReference<StateListener> listenerRef : listeners) {
            StateListener listener = listenerRef.get();
            if (listener != null && listener == listenerToRemove) {
                referencesToRemove = listenerRef;
                break;
            }
        }
        listeners.remove(referencesToRemove);
    }

    public void logout() {
        List referencesToRemove = new LinkedList();
        for (WeakReference<StateListener> listenerRef : listeners) {
            StateListener listener = listenerRef.get();
            if (listener != null) {
                listener.userLoggedOut();
            } else {
                referencesToRemove.add(listenerRef);
            }
        }
    }

    public void login() {
        List referencesToRemove = new LinkedList();
        for (WeakReference<StateListener> listenerRef : listeners) {
            StateListener listener = listenerRef.get();
            if (listener != null) {
                listener.userLoggedIn();
            } else {
                referencesToRemove.add(listenerRef);
            }
        }
    }
    ...    
}

Avec cette modification, peu importe que les écouteurs soient supprimés ou non, car UserController ne contient aucune référence forte à aucun des écouteurs. Cependant, écrire ce code passe-partout à chaque fois est encombrant. WeakCollection une classe générique appelée WeakCollection :

public class WeakCollection<T> {
    private LinkedList<WeakReference<T>> list;

    public WeakCollection() {
        this.list = new LinkedList<>();
    }
    public void put(T item){
        //Make sure that we don't re add an item if we already have the reference.
        List<T> currentList = get();
        for(T oldItem : currentList){
            if(item == oldItem){
                return;
            }
        }
        list.add(new WeakReference<T>(item));
    }

    public List<T> get() {
        List<T> ret = new ArrayList<>(list.size());
        List<WeakReference<T>> itemsToRemove = new LinkedList<>();
        for (WeakReference<T> ref : list) {
            T item = ref.get();
            if (item == null) {
                itemsToRemove.add(ref);
            } else {
                ret.add(item);
            }
        }
        for (WeakReference ref : itemsToRemove) {
            this.list.remove(ref);
        }
        return ret;
    }

    public void remove(T listener) {
        WeakReference<T> refToRemove = null;
        for (WeakReference<T> ref : list) {
            T item = ref.get();
            if (item == listener) {
                refToRemove = ref;
            }
        }
        if(refToRemove != null){
            list.remove(refToRemove);
        }
    }
}

Maintenant, laissez-nous réécrire UserController pour utiliser WeakCollection<T> place:

public class UserController {
    ...
    private WeakCollection<StateListener> listenerRefs;
    ...

    public void registerUserStateChangeListener(StateListener listener) {
        listenerRefs.put(listener);
    }

    public void removeUserStateChangeListener(StateListener listenerToRemove) {
        listenerRefs.remove(listenerToRemove);
    }

    public void logout() {
        for (StateListener listener : listenerRefs.get()) {
            listener.userLoggedOut();
        }
    }

    public void login() {
        for (StateListener listener : listenerRefs.get()) {
            listener.userLoggedIn();
        }
    }

    ...
}

Comme indiqué dans l'exemple de code ci-dessus, WeakCollection<T> supprime tout le code passe- WeakReference nécessaire pour utiliser WeakReference au lieu d'une liste normale. Pour couronner le tout: Si un appel à UserController#removeUserStateChangeListener(StateListener) est manquant, le listener et tous les objets qu'il référence ne vont pas fuir.