欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android内存优化,常见内存泄漏及优化方案

程序员文章站 2022-04-18 22:56:59
...

前言

在安卓开发中,一些不好的编程习惯会导致应用出现内存泄漏的情况。

1. 单例导致的内存泄漏

单例模式在开发中是非常常见的,但因为单例模式的静态特性使得其生命周期同应用生命周期一样长,如果一个对象没有用处了,但单例还持有它的引用,那么在整个生命周期中都不会被回收,就可能导致内存泄漏。如下代码:

public class Singleton { 
	private static Singleton singleton = null; 
	private Context mContext; 
	public Singleton(Context mContext) { 
		this.mContext = mContext; 
	} 
	public static Singleton getSingleton(Context context){ 
		if (null == singleton){ 
		singleton = new Singleton(context); 
		} 
		return singleton; 
	}
}

1.1 问题阐述

如果我们调用getInstance(Context context)方法的时候传入context参数是Activity、Service等上下文,就到导致内存泄漏。原因如下:

当我们退出Activity时,该Activity就没有用了,但是因为singleton作为静态单例(在应用整个生命周期都会存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,就可能导致内存泄漏。

1.2 优化意见

为了避免这种单例导致的内存泄漏,我们可以将context参数改为全局的上下文,如:

public Singleton(Context mContext) { 
	this.mContext = mContext.getApplicationContext(); 
}

全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就能避免内存泄漏。单例模式对应应用的生命周期,所以我们在构造单例的时候尽量避免使用Activit的上下文,而是用Application的上下文。

2. 非静态内部类导致的内存泄漏

2.1 问题阐述

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

非静态内部类导致的内存泄漏在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Hander是这样写的:

public class MainActivity extends AppCompatActivity { 

	@Override protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); start(); 
		}
	
	 private void start() { 
	 	Message msg = Message.obtain(); 
	 	msg.what = 1; mHandler.sendMessage(msg); 
	 } 
		
	private Handler mHandler = new Handler() { 
		@Override public void handleMessage(Message msg) { 
			if (msg.what == 1) { 
				// 做相应逻辑 
			}
		} 
	};
}

当Activity退出后,msg仍然可能存在于消息队列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,从而导致Activity的内存泄漏。

2.2 优化意见

通常在Android开发中如果要使用内部类,但又要规避内存泄漏,一般都会采用“静态内部类+弱应用”的方式。

public class MainActivity extends AppCompatActivity { 
	private Handler mHandler; 
	@Override 
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		mHandler = new MyHandler(this); 
		start(); 
	} 

	private void start() { 
		Message msg = Message.obtain(); 
		msg.what = 1; 
		mHandler.sendMessage(msg); 
	} 

	private static class MyHandler extends Handler { 
		private WeakReference activityWeakReference; 
		public MyHandler(MainActivity activity) { 
			activityWeakReference = new WeakReference<>(activity); 
		} 

	@Override 
	public void handleMessage(Message msg) { 
		MainActivity activity = activityWeakReference.get(); 
		if (activity != null) { 
			if (msg.what == 1) { 
				// 做相应逻辑 
			} 
		} 
	} 
}

mHandler通过若引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占有的内存单元,这样就不会发生内存泄漏了。

上面的做法确实避免了Activity导致的内存泄漏,发送的msg不再持有Activity的引用了,但msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息移除:

@Override 
protected void onDestroy() { 
	super.onDestroy(); 
	mHandler.removeCallbacksAndMessages(null); 
}

情况2

非静态内部类造成内存泄漏还有一种情况就是使用Thread或者AsyncTask。
比如在Activity中直接new一个子线程Thread:

public class MainActivity extends AppCompatActivity { 
	@Override 
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		new Thread(new Runnable() { 
			@Override 
			public void run() { 
				// 模拟相应耗时逻辑 
				try { 
					Thread.sleep(2000); 
				} catch (InterruptedException e) { 
					e.printStackTrace();
			} } }).start(); 
		}
}

或者直接新建AsyncTask异步任务:

public class MainActivity extends AppCompatActivity { 
	@Override 
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		new AsyncTask() { 
			@Override 
			protected Void doInBackground(Void... params) { 
				// 模拟相应耗时逻辑 
				try {
					Thread.sleep(2000); 
				} catch (InterruptedException e) { 
					e.printStackTrace(); 
				} 
				return null; 
				} 
			}.execute(); 
	}
}

