Android
Geheugenlekken
Zoeken…
Veelvoorkomende geheugenlekken en hoe ze te verhelpen
1. Fix je contexten:
Probeer de juiste context te gebruiken: bijvoorbeeld omdat een Toast te zien is in veel activiteiten in plaats van in slechts één, gebruik getApplicationContext()
voor toasts, en aangezien services kunnen blijven draaien, hoewel een activiteit is beëindigd, start een service met:
Intent myService = new Intent(getApplicationContext(), MyService.class);
Gebruik deze tabel als een snelle gids voor welke context geschikt is:
Oorspronkelijk artikel over context hier .
2. Statische verwijzing naar context
Een ernstige geheugenlekfout is het behouden van een statische verwijzing naar View
. Elke View
heeft een innerlijke verwijzing naar de Context
. Dat betekent dat een oude activiteit met de hele weergavehiërarchie niet zal worden verzameld voordat de app is beëindigd. Je hebt je app twee keer in het geheugen wanneer je het scherm draait.
Zorg ervoor dat er absoluut geen statische verwijzing is naar Beeld, Context of een van hun nakomelingen.
3. Controleer of u daadwerkelijk uw services afmaakt.
Ik heb bijvoorbeeld een intentService die de Google-locatieservice-API gebruikt. En ik vergat googleApiClient.disconnect();
te bellen googleApiClient.disconnect();
:
//Disconnect from API onDestroy()
if (googleApiClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, GoogleLocationService.this);
googleApiClient.disconnect();
}
4. Controleer het gebruik van afbeeldingen en bitmaps:
Als je de Picasso-bibliotheek van Square gebruikt, ontdekte ik dat ik geheugen lekte door de .fit()
niet te gebruiken, waardoor mijn geheugenvoetafdruk drastisch werd gereduceerd van gemiddeld 50 MB tot minder dan 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. Als u uitzendontvangers gebruikt, moet u ze afmelden.
6. Als u java.util.Observer
(waarnemerspatroon) gebruikt:
Zorg ervoor dat u deleteObserver(observer);
Vermijd lekkende activiteiten met AsyncTask
Een waarschuwing : AsyncTask heeft veel gotcha's afgezien van het geheugenlek dat hier wordt beschreven. Dus wees voorzichtig met deze API, of vermijd het helemaal als u de implicaties niet volledig begrijpt. Er zijn veel alternatieven (Thread, EventBus, RxAndroid, enz.).
Een veel voorkomende fout bij AsyncTask
is het vastleggen van een sterke verwijzing naar de host- Activity
(of 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!
}
}
Dit is een probleem omdat AsyncTask
makkelijk de ouder kan overleven Activity
, bijvoorbeeld wanneer een wijziging in de configuratie gebeurt terwijl de taak wordt uitgevoerd.
De juiste manier om dit te doen, is om van uw taak een static
klasse te maken, die de ouder niet vasthoudt en een zwakke verwijzing naar de 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
}
}
}
Anoniem terugbellen in activiteiten
Elke keer dat u een anonieme klasse maakt, behoudt deze een impliciete verwijzing naar de bovenliggende klasse. Dus als je schrijft:
public class LeakyActivity extends Activity
{
...
foo.registerCallback(new BarCallback()
{
@Override
public void onBar()
{
// do something
}
});
}
U stuurt in feite een verwijzing naar uw LeakyActivity-instantie naar foo. Wanneer de gebruiker weg van uw LeakyActivity navigeert, kan deze verwijzing voorkomen dat de LeakyActivity-instantie afval wordt verzameld. Dit is een ernstig lek omdat activiteiten een verwijzing bevatten naar hun gehele weergavehiërarchie en daarom vrij grote objecten in het geheugen zijn.
Hoe dit lek te voorkomen:
U kunt natuurlijk voorkomen dat u anonieme callbacks volledig in activiteiten gebruikt. U kunt ook de registratie van al uw callbacks met betrekking tot de activiteitenlevenscyclus ongedaan maken. zoals zo:
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);
}
}
Activiteitscontext in statische klassen
Vaak wil je sommige van de Android-klassen verpakken in gemakkelijker te gebruiken hulpprogramma-klassen. Die hulpprogramma's vereisen vaak een context om toegang te krijgen tot het Android-besturingssysteem of de bronnen van uw apps. Een bekend voorbeeld hiervan is een wrapper voor de klasse SharedPreferences. Om toegang te krijgen tot de gedeelde voorkeuren van Androids moet men schrijven:
context.getSharedPreferences(prefsName, mode);
En dus kan men in de verleiding komen om de volgende klasse te maken:
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);
}
}
nu, als u init()
aanroept met uw activiteitscontext, zal de LeakySharedPrefsWrapper een verwijzing naar uw activiteit behouden, waardoor wordt voorkomen dat het afval wordt verzameld.
Hoe te vermijden:
Wanneer u statische helperfuncties aanroept, kunt u de toepassingscontext verzenden met context.getApplicationContext();
Wanneer u statische helperfuncties maakt, kunt u de toepassingscontext extraheren uit de context die u krijgt (aanroepen van getApplicationContext () in de toepassingscontext retourneert de toepassingscontext). Dus de oplossing voor onze wrapper is eenvoudig:
public static void init(Context context)
{
sContext = context.getApplicationContext();
}
Als de toepassingscontext niet geschikt is voor uw use case, kunt u een Context-parameter opnemen in elke hulpprogramma-functie. Vermijd daarom verwijzingen naar deze contextparameters. In dit geval ziet de oplossing er als volgt uit:
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);
}
Detecteer geheugenlekken met de LeakCanary-bibliotheek
LeakCanary is een Open Source Java-bibliotheek om geheugenlekken in uw debug-builds te detecteren.
Voeg gewoon de afhankelijkheden toe in de 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'
}
Vervolgens in uw 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);
}
}
Nu zal LeakCanary automatisch een melding tonen wanneer een activiteitgeheugenlek wordt gedetecteerd in uw debug- build.
OPMERKING: Release-code bevat geen andere verwijzing naar LeakCanary dan de twee lege klassen die bestaan in de leakcanary-android-no-op
.
Vermijd lekkende activiteiten met luisteraars
Als u een luisteraar in een activiteit implementeert of maakt, let dan altijd op de levenscyclus van het object waarop de luisteraar is geregistreerd.
Overweeg een toepassing waarbij we verschillende activiteiten / fragmenten hebben waarin geïnteresseerd is wanneer een gebruiker is ingelogd of uitgelogd. Een manier om dit te doen zou zijn om een enkele instantie van een UserController
waarop kan worden geabonneerd om een melding te ontvangen wanneer de status van de gebruiker verandert:
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();
}
}
Dan zijn er twee activiteiten, 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();
}
}
En 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();
}
}
Wat er met dit voorbeeld gebeurt, is dat elke keer dat de gebruiker inlogt en vervolgens weer MainActivity
, een instantie MainActivity
wordt gelekt. Het lek treedt op omdat er een verwijzing is naar de activiteit in UserController#listeners
.
Let op: zelfs als we een anonieme binnenklasse als luisteraar gebruiken, zou de activiteit nog steeds lekken:
...
this.userController.registerUserStateChangeListener(new UserController.StateListener() {
@Override
public void userLoggedIn() {
showUserAccount();
}
@Override
public void userLoggedOut() {
finish();
}
});
...
De activiteit zou nog steeds lekken, omdat de anonieme binnenklasse een impliciete verwijzing heeft naar de buitenklasse (in dit geval de activiteit). Daarom is het mogelijk om instantiemethoden in de buitenklasse vanuit de binnenklasse aan te roepen. In feite is het enige type binnenklassen dat geen verwijzing naar de buitenklasse heeft, statische binnenklassen.
Kortom, alle instanties van niet-statische binnenklassen bevatten een impliciete verwijzing naar de instantie van de buitenklasse die ze heeft gemaakt.
Er zijn twee WeakReference
om dit op te lossen, hetzij door een methode toe te voegen om een luisteraar van UserController#listeners
te verwijderen of door een WeakReference
te gebruiken om de referentie van de luisteraars vast te houden.
Alternatief 1: luisteraars verwijderen
Laten we beginnen met het maken van een nieuwe methode removeUserStateChangeListener(StateListener listener)
:
public class UserController {
...
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(listener);
}
public void removeUserStateChangeListener(StateListener listener) {
listeners.remove(listener);
}
...
}
Laten we deze methode vervolgens aanroepen in de onDestroy
methode van de activiteit:
public class MainActivity extends Activity implements UserController.StateListener{
...
@Override
protected void onDestroy() {
super.onDestroy();
userController.removeUserStateChangeListener(this);
}
}
Met deze aanpassing worden de instanties van MainActivity
niet meer gelekt wanneer de gebruiker in en uitlogt. Als de documentatie echter niet duidelijk is, is de kans groot dat de volgende ontwikkelaar die UserController
begint te gebruiken, mist dat het nodig is om de luisteraar af te melden wanneer de activiteit wordt vernietigd, wat ons leidt tot de tweede methode om dit soort lekken te vermijden.
Alternatief 2: zwakke referenties gebruiken
Laten we eerst beginnen met uit te leggen wat een zwakke referentie is. Een zwakke verwijzing, zoals de naam al doet vermoeden, bevat een zwakke verwijzing naar een object. Vergeleken met een normaal instantieveld, dat een sterke referentie is, weerhoudt een zwakke referentie de vuilnisman, GC, er niet van de objecten te verwijderen. In het bovenstaande voorbeeld zou MainActivity
vuilnis kunnen worden verzameld nadat het is vernietigd als de UserController
WeakReference
gebruikt als referentie voor de luisteraars.
Kort gezegd, een zwakke verwijzing vertelt de GC dat als niemand anders een sterke verwijzing naar dit object heeft, dit moet doorgaan en verwijderen.
Laten we de UserController
aanpassen om een lijst van WeakReference
te gebruiken om zijn luisteraars bij te houden:
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);
}
}
}
...
}
Met deze aanpassing maakt het niet uit of de luisteraars al dan niet worden verwijderd, omdat UserController
geen sterke verwijzingen naar een van de luisteraars bevat. Het is echter altijd lastig om deze standaardplaatcode te schrijven. Laten we daarom een generieke klasse genaamd 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);
}
}
}
Laten we nu UserController
opnieuw schrijven om in plaats daarvan WeakCollection<T>
gebruiken:
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();
}
}
...
}
Zoals getoond in het bovenstaande codevoorbeeld, verwijdert de WeakCollection<T>
alle boilerplate-code die nodig is om WeakReference
te gebruiken WeakReference
plaats van een normale lijst. Om het helemaal af te maken: Als een aanroep van UserController#removeUserStateChangeListener(StateListener)
wordt gemist, zullen de luisteraar en alle objecten waarnaar wordt verwezen niet lekken.
Vermijd geheugenlekken met Anonymous Class, Handler, Timer Task, Thread
In Android gebruikt elke ontwikkelaar minstens één keer in een project Anonymous Class
(uitvoerbaar). Elke Anonymous Class
verwijst naar de ouder (activiteit). Als we een langlopende taak uitvoeren, wordt de bovenliggende activiteit niet vernietigd totdat de taak is beëindigd.
Voorbeeld gebruikt handler en klasse Anoniem Runnable
. Het geheugen zal lekken als we de activiteit stoppen voordat de Runnable
is afgelopen.
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...
Hoe lossen we het op?
- Werk niet lang met
Anonymous Class
of we hebben er eenStatic class
voor nodig en geven erWeakReference
door (zoals activiteit, weergave ...).Thread
is hetzelfde metAnonymous Class
. - Annuleer de
Handler
,Timer
wanneer activiteit is vernietigd.