(转)Hunting Your Leaks, Memory Management in Android

原文地址:https://www.raizlabs.com/dev/2014/03/wrangling-dalvik-memory-management-in-android-part-1-of-2/
https://www.raizlabs.com/dev/2014/04/hunting-your-leaks-memory-management-in-android-part-2-of-2/

There are some people who believe this myth that you don’t need to worry about managing memory while writing Android applications. It’s a self-contained Java environment, right? What’s the worst that could happen?

Well, it’s true – the Android OS, through the Dalvik runtime1 (now being superceded in some cases with ART), doesn’t have to worry about your app crashing the whole system due to poor memory management. But, alas, that doesn’t mean that your app doesn’t have to deal with managing its memory: Dalvik will be happy to kick you out of execution if you screw up your heap allocation, and your users will start leaving negative reviews about all the crashes they’re getting.

By the way, what we’re looking for throughout this exploration of potential memory problems is a crash called an OutOfMemoryError (more notoriously and affectionately known as an OOM): thrown by the application when you try to allocate something past the capacity of the heap. It’s important to note that this can happen at any time, caused by any object, so that doesn’t provide a very good reference of what’s actually the cause of the crash (probably something you’ve “leaked” into memory earlier).

Simply, you’re going to have to face it and trace it back to its source. But where do you start?

Lesson #1: You’re not going to find it by accident

Don’t trust your own test devices to fail in all of the remarkably innovative ways your users’ devices will manage to fail.

Here are several tips:

1.Understand potential sources of issues: if you know what not to do, you never need to know how to fix it.
2.Plan for a lot more QA than you ever expected.
3.Very thorough unit testing.
4.Run analyses on builds before shipping .
5.Utilize prayer, lucky rabbit feet, or indomitable willpower in the face of the inevitable flood of negative reviews when you ignore this issue.

The best way to effectively pass timely, event-driven data around in Android ecosystems can be debated without end. And, in fact, it often is. But, that’s not the purpose of this post.

One method is attaching an implementation of an interface definition to the object sending the data, and holding that implementation within the receiver object.

At Raizlabs, one of our go-to simple-syntax elements is an interface called EventListener:

1
2
3
4

public interface EventListener<EventResponseType> {
public void onEvent(EventResponseType response);
}

EventListener might be used to receive data from a separate object, like an Activity class which wants to get notifications when a given Object is updated successfully:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public void onResume() {
super.onResume();

SomeObject object = new SomeObject();

object.setSuccessListener(new EventListener<Boolean>() {
public void onEvent(Boolean response) {
Log.d(TAG_NAME, "Valid response? "+response);
}
});

SomeObjectManager.getSingleton().addObject(object);
}

And, in SomeObject, we might see the following, indicating that our data has been saved successfully:

1
2
3
4
5

public void saveData(Data newData) {
this.data = newData;
this.successListener.onEvent(true);
}

Which would notify the EventListener instance that we created in the Activity’s onResume method that the event has successfully been completed.

Now, for those well-seasoned memory wranglers, seeing the immediate danger of this kind of pattern must be super easy. But, for the rest of us, we’re going to have to keep an eye out for problems like these. As it turns out, we just leaked an Activity. And most Android devices don’t need many Activities before you hit an OOM.

Lesson #2: Stalk your references

You’re going to have to keep an eye on what you reference in the course of developing an Android app. For iOS developers reading this, you don’t have to worry about retain cycles, since Dalvik’s GarbageCollector will parse the entire map of referenced objects. But, you may accidentally hold strong references to objects you no longer need, and in that case, the GarbageCollector will pass over those unneeded objects. For example, a Context that is no longer on screen, or a bitmap that won’t be displayed again for a while).

Anytime you find yourself creating a reference (that is, any time you assign a variable to an Object), think about what you might need to do to prevent a memory leak.

If the reference is to an object that is instantiated inside your own class, you usually2 don’t have to worry about it. That is, unless you’re sharing strong references with a different object. If that’s the case, then you must manage their references more manually.

For instance, in the example above: we’ve attached a reference to our Activity instance to some object, presumably persistent, and in a manager somewhere. The point is, the Activity doesn’t know that the lifespan of SomeObject will end when the instance of the Activity ends. If that object remains in memory, it will hold that Activity in memory as well, even after the Activity has been visually destroyed (a user going back, or rotating the device, or some other lifecycle-ending event). So, what we need to make sure to do, is remove that reference in onDestroy() or similar end-of-lifecycle-method:

1
2
3
4
5
6
7

public void onDestroy() {
super.onDestroy();

SomeObject objectFromBefore = SomeObjectManager.getSingleton().getOurObject();
objectFromBefore.setSuccessListener(null);
}
Lesson #3: Survival of the Fittest References

Android contains a few different possible types of references. Each level of strength indicates how the system’s GarbageCollector will interact with that Object.

The reference you’re inevitably used to using is a Strong reference. This is what you get when you do:

1
2

String myVariable = new String("Hello world");

A strong reference means the GarbageCollector will ignore the instantiated object “Hello World” as long as the “pointer” myVariable references it. Once you set myVariable = null;, that String can be swept up out of memory at any time. (Not that it matters anymore, since you no longer have a reference to that string! You just have a reference to null).

The rest of the reference types are actually a part of the SDK, and they are all created in the same general way. We’ll use a SoftReference to demonstrate:

1
2
3

String myStrongVariable = "Hello World";
SoftReference<String> myReference = new SoftReference<String>(myStrongVariable);

A Reference object will give you access to that “Hello World” string, but at any point the class could decide to reassign myStrongVariable, like so:

1
2

myStrongVariable = "Your string is in danger";

Now that object we have a Reference to, “Hello World”, might be GarbageCollected at any time. You can try to access it:

1
2

String testString = myReference().get();

But you can’t be sure testString is not null. So, how do each of the non-Strong reference types stack up?3

SoftReference: if there are only SoftReferences to an Object, the reference will generally be held on to as long as there’s memory for it to fit.
WeakReference: if there are only WeakReferences to an object, it will be purged from memory at the next GarbageCollection cycle.
PhantomReference: the weakest and most enigmatic reference, that get() method will always return null – you can never access the Object it references (even when the Object still exists, and has other references). Realistically, you should probably never be using these.4
…With great knowledge comes great responsibility.

Just because you now know the difference between these references, doesn’t mean you should be throwing them around as a solution to memory management without a very good reason.

The use of the Reference classes is usually a sign of a dirty design pattern. The first thing most people think of when they learn about SoftReference is, “Oh, what a good idea for designing a cache system!” But, while a decent idea, that’s an official Android no-no.

Lesson #4: “There is no GarbageCollector, Neo”

Occasionally, I’ll see a memory-related StackOverflow answer that suggests, as a production-environment solution, the line:

1
2

System.gc();

And when this happens, I cry myself to sleep.

The Android GarbageCollector is not some pet that you should whistle for when you want it to come play. It’s slow, and heavy-handed, and totally and completely capable of doing a great job without your input.

When an activity lifecycle ends (e.g. pressing back, or rotating the device) – that is, once onDestroy() has finished – it should have no references whatsoever still pointing to it. Otherwise, the GarbageCollector will politely ignore it, thinking you’re not done with the Activity object, and you’ll wind up with some massive memory leak.

OK, this should be in the most tiny font we have available, but… once in a while, and we’re talking every four or five blue moons at the most, you may use System.gc(). But, if you can’t write at least twelve reasons that’s your only solution, you should probably re-think your memory management design.

Lesson #5: Don’t hold on to references to Activities. Ever.

This also applies to Fragments, Views, Resources and anything else tightly associated with a Context. These kinds of objects are sprawling metropolises of references to absolutely everything. We can get more into investigating the runtime memory map of your app a bit later, but until then, the tangled web we Context-based objects weave is insidious.

If you accidentally hold a reference to a View beyond its lifetime (i.e., you “leak” it), it has a reference to the Activity context that created it, and in turn, every other View, Fragment, Dialog, and so forth associated with that activity.

And if this happens, you’ll not be able to get it out of memory. So, you can kiss handful after handful of MBs goodbye each time this happens.5 If you’re ever having memory trouble while developing an Android application, the very first thing to check for is leaked Activities.

Lesson #6: It can happen to you…

I wouldn’t be writing this if it wasn’t something we recently ran into. Fortunately, we caught the problem before shipping to the Play Store, but it was entirely possible that it could have made it through.

Our problem? We had tripped over the fine line between abstraction and obfuscation.

Story time!

