Android内存优化,常见内存泄漏及优化方案
前言
在安卓开发中,一些不好的编程习惯会导致应用出现内存泄漏的情况。
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内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄漏我们不一定就能注意到,所以在编码过程中一定要养成良好的习惯。
总结下来只要做到以下几点就能避免大多数情况的内存泄漏:
- 构造单例的时候别用Activity的引用;
- 静态引用时注意引用对象的置空或者少用静态变量;
- 使用静态内部类+软引用代替非静态内部类;
- 及时取消广播或者观察者注册;
- 耗时任务、属性动画在activity销毁时记得cancel
- 文件流、Cursor等资源及时关闭;
- activity销毁时WebView的移除和销毁。
说明
本文是翻译的一个大佬的文章,该文章写的是真的好,但大原文章的代码块过于混乱,故将代码块做此整理,并将文章按序排列。
原文地址:https://www.huaweicloud.com/articles/12589272.html