Ricerca…


Perdite di memoria comuni e come risolverli

1. Risolvi i tuoi contesti:

Prova ad usare il contesto appropriato: ad esempio poiché un Toast può essere visto in molte attività invece che in una sola, usa getApplicationContext() per toast e dato che i servizi possono continuare a funzionare anche se un'attività è terminata, avvia un servizio con:

Intent myService = new Intent(getApplicationContext(), MyService.class);

Utilizza questa tabella come guida rapida per quale contesto è appropriato: inserisci la descrizione dell'immagine qui

Articolo originale sul contesto qui .

2. Riferimento statico al contesto

Un errore di perdita di memoria serio sta mantenendo un riferimento statico a View . Ogni View ha un riferimento interno al Context . Il che significa che una vecchia attività con la sua intera gerarchia di viste non sarà raccolta di rifiuti fino a quando l'app non viene terminata. Avrai la tua app due volte in memoria ruotando lo schermo.

Assicurati che non ci sia assolutamente alcun riferimento statico a View, Context o ai loro discendenti.

3. Controlla che stai effettivamente finendo i tuoi servizi.

Ad esempio, ho un intentService che utilizza l'API del servizio di localizzazione di Google. E ho dimenticato di chiamare googleApiClient.disconnect(); :

//Disconnect from API onDestroy()
if (googleApiClient.isConnected()) {
    LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, GoogleLocationService.this);
    googleApiClient.disconnect();
}

4. Verifica l'utilizzo di immagini e bitmap:

Se stai usando la libreria di Picasso di Square, ho scoperto che stavo perdendo memoria non usando il .fit() , che ha ridotto drasticamente il mio footprint di memoria da 50 MB in media a meno di 19 MB:

Picasso.with(ActivityExample.this)                   //Activity context
                .load(object.getImageUrl())           
                .fit()                                //This avoided the OutOfMemoryError
                .centerCrop()                         //makes image to not stretch
                .into(imageView);

5. Se si utilizzano ricevitori di trasmissione, annullarne la registrazione.

6. Se si utilizza java.util.Observer (pattern Observer):

Assicurati di usare deleteObserver(observer);

Evitare le perdite di attività con AsyncTask

Una parola di cautela : AsyncTask ha molti segreti a parte la perdita di memoria descritta qui. Quindi, fai attenzione con questa API o evita del tutto se non comprendi pienamente le implicazioni. Ci sono molte alternative (Thread, EventBus, RxAndroid, ecc.).

Un errore comune con AsyncTask è quello di acquisire un riferimento forte Activity (o Fragment ) dell'host:

class MyActivity extends Activity {
  private AsyncTask<Void, Void, Void> myTask = new AsyncTask<Void, Void, Void>() {
    // Don't do this! Inner classes implicitly keep a pointer to their
    // parent, which in this case is the Activity!
  }
}

Questo è un problema perché AsyncTask può facilmente sopravvivere Activity padre, ad esempio se una modifica alla configurazione avviene mentre l'attività è in esecuzione.

Il modo giusto per farlo è quello di rendere il vostro compito di una static di classe, che non cattura il genitore, e in possesso di un debole, di riferimento per l'host Activity :

class MyActivity extends Activity {
  static class MyTask extends AsyncTask<Void, Void, Void> {
    // Weak references will still allow the Activity to be garbage-collected
    private final WeakReference<MyActivity> weakActivity;

    MyTask(MyActivity myActivity) {
      this.weakActivity = new WeakReference<>(myActivity);
    }

    @Override
    public Void doInBackground(Void... params) {
      // do async stuff here
    }

    @Override
    public void onPostExecute(Void result) {
      // Re-acquire a strong reference to the activity, and verify
      // that it still exists and is active.
      MyActivity activity = weakActivity.get();
      if (activity == null
          || activity.isFinishing()
          || activity.isDestroyed()) {
        // activity is no longer valid, don't do anything!
        return;
      }

      // The activity is still valid, do main-thread stuff here
    }
  }
}

Richiamata anonima in attività

Ogni volta che crei una classe anonima, mantiene un riferimento implicito alla sua classe genitore. Quindi quando scrivi:

public class LeakyActivity extends Activity
{

...

    foo.registerCallback(new BarCallback() 
    {            
        @Override
        public void onBar() 
        {
            // do something                
        }            
    });
}

