Android内存泄露

Android内存泄露

Posted by pxf on March 16, 2018

Android内存泄露

定义

内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。

原因

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收, 这就是java中内存泄露的发生场景。

危害

只有一个,那就是虚拟机占用内存过高,导致OOM(内存溢出)。 对于Android应用来说,就是你的用户打开一个Activity,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制就会出现FC

先来说一下Java程序运行时的内存分配策略:

  • 静态内存:存放静态数据,这块内存是在编译时就已经分配好的,在程序整个运行期间都存在。
  • 栈内存:程序执行时,局部变量的创建存储区,执行结束后将自动释放。(当时学习java时花的内存分配图,现在都生疏了- -!)
  • 堆内存:存储一些new出来的内存,也叫动态内存分配,这部分内存在不使用时将会有Java垃圾回收器来负责回收。

举一个典型的内存泄漏的例子:

Vector v = new Vector(100);
for (int i = 1; i < 100,; i++) {
	Object o = new Object();
	v.add(o);
	o = null;
}

在这个例子中,我们循环申请了Object对象,并将所申请的对象放入一个集合中,如果我们仅仅释放引用本身,那么Vector仍然引用 该对象,所以这个对象对GC来说是不可回收的。因此,如果对象假如Vector后,还必须从Vector中删除,最简单的方法就是将 Vector对象设置为null.

JavaC++一个很大的区别就是Java有垃圾回收GC(Garbage Collection)自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 Java中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 GC会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么GC就认为这块内存不再使用了。

Android手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成OutOfMemoryError

主要由以下几种情况造成:

1. 数据库Cursor没关。

当我们操作完数据库后,一定要调用`close()`释放资源。 

2. 构造Adapter没有使用缓存ContentView

```java
@Override  
public View getView(int position, View convertView, ViewGroup parent) {  
    ViewHolder vHolder = null;  
    //如果convertView对象为空则创建新对象,不为空则复用  
    if (convertView == null) {  
        convertView = inflater.inflate(..., null);  
        // 创建 ViewHodler 对象  
        vHolder = new ViewHolder();  
        vHolder.img= (ImageView) convertView.findViewById(...);  
        vHolder.tv= (TextView) convertView  
                .findViewById(...);  
        // 将ViewHodler保存到Tag中  
        convertView.setTag(vHolder);  
    } else {  
        //当convertView不为空时,通过getTag()得到View  
        vHolder = (ViewHolder) convertView.getTag();  
    }  
    // 给对象赋值,修改显示的值  
    vHolder.img.setImageBitmap(...);  
    vHolder.tv.setText(...);  
    return convertView;  
}  

static class ViewHolder {  
    TextView tv;  
    ImageView img;  
}  
```

3. 未取消注册广播接收者

`registerReceiver()`和`unregisterReceiver()`要成对出现,通常需要在`Activity`的`onDestory()`方法去取消注册广播接收者。

4. IO流未关闭

注意用完后及时关闭

5. Bitmap使用后未调用recycle()

6. Context泄漏

这是一个很隐晦的`OutOfMemoryError`的情况。先看一个Android官网提供的例子: 

```java
private static Drawable sBackground;  
@Override  
protected void onCreate(Bundle state) {  
	super.onCreate(state);  
	TextView label = new TextView(this);  
	label.setText("Leaks are bad");  
	if (sBackground == null) {  
		sBackground = getDrawable(R.drawable.large_bitmap);  
	}  
	label.setBackgroundDrawable(sBackground);  
	setContentView(label);  
}  
```
这段代码效率很快,但同时又是极其错误的:    
我们看一下`setBackgroundDrawable(Drawable background)`的源码:
```java
 public void setBackgroundDrawable(Drawable background) {
	...
	background.setCallback(this);
 }
```
有`background.setCallback(this);`方法,也就是说`Drawable`拥有`TextView`的引用,而`TextView`又拥有`Activity`*(Context类型)*的引用,
因为`sBackground`为`static`的,即使`Activity`被销毁,但是`sBackground`的生命周期还没走完,所以内存仍然不会被释放。这样就会有内存泄露了。
对,这样想是对的,但是我们看一下`setCallback`的源码:
```java
  public final void setCallback(Callback cb) {
    mCallback = new WeakReference<Callback>(cb);
}
```
我们会发现里面使用了`WeakReference`,所以不会存在内存泄露了,但是官网当时提供的例子明明说有泄露,这是因为在3.0之后,
修复了这个内存泄露的问题,在3.0之前`setCallback`,方法是没有使用`WeakReference`的,所以这种泄露的情况在3.0之前会发生,3.0之后已经被修复。