这种方式新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄漏。要避免内存泄漏的话就要像上面handler一样使用“静态内部类+弱引用”的方式(写法参考上面handler的正确写法)

3. 未取消注册或回调导致内存泄漏

比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个广播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,会导致内部泄漏。因为注册广播后,一定要在Activity销毁后取消注册。

public class MainActivity extends AppCompatActivity { 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		this.registerReceiver(mReceiver, new IntentFilter()); 
	} 

	private BroadcastReceiver mReceiver = new BroadcastReceiver() { 
		@Override 
		public void onReceive(Context context, Intent intent) { 
			// 接收到广播需要做的逻辑 
		} 
	}; 

	@Override protected void onDestroy() { 
		super.onDestroy(); 
		this.unregisterReceiver(mReceiver); 
	}
}

在注册观察者模式时,如果不及时取消也会造成内存泄漏。比如,使用“Retrofit+RxJava”注册网络请求的观察者模式回调,同样作为匿名内部类持有外部引用,所以一定要记得在不用或者销毁时取消注册。

4. Timer和TimerTask导致的内存泄漏

Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无线轮播的ViewPager:

public class MainActivity extends AppCompatActivity { 
	private ViewPager mViewPager; 
	private PagerAdapter mAdapter; 
	private Timer mTimer; 
	private TimerTask mTimerTask; 
	
	@Override 
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		init(); 
		mTimer.schedule(mTimerTask, 3000, 3000); 
	} 

	private void init() { 
		mViewPager = (ViewPager) findViewById(R.id.view_pager); 
		mAdapter = new ViewPagerAdapter(); 
		mViewPager.setAdapter(mAdapter); 
		mTimer = new Timer(); 
		mTimerTask = new TimerTask() { 
		@Override 
		public void run() { 
			MainActivity.this.runOnUiThread(new Runnable() { 
				@Override 
				public void run() { 
					loopViewpager(); 
					} 
				}); 
			} };
		 } 

	private void loopViewpager() { 
		if (mAdapter.getCount() > 0) { 
			int curPos = mViewPager.getCurrentItem(); 
			curPos = (++curPos) % mAdapter.getCount();
			mViewPager.setCurrentItem(curPos); 
		} 
	}
	
	private void stopLoopViewPager() { 
		if (mTimer != null) { 
			mTimer.cancel(); 
			mTimer.purge(); 
			mTimer = null; 
		} 
		if (mTimerTask != null) { 
			mTimerTask.cancel(); 
			mTimerTask = null; 
		} 
	}
 
 	@Override 
 	protected void onDestroy() { 
 		super.onDestroy(); 
 		stopLoopViewPager(); 
 	}
}

当我们Activity销毁时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁时要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。

5. 集合中的对象未清理造成内存泄漏

如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此时对象已经无用了),这个对象就造成了内存泄漏。并且如果集合被静态引用的话,集合里面哪些没有用的对象更会造成静态引用了。
所以如果在使用集合时要及时将不用的对象从集合remove,或者clear集合,以免内存泄漏。

6. 资源未关闭或释放导致内存泄漏

在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

7. 属性动画造成内存泄漏

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() { 
	super.onDestroy(); 
	mAnimator.cancel();
}

WebView造成内存泄露:

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
解决方案:
在销毁WebView之前需要先将WebView从父容器中移除,然后再销毁WebView。

@Override
protected void onDestroy() { 
	super.onDestroy(); 
	mWebViewContainer.removeView(mWebView); 	// 先从父控件中移除WebView
	mWebView.stopLoading(); 
	mWebView.getSettings().setJavaScriptEnabled(false); 
	mWebView.clearHistory(); 
	mWebView.removeAllViews(); 
	mWebView.destroy();
}

总结

内存泄漏在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄漏我们不一定就能注意到,所以在编码过程中一定要养成良好的习惯。
总结下来只要做到以下几点就能避免大多数情况的内存泄漏:

  1. 构造单例的时候别用Activity的引用;
  2. 静态引用时注意引用对象的置空或者少用静态变量;
  3. 使用静态内部类+软引用代替非静态内部类;
  4. 及时取消广播或者观察者注册;
  5. 耗时任务、属性动画在activity销毁时记得cancel
  6. 文件流、Cursor等资源及时关闭;
  7. activity销毁时WebView的移除和销毁。

说明

本文是翻译的一个大佬的文章,该文章写的是真的好,但大原文章的代码块过于混乱,故将代码块做此整理,并将文章按序排列。
原文地址:https://www.huaweicloud.com/articles/12589272.html