In effetti, stai inviando un riferimento alla tua istanza LeakyActivity per cedere. Quando l'utente si allontana da LeakyActivity, questo riferimento può impedire che l'istanza LeakyActivity venga trucchiata. Si tratta di una grave perdita poiché le attività mantengono un riferimento all'intera gerarchia della vista e sono quindi oggetti di grandi dimensioni nella memoria.

Come evitare questa perdita:

Ovviamente puoi evitare di utilizzare interamente le callback anonime nelle attività. Puoi anche annullare la registrazione di tutte le tue richiamate rispetto al ciclo di vita delle attività. così:

public class NonLeakyActivity extends Activity
{
    private final BarCallback mBarCallback = new BarCallback() 
    {            
        @Override
        public void onBar() 
        {
            // do something                
        }            
    });

    @Override
    protected void onResume()
    {
        super.onResume();
        foo.registerCallback(mBarCallback);
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        foo.unregisterCallback(mBarCallback);
    }
}

Contesto di attività in classi statiche

Spesso vorrai avvolgere alcune delle classi di Android in classi di utilità più facili da usare. Queste classi di utilità spesso richiedono un contesto per accedere al sistema operativo Android o alle risorse delle tue app. Un esempio comune di questo è un wrapper per la classe SharedPreferences. Per accedere alle preferenze condivise di Androids è necessario scrivere:

context.getSharedPreferences(prefsName, mode);

E quindi si può essere tentati di creare la seguente classe:

public class LeakySharedPrefsWrapper
{
    private static Context sContext;

    public static void init(Context context)
    {
        sContext = context;
    }

    public int getInt(String name,int defValue)
    {
        return sContext.getSharedPreferences("a name", Context.MODE_PRIVATE).getInt(name,defValue);
    }
}

ora, se chiami init() con il tuo contesto di attività, LeakySharedPrefsWrapper manterrà un riferimento alla tua attività, impedendole di essere triturata.

Come evitare:

Quando si chiamano le funzioni di supporto statico, è possibile inviare il contesto dell'applicazione utilizzando context.getApplicationContext();

Quando si creano funzioni di supporto statiche, è possibile estrarre il contesto dell'applicazione dal contesto in cui viene fornito (Chiamare getApplicationContext () sul contesto dell'applicazione restituisce il contesto dell'applicazione). Quindi la correzione del nostro wrapper è semplice:

public static void init(Context context)
{
    sContext = context.getApplicationContext();
}

Se il contesto dell'applicazione non è appropriato per il tuo caso d'uso, puoi includere un parametro di contesto in ciascuna funzione di utilità, dovresti evitare di mantenere i riferimenti a questi parametri di contesto. In questo caso la soluzione sarebbe così:

public int getInt(Context context,String name,int defValue)
{
    // do not keep a reference of context to avoid potential leaks.
    return context.getSharedPreferences("a name", Context.MODE_PRIVATE).getInt(name,defValue);
}

Rileva le perdite di memoria con la libreria LeakCanary

LeakCanary è una libreria Java Open Source per rilevare perdite di memoria nelle build di debug.

Basta aggiungere le dipendenze in build.gradle :

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
 }

Quindi nella classe Application :

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }

    LeakCanary.install(this);
  }
}

Ora LeakCanary mostrerà automaticamente una notifica quando viene rilevata una perdita di memoria dell'attività nella build di debug .

NOTA: il codice di rilascio non conterrà alcun riferimento a LeakCanary oltre alle due classi vuote esistenti nella leakcanary-android-no-op .

Evitare le perdite di attività con gli ascoltatori

Se si implementa o si crea un listener in un'attività, prestare sempre attenzione al ciclo di vita dell'oggetto che ha registrato il listener.

Considera un'applicazione in cui abbiamo diverse attività / frammenti interessati quando un utente ha effettuato il login o l'uscita. Un modo per farlo sarebbe quello di avere un'istanza singleton di un UserController che può essere sottoscritto per ricevere una notifica quando lo stato dell'utente cambia:

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();
    }
}

Quindi ci sono due attività, 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();
    }
}

E 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();
    }
}

Quello che succede con questo esempio è che ogni volta che l'utente esegue il login e poi si disconnette di nuovo, viene fuoriuscita un'istanza MainActivity . La perdita si verifica perché vi è un riferimento all'attività in UserController#listeners .

