有选择性的翻译自:Wrangling Dalvik: Memory Management in Android,与原文的标题略有出入,感觉“如何避免内存泄漏”更贴近内容一些。
如何查找OutOfMemoryError的源头
建议:
1,理解潜在的源头:如果不知道哪些不能做,则永远也不会想到如何修复它。
2,彻底的单元测试
3,在发行前对发现版本做内存泄漏分析
教训1:不要指望偶然发现内存泄漏
一个常见的场景是将一个接口的实现与发送数据的对象关联,如设置各种Listener等。首先是Listener的定义:
public void onEvent(EventResponseType response);
}
上述EventListener可能会用来接收来自一个独立的对象的数据。如,Activity类希望在某对象更新成功后得到通知。
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);
}
在该对象的代码中,我们可以看到如下代码,显示数据已经被成功保存:
public void saveData(Data newData) {
this.data = newData;
this.successListener.onEvent(true);
}
上述例子中,我们就泄漏了一个Activity,而这个错误是很难被发现的。我们需要警惕类似的问题。
教训2:跟踪你的引用
任何时候创建一个引用(即每当你将一个变量指向一个对象时),考虑你需要做什么来避免内存泄漏。
如果指向的对象的生命周期局限在当前类中,则不需要考虑这个问题。需要考虑的是指向不可控对象的强引用。在这种情况下,必须主动控制引用的使用。如在上例中,我们将一个执行Activity的引用与一个对象关联,而该对象可能是在某个Manager中的一个持久性对象(即它的生命周期有可能远大于上述Activity的生命周期)。如果该对象仍然在内存中的话,它会在Activity被显式的销毁(如点击后退键,旋转设备等)后,仍然持有对Activity的引用,从而使其仍然保存在内存中。因此,我们必须确保在onDestroy()等结束生命周期的方法中,去除引用。
public void onDestroy() {
super.onDestroy();
SomeObject objectFromBefore = SomeObjectManager.getSingleton().getOurObject();
objectFromBefore.setSuccessListener(null);
}
教训3:选择合适的引用类型
除了我们经常使用的强引用外,还有如下引用类型,使用时根据需要选择合适的引用类型:
- 软引用(SoftReference):如果只有一个软引用指向该对象,则只要有足够的内存空间,该引用会尽可能的持有该对象。即,会尽可能长的保留引用直到 JVM 内存不足时才会被回收(虚拟机保证), 这一特性使得 SoftReference 非常适合缓存应用。
- 弱引用(WeakReference): 如果只有一个弱引用指向该对象,则该对象会在下个垃圾回收周期中被移出内存。
- 幽灵引用(PhantomReference):最弱也是最令人困惑的引用,它的get()方法用于返回null—你永远都不能获取到它引用的对象(即使该对象仍然存在并且还有其他引用执行该对象)。关于该引用可以参考http://stackoverflow.com/questions/1599069/have-you-ever-used-phantom-reference-in-any-project。
教训4:垃圾回收机制无法主动启用
一个最常见的误解是调用如下语句后,系统就会执行垃圾回收:
System.gc();
事实上,垃圾回收机制是一个时耗性大,完全不受用户输入影响的工作。
我们能做的是关闭所有该关闭的引用,如前面的例子那样。
教训5:永远不要持有Activity这类对象的引用
该经验同样适用于Fragments, Views, Resources及其他与一个Context对象紧密关联的其他类的对象。
如果不小心持有了一个View对象的引用,则会持有创建该View对象的Activity的引用,从而持有了与该Activity关联的其他的View,Fragment, Dialog等对象的引用。因此,在Android应用程序开发过程中,首要的事情就是检查泄漏的Activity对象。
教训6:容易忽略的地方
这是一个真实遇到的例子。作者创建的很多Activity需要知道用户何时登录,因此在创建这些Activity时,作者创建了EventListener接口的实例并且把它添加到管理用户认证信息的单例对象中,因此这个单例对象就持有了Activity的引用,按照前面的法则在Activity的onDestroy()方法中去除了引用。但是,作者忽略了Fragment对象。当Fragment对象需要登录信息时,作者获取到它的父类activity,并调用方法:
public void addLoginListener(EventListener loginListener);
该方法用来添加listener到管理用户认证信息的单例对象。因为该方法看起来是添加指向Fragment的一个引用到它的父类Activity,这样在Activity被销毁时Fragment也会被回收,因此作者从来没有考虑去除上述引用。而事实上,上述方法只是一个添加引用到认证单例对象的方便的代理,因此最后运行过程中出现了很多Fragment的被泄漏的引用,而它们又持有父类Activity的引用,因此Activity也被泄漏。
该文档的第二部分是介绍如何使用Eclipse的DDMS发现Activity内存泄漏的过程的,见:analyze your app’s memory usage in Part 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:
Understand potential sources of issues: if you know what not to do, you never need to know how to fix it.
Plan for a lot more QA than you ever expected.
Very thorough unit testing.
Run analyses on builds before shipping (we’ll cover this next week, in Part 2).
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
Wait, that’s it?
Yes, this concludes Part 1, wherein we’ve gone over some details of how memory can be leaked. If you’re wondering how to fix all the problems you never knew existed until just now, we’ll get into how to analyze your app’s memory usage in Part 2.