常见的内存泄露

2016/02/02 Android

.常见的内存泄露

目录
. 内存泄露概述
. Webview导致的内存泄露
. Hanlder导致的内存泄露
. 静态变量导致的内存泄露
. Dialog导致的内存泄露
. Activity对象的OnDestroy与GC
. 参考文档

.内存泄露概述

分析问题的的三个阶段

  • 何时会发生 XX

  • 为何会发生 XX

  • 如何防止发生 XX

.Webview导致的内存泄露

何时Webview会导致内存泄露

为何Webview会导致内存泄露

Why does WebView in the SDK source code extend MockView?

	In the provided scope, the WebView in android.jar is a mock version, which is only for development purpose, with the mock version webview you can render the webview in the layout window, you can call the API in your code.
	
In the runtime scope, the WebView will be replaced.

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

private PluginManager(Context context) {
    mContext = context;
    mPackageInfoCache = new ArrayList<PackageInfo>();
}
public static synchronized PluginManager getInstance(Context context) {
    if (mInstance == null) {
        if (context == null) {
            throw new IllegalStateException(
                    "First call to PluginManager need a valid context.");
        }
        mInstance = new PluginManager(context.getApplicationContext());
    }
    return mInstance;
}

可以看出2.3开始,Android的webview已经解决了内存泄露的问题,至于自定义厂商开发的浏览器,不知道如何处理的。好像不用特殊处理也可以解决

如何防止Webview会导致内存泄露

使用方法:首先应该做的就是不能在xml中定义webview节点,而是在需要的时候动态生成。即:可以在使用WebView的地方放置一个LinearLayout类似ViewGroup的节点,然后在要使用WebView的时候,动态生成即:

WebView      mWebView = new WebView(getApplicationgContext()); 
LinearLayout mll      = findViewById(R.id.xxx); 
mll.addView(mWebView);

然后一定要在onDestroy()方法中显式的调用

protected void onDestroy() {
    super.onDestroy();
    mWebContainer.removeAllViews();
    mWebView.clearHistory();
    mWebView.clearCache(true);
    mWebView.loadUrl("about:blank");
    mWebView.freeMemory(); //new code   
    mWebView.pauseTimers(); //new code
    mWebView.destroy()
    mWebView = null;
}

或者

webViewPlaceholder.removeView(myWebView);
myWebView.removeAllViews();
myWebView.destroy();

或者

public void onDestroy() {
    if (webView != null) {
        ViewGroup parent = (ViewGroup) webView.getParent();
        if (parent != null) {
            parent.removeView(webView);
        }
        webView.removeAllViews();
        webView.destroy();
    }
    super.onDestroy();
  }
}

现在总结下这个问题的解决办法。先大概解释下这个错误,这个错误从字面意思来说是当你结束webview的时候,Webview还依附在父控件下,使用解决这个问题 就是在WebView.destroy()前要解除他们之间的依附关系。一般会碰上这个问题应该是这样对webview进行了操作:否则

Error: WebView.destroy() called while still attached

super.onDestroy();放在后面内存泄露的一些建议

In summary, to avoid context-related memory leaks, remember the following:
Do not keep long-lived references to a context-activity (a reference to an activity should have the same life cycle as the activity itself),

Try using the context-application instead of a context-activity

Avoid non-static inner classes in an activity if you don’t control their life cycle, use a static inner class and make a weak reference to the activity inside,

And remember that a garbage collector is not an insurance against memory leaks. Last but not least, we try to make such leaks harder to make happen whenever we can.

.Hanlder导致的内存泄露

.. Hanlder与Looper、MainThread的关系

如果一个内部类实例的生命周期比Activity更长,那么我们千万不要使用非静态的内部类。比较合理的解决方式是使用静态内部类,然后在该类里使用弱引用来指向所在的Activity。Handler便是此类的一个代表,使用不当很容易导致内存泄露,尤其是发送具有延时特性的消息时,当Android应用启动时,Android Framework会为主线程创建一个Looper对象。这个Looper对象包含一个简单的消息队列Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序Framework事件,例如Activity生命周期方法调用(onCreate)、View事件点击等,这些消息都会被添加到消息队列中并被逐个处理。并且,主线程的Looper对象会伴随该应用程序的整个生命周期。如果在主线程里,实例化一个Handler对象后,它就会自动与主线程Looper的消息队列关联起来,所有发送到消息队列的消息Message都会拥有一个对Handler的引用,但是,从Java角度来看,非静态内部类和匿名类都会隐性的的引用它们所属的外部类,而静态内部类却不会,因此,非静态内部类和匿名类有时候就会导致外部类无法被及时回收。

