本文开始列举常见的内存泄漏场景,分析为何会产生,并且给出解决方案。总共列举了几下几种场景。
1. 静态activity
2. 静态view
3. 单例模式使用不当
4. 非静态内部类、匿名内部类、线程
5. Handler造成的内存泄漏
6. 资源未关闭造成的内存泄漏
7. Activity、Context的不正确使用
8. 集合中对象没清理造成的内存泄漏
9. WebView造成的泄露
1. 静态Activity
泄漏 activity 最简单的方法就是在 activity 类中定义一个 static 变量,并且将其指向一个运行中的 activity 实例。如果在 activity 的生命周期结束之前,没有清除这个引用,那它就会泄漏了。这是因为 activity(例如 MainActivity) 的类对象是静态的,一旦加载,就会在 APP 运行时一直常驻内存,因此如果类对象不卸载,其静态成员就不会被垃圾回收。
private static GcActivity mActivity;
private void setActivity(){
mActivity = this;
}
mClickBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setActivity();
startActivity(new Intent(mActivity, GcActivity.class));
}
});
2. 静态View
另一种类似的情况是对经常启动的 activity 实现一个单例模式,让其常驻内存可以使它能够快速恢复状态。然而,就像前文所述,不遵循系统定义的 activity 生命周期是非常危险的,也是没必要的,所以我们应该极力避免。
但是如果我们有一个创建起来非常耗时的 View,在同一个 activity 不同的生命周期中都保持不变呢?所以让我们为它实现一个单例模式,就像这段代码。现在一旦 activity 被销毁,那我们就应该释放大部分的内存了。
内存泄漏了!因为一旦 view 被加入到界面中,它就会持有 context 的强引用,也就是我们的 activity。由于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其从 view 层级中移除。
3. 单例模式使用不当
单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象仍然还持有该对象的引用,那么这个对象就不能被正常回收,这就导致了内存泄漏。
public class AppManager {
private Context mContext;
private static AppManager mInstance;
private AppManager(Context context){
mContext = context;
}
public static AppManager getInstance(Context context){
if (mInstance==null)
mInstance = new AppManager(context);
return mInstance;
}
}
分两种情况来讨论。
case1:传入的是activity的context,当该activity被销毁后,该单例模式仍然持有activity的引用并且生命周期是整个App运行周期内。即activity被销毁后,单例还持有activity的引用,造成内存泄漏。
case2:传入application的context,单例的生命周期和application一样长,没有问题。
如何来解决此类情况,保证传入单例模式的context的对象,都是application的引用。
private AppManager(Context context){
mContext = context.getApplicationContext();
}
public static AppManager getInstance(Context context){
if (mInstance==null)
mInstance = new AppManager(context);
return mInstance;
}
4. 非静态内部类、匿名内部类、线程
java的非静态内部类和匿名内部类会默认包含外部类的引用,因为在非静态(或匿名)内部类中可以直接访问外部类的方法和私有变量。
普通情况下我们使用的都是强引用,比如
A a = new A();
B b = new B(a);
当我们手动将a赋值为null时,正常来讲GC会回收a,但此时由于b持有a的强引用导致GC无法回收a,这就造成java内存消耗越来越大,从而导致了内存泄漏。
new AsyncTask() {
@Override
protected Object doInBackground(Object[] params) {
SystemClock.sleep(10000);
return null;
}
}.execute();
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
}
}).start();
上面的异步任务都是一个匿名内部类,因此它对activity会有一个隐式引用,当任务还未完成时activity就已经被销毁了,会导致activity的内存资源无法被回收而导致内存泄漏。
通常情况下我们利用静态类加弱引用来解决此类情况。
static class MyAsyncTask extends AsyncTask{
private WeakReference<Context> mWeakContext;
public MyAsyncTask(Context context){
mWeakContext = new WeakReference<>(context);
}
@Override
protected Object doInBackground(Object[] params) {
SystemClock.sleep(5000);
return null;
}
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
GcActivity activity = (GcActivity) mWeakContext.get();
if (activity==null){
return;
}
//
}
}
5. Handler造成的内存泄漏
Handler的应用场景比较常见,我们大部分时候是这样创建Handler的。
private Handler myHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
}
};
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
我们的做法应该是,创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,并且在Activity的Destroy时或者Stop时应该移除消息队列中的消息。
private MyHandler myHandler = new MyHandler(mActivity);
private static class MyHandler extends Handler{
private WeakReference<Context> mWeakContext;
public MyHandler(Context context){
mWeakContext = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
Activity activity = (Activity) mWeakContext.get();
if (activity==null)
return;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
6. 资源未关闭造成的内存泄漏
在使用BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等资源时,应该在activity注销或者关闭的时候及时关闭或注销资源。否则这些资源将不会被回收从而造成内存泄漏。
7. Activity、Context的不正确使用
应用场景
在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。
假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。
解决方案
1. 使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;
2. 对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。
8. 集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
private ArrayList<String> mList;
@Override
protected void onDestroy() {
super.onDestroy();
if (mList!=null){
mList.clear();
mList = null;
}
}
9. WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
总结
- 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内;如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。
- 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。
- 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:1.将内部类改为静态内部类;2.静态内部类中使用弱引用来引用外部类的成员变量
- Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable。
- 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
- 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
- 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。