Android内存泄漏那些事

这篇博文总结一下最近开发中遇到的内存泄漏的场景,并提供一些我所能找到的解决方案。

静态全局类

说起静态全局类,在Android里面用的最多的要数全局单例类(单例模式)。大多数人在构造单例类的时候,都会毫不犹豫地使用这种模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Singleton {

private static Singleton instance = null;

private Context mContext;

public static Singleton getInstance(Context context) {
if (instance != null) {
return instance;
}
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}

private Singleton(Context context) {
this.mContext = context;
}


}

这个时候问题来了:类中的Context指的是什么?想想平时用的时候是不是都会毫不犹豫地传入this,就是Activity本身。而对于单例类来说,mContext成员是永远不会被回收的,也就是说,这个引用指向的Activity无法被回收,从而造成内存泄漏。Google在这篇博客http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html 中提出了解决办法,用Application代替Activity,因此我们可以这样改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Singleton {

private static Singleton instance = null;

private Context mContext;

public static Singleton getInstance(Context context) {
if (instance != null) {
return instance;
}
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}

private Singleton(Context context) {
if (!(context instanceof Application)) {
throw new IllegalArgumentException("context must be instance of Application, " +
"try using getApplicationContext()");
}
this.mContext = context;
}

}

在传入Context的时候检查类型,强制外部传入Application。由于后者的生命周期属于整个应用,所以不存在内存泄漏问题。

如果单例类确实需要Activity的上下文(比方说需要做些UI相关的操作),那么可以提供一个detech()方法:

1
2
3
public void detech() {
this.mContext = null;
}

onDestroy方法中调用单例类的detech()来解除引用关系,防止泄漏。

但通常来说,涉及到UI相关的工作,更提倡用回调来执行,于是就有了更进一步的改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Singleton {

private static Singleton instance = null;

public static Singleton getInstance(Context context) {
if (instance != null) {
return instance;
}
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}

private Context mContext;

private CallBack callBack;

private Singleton(Context context) {
if (!(context instanceof Application)) {
throw new IllegalArgumentException("context must be instance of Application, " +
"try using getApplicationContext()");
}
this.mContext = context;
}

public void setCallBack(CallBack callBack) {
this.callBack = callBack;
}

public void removeCallback() {
this.callBack = null;
}

public interface CallBack {
void doSomethingInUI();
}
}

这样,可以在Activity中向单例类传入一个CallBack,并在doSomethingInUI()回调中做一些UI相关的事情。需要注意的是,由于这个CallBack属于Activity的内部类,这个内部类会拥有外部类Activity的引用,所以需要在onDestroy()方法中调用单例类的removeCallback()类来解除引用关系,防止内存泄漏。

<br>

WebView

WebView一直是我不敢轻易使用的组件(个人感觉越强大的组件,菜鸟用起来越危险)。最近需要重点使用到这个组件,但却遭遇内存泄漏的问题。上网一查才知道,这是Google留给开发者的大坑:WebView自带内存泄漏属性。而且考虑到Android的碎片化情况严重,不同版本的系统泄漏的问题可能还不一样,比如,Android4.4以前的内核采用webkit,而4.4及以后就用chromium内核,所以4.4之前的解决方法可能在4.4及以后的系统不适用。简直蛋疼到极点~囧~

在实战的时候,我发现WebView会持有原Activity的引用,即使在onDestroy()中将WebView置空也会导致Activity泄漏(大概是jni层有指针没有释放这个Activity),加上这个Activity的内容比较多,稍不留神便OOM。

再经过一天的搜索后,我找到一种可以暂时解决这种问题的方法,关键代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private WebView webView;
private LinearLayout webContainer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_face_webview);
initView();
}

private void initView() {
webContainer = (LinearLayout) findViewById(R.id.webview_container);
webView = new WebView(getApplicationContext());
webView.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
webContainer.addView(webView);

// 初始化网页浏览器
webView.requestFocus();
webView.setWebChromeClient(new WebChromeClient());

webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDefaultTextEncodingName("utf-8");

webView.setHorizontalScrollBarEnabled(false);
webView.setVerticalScrollBarEnabled(false);
}

@Override
protected void onResume() {
super.onResume();
webView.loadUrl(URL);
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url){
}
});
}


@Override
protected void onDestroy() {
super.onDestroy();
if (webView != null) {
if (webContainer != null) {
webContainer.removeView(webView);
}
webView.stopLoading();
webView.setWebChromeClient(null);
webView.setWebViewClient(null);
webView.setTag(null);
webView.clearHistory();
webView.removeAllViews();
webView.destroy();
webView = null;
}

}

个人认为这种方法的关键是将WebViewApplication绑定,也就是这句代码

webView = new WebView(getApplicationContext());。而这也意味着你不能在xml中声明webview节点,而必须动态添加进去。另外,在onDestroy()方法中必须手动将WebView从整棵View树中移除(至少在Android4.4的华为P7上需要这样做)。

前面说这种做法能暂时解决问题,但如果WebView需要用到Activity其他的元素,那么它会将Context强转为Activity对应的Context,这时这种做法就会出问题。

那有没有什么方法来“根治”这种问题呢?胡凯在他的文章中提到这种方法:

Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

也就是通过一条新的进程来控制WebView的生命周期。这种方法可以更好地防止内存泄漏影响到主进程。目前我尚未尝试这种方法(好吧还没学会)。

<br>

内部类

内部类指的是那些在类的内部定义的类,又称嵌套类。Java中的内部类会持有外部类的引用,这是虚拟机帮我们处理的(所以才能在内部类中通过.this获得外部类)。如果内部类泄漏了,那么会进一步导致外部类也泄漏。

这种情况发生最多的案例是Activity中持有Handler的引用。Handler的生命周期比较特殊,当Handler发送Message到MessageQueue时,Message会持有一个名为target的引用,这个引用就是Handler本身。熟悉Handler的童鞋知道,MessageQueue是跟线程绑定在一起的消息队列,而我们用的最多的一般都是UI线程的MessageQueue。所以如果Activity有一个继承自Handler的内部类,在Activity启动finish()方法的时候,如果该Handler还在发送消息(即MessageQueue间接持有了该Handler的引用),便容易导致Activity的泄漏。

解决办法有两个,其一是将Handler声明为static,因为静态内部类不会持有外部类的引用,如果要在静态内部类中使用外部类的成员,可以通过WeakReference来持有外部类的引用。另一种方法是将Handler定义在单独的类文件中。方法的选择可以依个人喜好决定。

参考

Avoiding memory leaks

Android内存优化之OOM

Android WebView:性能优化不得不说的事

Android 彻底关闭WebView,防止WebView造成OOM

【Android】 WebView内存泄漏优化之路

Android 5.1 Webview 内存泄漏新场景

Android中Handler引起的内存泄露

细话Java:“失效”的private修饰符