Android应用在T-MobileG1上被限制只能使用16MB的内存。这对于手机来说已经是很大的内存了但对于很多开发者来说却仍然有点少。就算你不想把内存耗尽,你也应该尽可能的节约内存来避免其它应用不足以运行。Android保存在内存里的应用越多,用户切换应用的速度也会越快。作为工作的一部分,在开发Android应用的时候我碰到了很多内存泄漏问题,而绝大部分都出自于一个错误:对Context保持了长期的有效引用。
在Android里,context可以有很多用途,但更多的是加载和访问资源。这也就是为什么很多wedget在它们的构造函数里都会接收一个context参数。一般你可能会碰到两种context:Activity和Application,通常开发者都将前者作为需要传入到类或者方法里的context:
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
label.setText("Leaks are bad");
setContentView(label);
}
上面的代码意味着,label这个view持有了整个activity以及activity所拥有的所有资源(通常是整个View层次以及所有的资源)的引用。这就是说,如果你泄漏(意思是你保持了对某一个对象的长期引用使得垃圾回收器无法将其回收)了context,你就泄漏了很多内存,而假如你不加以注意,泄漏整个activity是一件很容易的事情。
当屏幕方向改变时,系统默认会销毁当前activity并创建一个新的保持了之前状态的activity。为此,Android会从资源文件里重新加载应用的UI。现在想象一下假如你的应用里有一张很大的bitmap,而你并不想在每次屏幕旋转的时候都重新加载这张图片,最简单的方式是使用一个静态字段保持对它的引用。
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
label.setText("Leaks are bad");
sBackground = getDrawable(R.drawable.large_bitmap);
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
这段代码很快但是却犯了大错误。它泄漏了第一次屏幕旋转之前创建的第一个activity。当一个Drawable被绑定到一个view时,这个view就被设定成这个drawable的callback了。这意味着这个drawabel拥有了对这个TextView的引用,而这个TextView又拥有对activity的引用。取决于你的代码,activity又会拥有很多其它资源的引用。
这个例子只是context泄漏最简单的一种情况,你可以在源代码里看看我们是怎么通过在activity被销毁的时候将存储的drawables的callbacks置空来解决这个问题。有意思的是,你可以通过很多手段创建一个context泄漏链,那样会更糟糕。他们能让你很快的把内存耗尽。
这里有两个简单的方法来避免Context相关的内存泄漏。最有效的一种方法是避免将context带出它本身的作用域。上个例子说明了,对隐式引用了外部类的内部类的静态引用方式也同样非常危险。(译者:sBackground是静态引用,sBackground对label的隐式引用)第二个解决方案是使用Application上下文。这个context会存活到你的应用终止时,并且不会依存于Activity的生命周期。如果你打算保持一个需要context的长期存活的对象,记住使用Application上下文。可以通过调用Context.getApplicationContext()或者Activity.getApplication()来获得此对象。
总结,避免context相关的内存泄漏,记住以下事项:
不要保持一个对context-activity的长期有效引用(对一个activity的引用的生命周期和这个activity一致)
使用context-application而不是context-activity
如果你不想控制对象的生命周期请避免使用非静态内部类,取而代之是使用内部静态类并保持一个对acticity的弱引用(weakreference)。方法是让内部静态类保持一个对acticity的WeakReference,就像ViewRoot和它的内部类W所做的那样。
垃圾回收器并不是内存泄漏的保险。
一下为VIew的
setBackgroundDrawable 源码
public void setBackgroundDrawable(Drawable d) {
boolean requestLayout = false;
mBackgroundResource = 0;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable.
*/
if (mBGDrawable != null) {
mBGDrawable.setCallback(null);
unscheduleDrawable(mBGDrawable);
}
if (d != null) {
Rect padding = sThreadLocal.get();
if (padding == null) {
padding = new Rect();
sThreadLocal.set(padding);
}
if (d.getPadding(padding)) {
setPadding(padding.left, padding.top, padding.right, padding.bottom);
}
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
if (mBGDrawable == null || mBGDrawable.getMinimumHeight() != d.getMinimumHeight() ||
mBGDrawable.getMinimumWidth() != d.getMinimumWidth()) {
requestLayout = true;
}
// 以下设置了Drawable 对View的引用
d.setCallback(this);
if (d.isStateful()) {
d.setState(getDrawableState());
}
d.setVisible(getVisibility() == VISIBLE, false);
mBGDrawable = d;
if ((mPrivateFlags & SKIP_DRAW) != 0) {
mPrivateFlags &= ~SKIP_DRAW;
mPrivateFlags |= ONLY_DRAWS_BACKGROUND;
requestLayout = true;
}
} else {
/* Remove the background */
mBGDrawable = null;
if ((mPrivateFlags & ONLY_DRAWS_BACKGROUND) != 0) {
/*
* This view ONLY drew the background before and we're removing
* the background, so now it won't draw anything
* (hence we SKIP_DRAW)
*/
mPrivateFlags &= ~ONLY_DRAWS_BACKGROUND;
mPrivateFlags |= SKIP_DRAW;
}
/*
* When the background is set, we try to apply its padding to this
* View. When the background is removed, we don't touch this View's
* padding. This is noted in the Javadocs. Hence, we don't need to
* requestLayout(), the invalidate() below is sufficient.
*/
// The old background's minimum size could have affected this
// View's layout, so let's requestLayout
requestLayout = true;
}
computeOpaqueFlags();
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate();
}