.. 实例分析

首先,请浏览下面这段handler代码:

	public class SampleActivity extends Activity {
	  private final Handler mLeakyHandler = new Handler() {
	    @Override
	    public void handleMessage(Message msg) {
	    }
	  }
	  
 @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    finish();
  }
 }
}

在使用handler时,这是一段很常见的代码。但是,它却会造成严重的内存泄漏问题。在实际编写中,开发工具往往会提示如下警告:

	 ⚠ In Android, Handler classes should be static or leaks might occur.

对于上面的代码,当activity结束finish时,在延时消息在得到处理前,消息会一直保存在主线程的消息队列里持续10分钟。而且,这条消息持有对handler的引用,而handler又持有对其外部类(在这里,即SampleActivity)的潜在引用。这条引用关系会一直保持直到消息得到处理,从而,这阻止了SampleActivity被垃圾回收器回收,造成应用程序的泄漏。

  • 只要有未处理的消息,那么消息会引用handler(消息的处理会依赖Hanlder,因此肯定不会释放Handlder),非静态的handler又会引用外部类,即Activity,导致Activity无法被回收,造成泄漏;

                  public class Handler {
      ...
         		private boolean enqueueMessage(Message..) {
              msg.target = this;
       ...
    
  • Runnable类属于非静态匿名类,同样会引用外部类。

  • 为了解决遇到的问题,我们要明确一点:静态内部类不会持有对外部类的引用。所以,我们可以把handler类放在单独的类文件中,或者使用静态内部类便可以避免泄漏。

  • 另外,如果想要在handler内部去调用所在的外部类Activity,那么可以在handler内部使用弱引用的方式指向所在Activity,这样统一不会导致内存泄漏。

  • 对于匿名类Runnable,同样可以将其设置为静态类。因为静态的匿名类不会持有对外部类的引用。

      public class SampleActivity extends Activity {
    	
        private static class MyHandler extends Handler {
          private final WeakReference<SampleActivity> mActivity;
    	
          public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
          }
    	
          @Override
          public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {
              // ...
            }
          }
        }
    	
        private final MyHandler mHandler = new MyHandler(this);
    	
        /**
         * Instances of anonymous classes do not hold an implicit
         * reference to their outer class when they are "static".
         */
        private static final Runnable sRunnable = new Runnable() {
            @Override
            public void run() { /* ... */ }
        };
    	
        @Override
        protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
    	
          // Post a message and delay its execution for 10 minutes.
          mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
    	
          // Go back to the previous Activity.
          finish();
        }
      } 但是对于
    
      new Handler().postDelayed(mRunnableLocation, UPDATE_LOCATION_TIME);
    

的使用方式,并没有什么问题,因为此时的Handler不是内部类,不含有任何context的引用,因此Hanlder会不会被回收并不会对Activity有什么影响。所以说,这里说的内部类其实是在该类内部定义的类,对于外部定义的类,并不会有什么引用。

在Handler中使用postDelayed需要额外的注意,为了解决问题,我们有三种方法

  • 使用静态内部Handler/Runnable + 弱引用

  • 在onStop或者onDestory的时候,手动清除Message

在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable。

removeCallbacks(Runnable r)和removeMessages(int what)

public void onDestroy() {  
    //  If null, all callbacks and messages will be removed.  
    mHandler.removeCallbacksAndMessages(null);  
} 


 @Override  
 public void onDestroy() {  
     mHandler.removeMessages(MESSAGE_1);  
     mHandler.removeMessages(MESSAGE_2);  
     mHandler.removeMessages(MESSAGE_3);  
  
     // ... ...  
  
     mHandler.removeCallbacks(mRunnable);  
  
     // ... ...  
 } 
  • 使用Badoo开发的第三方的 WeakHandler

这三种你可以任意选用,第二种看起来更加合理,但是需要额外的工作。第三种方法是我最喜欢的,当然你也要注意WeakHandler不能与外部的强引用共同使用。

.静态变量导致的内存泄露

无用的静态强引用没有显性置空

对于不合理的使用单利模式,或者静态变量,如果该变量包含某个占用内存大的类的引用,在该静态变量回收之前,被引用的类是不会被回收的,这就可能导致内存泄露,因为可能在某个界面,已经不需要那个类了,但是它仍然占用着内存。

例子:

class B{

	public B(Context context){}
}

class A{

	private static B b;
}

  一旦创建A,并在其中实例化B,即使A被回收了,如未显性的将B置空,B将不会被回收,由于B保持着context的引用,导致context也无法被回收,如果context是某个页面的Activity,就会导致很严重的内存泄露问题。

.. 不合理的单例模式造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

	public class AppManager {
	private static AppManager instance;
	private Context context;
	private AppManager(Context context) {
	this.context = context;
	}
	public static AppManager getInstance(Context context) {
	if (instance != null) {
	instance = new AppManager(context);
	}
	return instance;
	}
	}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

  • 1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

  • 2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式应该改为下面这种方式:

	public class AppManager {
	private static AppManager instance;
	private Context context;
	private AppManager(Context context) {
	this.context = context.getApplicationContext();// 使用Application 的context
	}
	public static AppManager getInstance(Context context) {
	if (instance != null) {
	instance = new AppManager(context);
	}
	return instance;
	}
	}

或者这样写,连 Context 都不用传进来了:在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,

context = getApplicationContext();

...
   /**
     * 获取全局的context
     * @return 返回全局context对象
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance != null) {
instance = new AppManager();
}
return instance;
}
}

因此对于静态变量中使用Context一定要保持敏感,如果一个类,自己定义成了单例模式,最好使用全局的context,或者说ApplicationContext,这样能有效保证不内存泄露。如果是某个Activity内部使用,那就说明,该类不适合采用单利模式。

.Dialog不合理使用导致的内存泄露

Dialog最简单的内存泄露,当前Activity被直接Finish,但是Dialog却没有dismiss,Android的每一个Activity都有个WindowManager窗体管理器,同样,构建在某个Activity之上的对话框、PopupWindow也有相应的WindowManager窗体管理器。因为对话框、PopupWindown不能脱离Activity而单独存在着,所以当某个Dialog或者某个PopupWindow正在显示的时候我们去finish()了承载该Dialog(或PopupWindow)的Activity时,就会抛Window Leaked异常了,因为这个Dialog(或PopupWindow)的WindowManager已经没有谁可以附属了,所以它的窗体管理器已经泄漏了。 android.view.WindowLeaked一般会发生在Activity 与Dialog的显示。 Activity 中create 一个Dialog,若你先关闭Dialog再关闭Activity就是正常的,若你先关闭Activity再关闭Dialog就会报错这个android.view.WindowLeaked错误了。

分析这个原因是:Dialog是基于Activity而创建的:new ProgressDialog(this);this 就是Activity。 Activtity先finish,那Dialog就没得依附了,所以就会报android.view.WindowLeaked。

解决方法:

关闭(finish)某个Activity前,要确保附属在上面的Dialog或PopupWindow已经关闭(dismiss)了。 也可在OnCreateDialog()中创建Dialog,让系统去管理对话框.

Activity对象的OnDestroy与GC

有时候对于静态变量的引用,虽然已经完成了onDestroy,但是由于静态变量持有Activity的引用,导致Activity无法被回收。因为,Activity对象的GC和Activity OnDestroy是两个不同的概念,一个是JAVA垃圾回收的概念,一个是Activity生命周期的概念。Activity onDestroy必然早于Activity对象的GC,而Activity OnDestroy 之后有可能Activity 对象不会被GC,例如:

public class ClassName {       
    private static Activity mContext;       //省略  
}  

以上的代码是很危险的,如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放。

Perform any final cleanup before an activity is destroyed. This can happen either because the activity is finishing (someone called finish() on it, or because the system is temporarily destroying this instance of the activity to save space. You can distinguish between these two scenarios with the isFinishing() method.

  • 第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。

  • 第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。

  • 第三、使用WeakReference代替强引用。比如可以使用WeakReference mContextRef;

线程造成的内存泄漏

如果在Activity中定义匿名内部类

    new Thread(new Runnable() {
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }).start(); 改进方式
    
     static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
}

其实还是匿名内部类保持context的引用

资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

避免在存活的对象中,引用即将被回收对象,可能导致循环

参考文档

How to Leak a Context: Handlers & Inner Classes http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html

使用Android studio分析内存泄露 基于屏幕旋转 http://www.jianshu.com/p/c49f778e7acf

Android 内存泄漏总结 https://yq.aliyun.com/articles/3009

Android内存泄露的整理 http://www.xuebuyuan.com/539037.html

Search

    Table of Contents