7. 线程

线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。
```java
public class MyActivity extends Activity {     
	@Override     
	public void onCreate(Bundle savedInstanceState) {         
		super.onCreate(savedInstanceState);         
		setContentView(R.layout.main);         
		new MyThread().start();     
	}       
	private class MyThread extends Thread{         
	@Override         
		public void run() {             
		super.run();             
		//耗时的操作       
		}     
	} 
}  
```
假设`MyThread`的`run`函数是一个很费时的操作,当调用`finish`的时候`Activity`会销毁掉吗?    
事实上由于我们的线程是`Activity`的内部类,所以`MyThread`中保存了`Activity`的一个引用,当`MyThread`的`run`函数没有结束时,`MyThread`是不会被销毁的,
因此它所引用的`Activity`也不会被销毁,因此就出现了内存泄露的问题。

8. 尽量使用ApplicationContext

**`Context`引用的生命周期超过它本身的生命周期,也会导致`Context`泄漏**。
所以如果打算保存一个长时间的对象时尽量使用`Application`这种`Context`类型。
例如: 
```java
mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
```
按道理来说这种系统服务是不会有问题,但是有些厂商在修改的时候,可能会导致`Context`无法被及时释放。

9.Handler导致内存泄露分析

定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 activity 泄漏。Runnable 对象间接地引用了定义它的 activity 对象,而它会被提交到 Handler 的 MessageQueue 中,如果它在 activity 销毁时还没有被处理,那就会导致 activity 泄漏了。

Handler的使用,在Activity退出的时候注意移除(尤其是循环的时候) 有关内存泄露请猛戳内存泄露

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
	    // do something.
    }
}

当我们这样创建Handler的时候Android Lint会提示我们这样一个warning: In Android, Handler classes should be static or leaks might occur.

一直以来没有仔细的去分析泄露的原因,先把主要原因列一下:

  • Android程序第一次创建的时候,默认会创建一个Looper对象,Looper去处理Message Queue中的每个Message,主线程的Looper存在整个应用程序的生命周期.
  • Hanlder在主线程创建时会关联到LooperMessage Queue,Message添加到消息队列中的时候Message(排队的Message)会持有当前Handler引用, 当Looper处理到当前消息的时候,会调用Handler#handleMessage(Message).就是说在Looper处理这个Message之前, 会有一条链MessageQueue -> Message -> Handler -> Activity,由于它的引用导致你的Activity被持有引用而无法被回收`
  • 在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。

##具体分析

public class SampleActivity extends Activity {

  private final Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
		  // do something
		}
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	// 发送一个10分钟后执行的一个消息
	mHandler.postDelayed(new Runnable() {
	  @Override
	  public void run() { }
	}, 600000);

	// 结束当前的Activity
	finish();
}

finish()的时候,该Message还没有被处理,Message持有Handler,Handler持有Activity,这样会导致该Activity不会被回收,就发生了内存泄露.