Nota: anche se usassimo una classe interna anonima come ascoltatrice, l'attività verrebbe comunque trapelata:

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

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

L'attività sarebbe ancora in perdita, perché la classe interna anonima ha un riferimento implicito alla classe esterna (in questo caso l'attività). Questo è il motivo per cui è possibile chiamare i metodi di istanza nella classe esterna dalla classe interna. In effetti, l'unico tipo di classi interne che non hanno un riferimento alla classe esterna sono classi interne statiche .

In breve, tutte le istanze di classi interne non statiche contengono un riferimento implicito all'istanza della classe esterna che le ha create.

Esistono due approcci principali per risolvere questo problema, aggiungendo un metodo per rimuovere un listener dagli ascoltatori di UserController#listeners o utilizzando un WeakReference per mantenere il riferimento degli ascoltatori.

Alternativa 1: rimozione degli ascoltatori

Iniziamo con la creazione di un nuovo metodo removeUserStateChangeListener(StateListener listener) :

public class UserController {

    ...

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

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

    ...
}

Chiamiamo quindi questo metodo nel metodo onDestroy dell'attività:

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

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

Con questa modifica le istanze di MainActivity non vengono più trapelate quando l'utente esegue il login e l'uscita. Tuttavia, se la documentazione non è chiara, è probabile che il prossimo sviluppatore che inizia a utilizzare UserController possa perdere la necessità di annullare la registrazione del listener quando l'attività viene distrutta, il che ci porta al secondo metodo per evitare questi tipi di perdite.

Alternativa 2: utilizzo di riferimenti deboli

Innanzitutto, iniziamo spiegando che cos'è un riferimento debole. Un riferimento debole, come suggerisce il nome, contiene un debole riferimento a un oggetto. Rispetto a un campo di istanza normale, che è un riferimento forte, un riferimento debole non impedisce al GC, il programma di raccolta dati obsoleti, di rimuovere gli oggetti. Nell'esempio sopra questo permetterebbe a MainActivity di essere garbage-collected dopo che è stato distrutto se UserController usato WeakReference al riferimento degli ascoltatori.

In breve, un riferimento debole indica al GC che se nessun altro ha un forte riferimento a questo oggetto, andare avanti e rimuoverlo.

Cerchiamo di modificare UserController per utilizzare un elenco di WeakReference per tenere traccia dei suoi ascoltatori:

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);
            }
        }
    }
    ...    
}

Con questa modifica non importa se gli ascoltatori vengono rimossi o meno, dal momento che UserController non ha riferimenti forti a nessuno degli ascoltatori. Tuttavia, scrivere questo codice boilerplate ogni volta è ingombrante. Pertanto, creiamo una classe generica chiamata 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);
        }
    }
}

Ora riscriviamo UserController per utilizzare WeakCollection<T> :

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();
        }
    }

    ...
}

Come mostrato nell'esempio di codice sopra, la WeakCollection<T> rimuove tutto il codice boilerplate necessario per usare WeakReference invece di una normale lista. Per UserController#removeUserStateChangeListener(StateListener) tutto: se UserController#removeUserStateChangeListener(StateListener) una chiamata a UserController#removeUserStateChangeListener(StateListener) , il listener e tutti gli oggetti a cui fa riferimento non si diffonderanno.

Evita perdite di memoria con la classe anonima, il gestore, il compito del timer, il thread


In Android, ogni sviluppatore utilizza la Anonymous Class (eseguibile) almeno una volta in un progetto. Qualsiasi Anonymous Class ha un riferimento al suo genitore (attività). Se eseguiamo un'attività di lunga durata, l'attività principale non verrà distrutta fino al termine dell'attività.
Esempio utilizza il gestore e la classe Runnable anonima. La memoria si perde quando abbandoniamo l'attività prima che il Runnable sia finito.

  new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            // do abc long 5s or so
        }
    }, 10000); // run "do abc" after 10s. It same as timer, thread...

Come lo risolviamo?

  1. Non eseguire operazioni a lungo con la Anonymous Class o abbiamo bisogno di una Static class per esso e passiamo a WeakReference (come attività, vista ...). Thread è lo stesso con la Anonymous Class .
  2. Annulla il Handler , Timer quando l'attività viene distrutta.


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow