Android 性能优化3 - 解决内存泄露

一、什么是内存泄漏

内存泄漏是指程序已经不会再使用的内存对象,由于 GC 时无法识别,不能及时地回收,一直保留在内存中占用存储空间,不释放给其他对象。

当越来越多的内存泄漏发生时(也可能是频繁地运行了导致内存泄漏这块的程序),系统为应用分配可用堆上的空间就会不断变小,会导致不断启动垃圾回收去释放空间用于执行其他程序(在 Logcat 上可以看到系统不停地打印出 GC 日志)就会造成很多性能问题。

对内存泄漏的优化是 Android 内存优化中很重要的一点。

二、常见内存泄露场景

1、资源性对象未关闭

资源性对象(比如Cursor、File文件等)往往都用了一些缓冲,在不使用时,应该及时关闭它们,以便它们的缓存数据能够及时回收。它们的缓存不只存在于Java虚拟机内,还存在于Java虚拟机外。如果仅仅是把它的引用设置为 null,而不关闭它们,往往会造成内存泄漏。

在编写资源文件读写时,需要在 finally 中关闭资源性对象,避免在异常情况下资源对象未被释放的隐患。

2、注册对象未注销

如果事件注册后未注销,会导致观察者列表中维持着对象的引用,阻止垃圾回收,一般发生在注册广播接收器、注册观察者等。

3、类的静态变量持有大数据对象

静态变量长期维持对象的引用,阻止垃圾回收,如果静态变量持有大的数据对象,如Bitmap等,就很容易引起内存不足等问题。

4、非静态内部类的静态实例

非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被系统回收。

代码示例:

public class MyActivity extends Activity {
private static TestModule mTestModule = null;

public void onCreate(...) {
mTestModule = new TestModule();
}

class TestModule {...}
}

示例分析:
在 Activity 内部创建了一个非静态内部类的静态实例 mTestModule,每次启动 Activity 时都会使用 mTestModule 的数据,这样虽然避免了资源的重复创建,但是 TestModule 为非静态内部类默认持有外部类的引用,而这里又使用该非静态内部类创建了一个静态的实例,该实例的生命周期和应用一样长,这就导致该静态实例一直持有该 Activity 的引用,Activity 的内存资源不能正常回收。

解决方案:

  1. 可以将内部类 TestModule 设为静态内部类。
  2. 将内部类 TestModule 抽取出来封装成一个单例。
  3. 如果需要使用 Context,就在没有特殊要求的情况下使用 Application Context。
  4. 如果需要使用 Activity Context,就记得用完后置空让 GC 可以回收,否则还是会内存泄漏。

5、Handler 临时性内存泄漏

Handler 通过发送 Message 与主线程交互,Message 发出之后存储在 MessageQueue 中,有些 Message 也不是马上就被处理到。在 Message 中存在一个 target,它是 Handler 的一个引用,Message 在 Queue 中存在的时间过长,就会导致 Handler 无法被回收。

代码示例:

public class MyActivity extends Activity {
	Handler mHander = new Handler() {
		public void handleMessage() {
		}	
	}
}

示例分析:
由于 mHandler 是 Handler 的非静态匿名内部类的实例,所以它持有外部类 Activity 的引用,并且消息队列是在一个 Looper 线程中不断轮询处理消息,那就有一种情况,当这个 Activity 退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏。

解决方案:

  • 使用一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时,也可以回收 Handler 持有的对象。
  • 在 Activity 的 Destroy 或者 Stop 时,应该移除消息队列中的消息,避免 Looper 线程的消息队列中有待处理的消息需要处理。
public class MyActivity extends Activity {
	NewHandler mHander = new NewHandler(this);

	public void onDestroy() {
		mHandler.removeCallbacksAndMessages(null);
	}

	private static class NewHandler extends Handler {
		private WeakReference<Context> mContext = null;

		public NewHandler(Context ctx) {
			mContext = new WeakReference<Context>(ctx);
		}
		public void handleMessage() {
		}
	}
}

6、容器中的对象没清理造成的内存泄漏

把一些对象的引用加入集合中,在不需要该对象时,如果没有把它的引用从集合中清理掉,这个集合就会越来越大。如果这个集合是 static,情况就更严重。

7、WebView

Android 中的 WebView 不仅仅存在很大的兼容性问题,不同 Android 系统版本中的 WebView 会有较大的差异,加上不同厂商定制的 ROM 中的 WebView 也存在着差异。更严重的是 WebView 都存在内存泄漏的问题,在应用中只要使用一次 Webview,内存就不会被释放掉。通常解决这个问题的办法是为 WebView 开启独立的一个进程,使用 AIDL 与应用的主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

三、检测

LeakCanary 是 Square 公司开源的功能非常强大的内存泄露检测工具,使用它来检测内存泄露简洁而方便。
网上有很多它的使用教程,在这儿只作推荐,就不详细说了。

四、总结

如果在内存泄漏发生后再去找原因并修复会增加开发的成本,最好是在编写代码时就能够很好地考虑到内存问题,写出更高质量的代码,在此列举了一些常见的内存泄漏场景,在以后的开发过程中需要避免这类问题。

五、推荐

  1. Android 性能优化1 - 启动优化
  2. Android 性能优化2 - 绘制优化
  3. Android 性能优化3 - 解决内存泄露
  4. Android 内存优化4 - 图片优化
  5. Android 性能优化5 - 内存优化
  6. Android 性能优化6 - Hybrid 应用启动优化
发布了179 篇原创文章 · 获赞 91 · 访问量 33万+

猜你喜欢

转载自blog.csdn.net/haha223545/article/details/95344074