Android
Fuites de mémoire
Recherche…
Fuites de mémoire communes et comment les corriger
1. Fixez vos contextes:
Essayez d'utiliser le contexte approprié: par exemple, comme Toast peut être vu dans de nombreuses activités plutôt que dans une seule, utilisez getApplicationContext()
pour les toasts, et comme les services peuvent continuer à fonctionner même si une activité est terminée, lancez un service avec:
Intent myService = new Intent(getApplicationContext(), MyService.class);
Utilisez ce tableau comme guide rapide pour savoir quel contexte est approprié:
Article original sur le contexte ici .
2. Référence statique au contexte
Une erreur de fuite de mémoire grave conserve une référence statique à View
. Chaque View
a une référence interne au Context
. Ce qui signifie qu'une ancienne activité avec toute sa hiérarchie de vues ne sera pas récupérée avant la fin de l'application. Vous aurez votre application deux fois en mémoire lors de la rotation de l'écran.
Assurez-vous qu'il n'y a absolument aucune référence statique à View, Context ou à l'un de leurs descendants.
3. Vérifiez que vous terminez réellement vos services.
Par exemple, j'ai un intentService qui utilise l'API du service de localisation Google. Et j'ai oublié d'appeler googleApiClient.disconnect();
:
//Disconnect from API onDestroy()
if (googleApiClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, GoogleLocationService.this);
googleApiClient.disconnect();
}
4. Vérifiez l’utilisation des images et des bitmaps:
Si vous utilisez la bibliothèque Picasso de Square, j'ai constaté que je .fit()
la mémoire en n'utilisant pas le .fit()
, ce qui réduisait considérablement mon empreinte mémoire de 50 Mo en moyenne à moins de 19 Mo:
Picasso.with(ActivityExample.this) //Activity context
.load(object.getImageUrl())
.fit() //This avoided the OutOfMemoryError
.centerCrop() //makes image to not stretch
.into(imageView);
5. Si vous utilisez des récepteurs de diffusion, désinscrivez-les.
6. Si vous utilisez java.util.Observer
(modèle Observer):
Veillez à utiliser deleteObserver(observer);
Évitez les fuites d'activités avec AsyncTask
Un mot d'avertissement : AsyncTask a beaucoup de pièges à part la fuite de mémoire décrite ici. Alors faites attention à cette API, ou évitez-la complètement si vous ne comprenez pas complètement les implications. Il existe de nombreuses alternatives (Thread, EventBus, RxAndroid, etc.).
Une erreur commune avec AsyncTask
est de capturer une référence forte à l' Activity
hôte (ou Fragment
):
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!
}
}
Cela pose un problème car AsyncTask
peut facilement survivre à l' Activity
parente, par exemple si une modification de configuration se produit pendant l'exécution de la tâche.
La bonne façon de faire est de faire de votre tâche une classe static
, qui ne capture pas le parent, et contenant une référence faible à l' Activity
de l'hôte:
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
}
}
}
Rappel anonyme dans les activités
Chaque fois que vous créez une classe anonyme, elle conserve une référence implicite à sa classe parente. Donc, quand vous écrivez:
public class LeakyActivity extends Activity
{
...
foo.registerCallback(new BarCallback()
{
@Override
public void onBar()
{
// do something
}
});
}
En fait, vous envoyez une référence à votre instance LeakyActivity à foo. Lorsque l'utilisateur navigue hors de votre LeakyActivity, cette référence peut empêcher l'instance LeakyActivity d'être récupérée. Il s'agit d'une fuite sérieuse car les activités ont une référence à la hiérarchie de vue entière et sont donc des objets plutôt volumineux en mémoire.
Comment éviter cette fuite:
Vous pouvez bien sûr éviter d'utiliser des rappels anonymes dans les activités. Vous pouvez également annuler l'enregistrement de tous vos rappels en fonction du cycle de vie de l'activité. ainsi:
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);
}
}
Contexte d'activité dans les classes statiques
Souvent, vous voudrez envelopper certaines classes d'Android dans des classes utilitaires plus faciles à utiliser. Ces classes d'utilitaires nécessitent souvent un contexte pour accéder au système d'exploitation Android ou aux ressources de vos applications. Un exemple courant de ceci est un wrapper pour la classe SharedPreferences. Pour accéder aux préférences partagées d'Androids, il faut écrire:
context.getSharedPreferences(prefsName, mode);
Et on peut être tenté de créer la classe suivante:
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);
}
}
maintenant, si vous appelez init()
avec votre contexte d'activité, le LeakySharedPrefsWrapper conservera une référence à votre activité, l'empêchant d'être nettoyé.
Comment éviter:
Lorsque vous appelez des fonctions d'assistance statique, vous pouvez envoyer le contexte d'application à l'aide de context.getApplicationContext();
Lors de la création de fonctions d'assistance statique, vous pouvez extraire le contexte d'application du contexte qui vous est fourni (Calling getApplicationContext () sur le contexte d'application renvoie le contexte d'application). Donc, le correctif à notre wrapper est simple:
public static void init(Context context)
{
sContext = context.getApplicationContext();
}
Si le contexte d'application n'est pas adapté à votre cas d'utilisation, vous pouvez inclure un paramètre Context dans chaque fonction d'utilitaire, vous devez éviter de conserver des références à ces paramètres de contexte. Dans ce cas, la solution devrait ressembler à ceci:
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);
}
Détecter les fuites de mémoire avec la bibliothèque LeakCanary
LeakCanary est une bibliothèque Java Open Source pour détecter les fuites de mémoire dans vos versions de débogage.
Ajoutez simplement les dépendances dans le 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'
}
Ensuite, dans votre classe d' 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);
}
}
Maintenant, LeakCanary affichera automatiquement une notification lorsqu'une fuite de mémoire d'activité est détectée dans votre version de débogage .
Remarque: le code de version ne contiendra aucune référence à LeakCanary autre que les deux classes vides qui existent dans la leakcanary-android-no-op
.
Évitez les fuites d'activités avec les auditeurs
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.
Évitez les fuites de mémoire avec la classe anonyme, le gestionnaire, la tâche de minuterie, le thread
Dans Android, chaque développeur utilise la Anonymous Class
(Runnable) au moins une fois dans un projet. Toute Anonymous Class
a une référence à son parent (activité). Si nous effectuons une tâche de longue durée, l'activité parent ne sera pas détruite tant que la tâche n'est pas terminée.
L'exemple utilise le gestionnaire et la classe anonyme Runnable
. La mémoire va fuir lorsque nous quitterons l'activité avant que le Runnable
soit terminé.
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...
Comment pouvons-nous le résoudre?
- Ne faites pas fonctionner longtemps avec la
Anonymous Class
ou nous avons besoin d'uneStatic class
pour cela et y passerWeakReference
(comme l'activité, la vue ...).Thread
est le même avec laAnonymous Class
. - Annuler le
Handler
,Timer
lorsque l'activité est détruite.