We had a bunch of activities that wanted to know when someone logged in; so, when the Activity was created, we would instantiate a class variable of that type EventListener, and add it as a reference to the singleton class (as in, an object that is never GarbageCollected in the lifespan of the app) that managed our user authentication. Then, of course, we would remove the reference in the Activity’s onDestroy() method like good little memory managers.

Alas, the key we had missed: we use many fragments. Occasionally, we have to reference the parent activity from a child fragment. When one of our fragments wanted to get that login notification, it simply retrieved its parent activity, and called the method:

1
2

public void addLoginListener(EventListener loginListener);

But, that method’s implementation was designed to add the listener straight on to the singleton that managed authentication. We never bothered removing it, because that method looked like it was adding a reference to the Fragment to its parent Activity – which would allow the Fragment to be GarbageCollected at the same time as that Activity. Since this method was just a convenient proxy to add a reference to the authentication singleton, we wound up with a leaked reference to the Fragment, and, in turn, a reference to that Fragment’s context (its parent Activity) and all of its references in turn.

Woo-hoo! Now you know what is happening with your app’s memory usage when you see one of those OOM exception. But, you don’t know where to find the source. Let alone, how to fix it.

Tracking down memory issues can be a pain in the neck. Here are a few strategies that will help you as you pursue the noble craft of Android-igami.

There are code snippets and screenshots of what I’m working on sprinkled throughout this post. These will come from a single sample project, and if you prefer to see things in their entirety before being divided into little pieces, I provided that sample project to download here.

Yes, it uses Eclipse. Sorry to all you cutting edge folks working with the still beta Android Studio.
Structure: One activity, with a fragment. You’ll see an image that turns on and off every 5 seconds.
We’re using a singleton manager to keep time for us, and notify the Fragment when it’s time to switch the image on or off.
For those keeping score at home, this project has one A-level memory leak: the type of leak that no developer should ever allow into their code. And I’m not going to tell you where it is. But, that’s the purpose of this experiment! So let’s take a look at where it goes wrong.

Examination 7: “Update Heap” Button

picture1

1.Open the DDMS1 Perspective in Eclipse (It should be found somewhere around the Window->Open Perspective->Other popup dialog).
2.Highlight the process you wish to profile (most real devices will only show “debug”-signed processes, so you’d only see it on a real device if you built and installed from adb).
3.Tap the little green “disk” icon (circled in above image), named “Update Heap.”
4.Make sure you open the Heap panel on the right side of the screen.
5.Tap the “Cause GC” button2.

This will put some numbers in that chart. The one we should focus on is “Allocated,” which shows you how much memory the Dalvik VM currently has given your app. This can stretch and shrink a bit, but there’s an upper limit (the size of which depends on the device). If you exceed the upper limit, you pop out an OOM and the app crashes.

This is a great tool to keep an eye on while you develop, since it shows a live snapshot of the system after any garbage collection cycle. If you ever notice the allocated memory gradually increasing without ever letting anything go, that’s a good indication that you might have a memory leak.3

Examination 8: MAT & HPROF — a.k.a., “The Hunt”

There are some acronyms for you, huh?

“MAT,” the Eclipse Memory Analyzer tool, can read a file that your virtual machine generates (Remember that one we talked about in the last post? It’s called Dalvik). That file, called a “heap dump,” represents the set of all objects currently stored on your process’s heap4. That means you can actually poke around the metadata of the objects you’re using during runtime.

What you’ll need:
1.Eclipse (the Google provided “ADT” version works well here).
2.MAT.
3.If you’re not using the DDMS plugin, you’ll need to manually convert the Dalvik profile into an .hprof file.
Plot a course
Run the MemoryLeek project on an emulator5. You should see a white screen with a slowly blinking image. Rotate the emulator ten or so times (either 7 on the numpad or Cntrl+F12). Now, return to Eclipse, and open the DDMS perspective once again. This time, we’re aiming for the green disc icon with an arrow attached — indicating you want to generate a heap dump. Go ahead and click it, and you should see a statement in the Android log that looks like:

1
2

I/dalvikvm(1948): hprof: dumping heap strings to "[DDMS]".

That’s good. Now leave it for a bit, because it could take a couple of minutes to generate (and the emulator is frozen until it finishes).

Once the output is ready, it should prompt you about some different types of reports. (If you instead see a prompt to save a file, go ahead and save it somewhere, then use the SDK tool hprof-conv to convert it to the appropriate format, and manually open it with Eclipse’s MAT). Once you have the “reports” prompt, just cancel out of it — we’re not going to use any pre-set reports for this.