##解决方法

  • 通过程序逻辑来进行保护。
    • 如果Handler中执行的是耗时的操作,在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线, Activity自然会在合适的时候被回收。
    • 如果Handler是被delayMessage持有了引用,那么在ActivityonDestroy()方法要调用Handlerremove*方法,把消息对象从消息队列移除就行了。
      • 关于Handler.remove*方法
        • removeCallbacks(Runnable r) ——清除r匹配上的Message。
        • removeC4allbacks(Runnable r, Object token) ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。
        • removeCallbacksAndMessages(Object token) ——清除token匹配上的Message。
        • removeMessages(int what) ——按what来匹配
        • removeMessages(int what, Object object) ——按what来匹配
          我们更多需要的是清除以该Handlertarget的所有Message(Callback)就调用如下方法即可handler.removeCallbacksAndMessages(null);
  • Handler声明为静态类。 静态类不持有外部类的对象,所以你的Activity可以随意被回收。但是不持有Activity的引用,如何去操作Activity中的一些对象? 这里要用到弱引用
public class MyActivity extends Activity {
	private MyHandler mHandler;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mHandler = new MyHandler(this);
	}

	@Override
	protected void onDestroy() {
		// Remove all Runnable and Message.
		mHandler.removeCallbacksAndMessages(null);
		super.onDestroy();
	}

	static class MyHandler extends Handler {
		// WeakReference to the outer class's instance.
		private WeakReference<MyActivity> mOuter;

		public MyHandler(MyActivity activity) {
			mOuter = new WeakReference<MyActivity>(activity);
		}

		@Override
		public void handleMessage(Message msg) {
			MyActivity outer = mOuter.get();
			if (outer != null) {
				// Do something with outer as your wish.
			}
		}
	}
}

10. 由上面的Handler可以引伸出来的匿名内部类、非静态内部类和异步现成导致的内存泄漏。

下面看一个非静态内部类创建静态实例导致的内存泄漏
```java
public class MainActivity extends AppCompatActivity {
	private static TestResource mResource = null;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		if(mManager == null){
			mManager = new TestResource();
		}
		//...
	}
	class TestResource {
		//...
	}
}
```
因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态实例,该实例的声明周期与应用的一样长,
这就导致了静态实例一直会持有`Activity`的引用而造成内存泄漏。             
下面再看一个匿名内部类和异步现成的现象:        
```java
public class MainActivity extends Activity {
	...
	Runnable ref1 = new MyRunable();
	Runnable ref2 = new Runnable() {
		@Override
		public void run() {

		}
	};
	...
}
```
上面的离职中`ref1`对象是没问题的,但是`ref2`这个匿名类的实现对象中有外部类的引用,如果此时线程的生命周期与`Activity`的不一致时就会造成了泄漏。

11. 单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如:
```java
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;
	}
}
```
这里如果传入的是`Activity`的`Context`,当该`Context`的`Activity`退出后,由于其被单例对象引用,所以会导致`Activity`无法被回收,就造成内存泄漏。

12. 集合类泄漏

集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有 响应的删除机制,很可能导致集合所占用的内存只增不减。

总结一下避免Context泄漏应该注意的问题:

  • 使用getApplicationContext()类型。
  • 注意对Context的引用不要超过它本身的生命周期。
  • 慎重的使用static关键字。
  • Activity里如果有线程或Handler时,一定要在onDestroy()里及时停掉。

13.静态的Activity

activity 类中定义一个 static 变量,并且将其指向一个运行中的 activity 实例。如果在 activity 的生命周期结束之前,没有清除这个引用,那它就会泄漏了。这是因为 activity(例如 MainActivity) 的类对象是静态的,一旦加载,就会在 APP 运行时一直常驻内存,因此如果类对象不卸载,其静态成员就不会被垃圾回收。

14.静态View

一旦 view 被加入到界面中,它就会持有 context 的强引用,也就是我们的 activity。由于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其从 view 层级中移除

15.内部类

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。

16.Threads和Timer Tasks

只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 activity 的强引用,进而导致内存泄漏。

17.Sensor Manager

系统服务可以通过 context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。

什么情况导致oom-乐视-美团

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0920/3478.html

1)使用更加轻量的数据结构 2)Android里面使用Enum 3)Bitmap对象的内存占用 4)更大的图片 5)onDraw方法里面执行对象的创建 6)StringBuilder