Load your ammo
Hooray! If you’re reading this, you’re hopefully looking at some kind of graph of your app’s current memory state. In my case, a pie chart.

picture2

Now what?
Let’s try poking around a bit. Click the little bar chart icon in the top left, that says “Create a histogram…” when you hover over it. This will show you a list of all the objects currently allocated, in no particular order.

You’re looking at the name and package of the objects in the first column, the number of that kind of object in existence at the moment in the next column and then some representation of the total heap sizes in the third and fourth columns6.

It might be interesting to sort by size or count to see where the majority of memory is being used in your code7. If you right-click on one of these listings, you can get some cool options, like all instances of objects of that type with all the references that keep it from being garbage collected.

picture3

But wait, we’re supposed to be hunting for something, right? We need a target of some kind.

Tracking the game
There’s a lot of noise in an app’s memory space. Fortunately, we can narrow it down in a few ways.

First, try applying filtering to this list. In the first row, you can enter text. Since you almost always want to take a closer look at your code, I suggest entering something like “com.example.memoryleek”. This should filter down to a handful of entries.
picture4
Gazing upon these, I’m hoping you notice something horrifying. If you don’t, I recommend taking another look at Part 1 of this series. Hopefully you will figure out what the worst thing in the world to leak would be.

Hint: it’s Activities.

And, good Lord, I have 24 instances of LeekActivity right now! That’s totally not OK.

Why might this be?

Well, kindly, MAT has a way to tell us. You can right-click that list item, and select “Merge Shortest Paths to GC Roots”, excluding WeakReferences (remember, WeakReferences will have no effect on what is retained). This means we want to see what memory root all these objects have in common. That should indicate if there’s something retaining all of them at once, ergo, a leak of some kind!

picture5

In this case, there are 3 roots. Two of them are external to this project, so aren’t likely culprits. But, that LeekManager instance looks suspicious….

A-Ha! If you open it up, you can see that there’s a LinkedList called “listeners” on LeekManager that has a reference to all of those Activities.

But, where did that come from?

Well, check this out:

1
2
3
4
5
6
7
8
9
10
11

// In LeekFragment.class:

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

// Add this fragment as a leek listener so
// it can update its view accordingly
LeekManager.get().addListener(this);
}

Every time our LeekFragment is created, we add it as a LeekListener to the LeekManager singleton. But, when we did those rotations, the Fragment was never removed as a listener. The Fragment has a reference to the parent Activity, and the parent LeekActivity has references to all the other view elements.

Taking the shot
There’s our leak. Fixing it? Easy: LeekFragment needs to be removed from LeekManager upon its onDestroy() lifecycle method.

Now, the above code looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// In LeekFragment.class:

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

// Add this fragment as a leek listener so
// it can update its view accordingly
LeekManager.get().addListener(this);
}

@Override
public void onDestroy() {
super.onDestroy();

LeekManager.get().removeListener(this);
}

And when we run the app, we don’t see any more indications of leaky memory! Hoorah!

Things that are trying to sneak into this post:

Options for unit testing. If anyone has useful memory analysis-based unit test strategies, feel free to shout at me @onemahon. Props to Reddit user stepwise_refinement for the great question.
Static vs. non-static inner classes. Non-static inner classes keep implicit references to their enclosing class. Be careful when deciding on the static modifier for inner classes, as such.
Some strategies for testing bad memory management during QA phases:
Lots of rotations.
Kill the app at unfortunate times (i.e. during activity loads).
Leave the app open for long periods of time, especially if you have some depth to your activity stack.
Run the app on an emulator with super small RAM.
Parting Shots

Never leak an Activity.
Keep an eye on the heap while the app is running.
Profile the app’s memory usage every so often with MAT.
Now, you’re ready! Go forth, and leak not!

文章目录
  1. 1. Lesson #1: You’re not going to find it by accident
  2. 2. Lesson #2: Stalk your references
  3. 3. Lesson #3: Survival of the Fittest References
  4. 4. Lesson #4: “There is no GarbageCollector, Neo”
  5. 5. Lesson #5: Don’t hold on to references to Activities. Ever.
  6. 6. Lesson #6: It can happen to you…
  7. 7. Examination 7: “Update Heap” Button
  8. 8. Examination 8: MAT & HPROF — a.k.a., “The Hunt”
,