Android 内存优化——常见内存泄露及优化方案

看到了一篇关于内存泄漏的文章后,就想着分享给大家,最后一起学习,一起进步:

如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这种情况下就是内存泄漏。

在Android开发中,一些不好的编程习惯会导致我们开发的app存在内存泄漏的情况,下面介绍一些在Android开发中常见的内存泄漏场景及优化方案。

单例导致内存泄漏

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄漏。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄漏。

java 复制代码
public class AppSettings {
	private static AppSettings sInstance;
	private Context mContext;
	private AppSettings(Context context) {
		this.mContext = context;
	}
	public static AppSettings getInstance(Context context) {
		if (sInstance == null) {
		sInstance = new AppSettings(context);
		}
		return sInstance;
	}
}

向上面代码中这样的单例,如果我们再调用getInstance(Context context)方法的时候传入的context参数是Activity,Service等上下文,就会导致内存泄漏。

以Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有了,但是因为sInstance作为静态单例,(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄漏。

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

java 复制代码
private AppSettings(Context context) {
	this.mContext = context.getApplicationContext();
}

全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。

单例模式对应 应用程序的生命周期,所以我们再构造单例的时候尽量避免使用Activity的上下文,而是使用Appliction的上下文。

静态变量导致内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。

比如下面这样的情况,在Activity中为了避免重复的创建info,将info作为静态变量:

java 复制代码
public class MainActivity extends AppCompatActivity {
private static Info sInfo;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
		if (sInfo != null) {
			sInfo = new Info(this);
		}
	}
}
java 复制代码
class Info {
	public Info(Activity activity) {
	}
}

Info作为Activity的静态成员,并且持有Activity的引用,但是sInfor作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfor仍然引用了Activity,Activity不能被回收,这就导致了内存泄漏。

在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄漏,所以我们再新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄漏。当然,我们也可以在适当的时候将静态变量重置为null,使其不再持用引用,这样也可以避免内存泄漏。

非静态内部内导致内存泄漏

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

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

java 复制代码
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) {
			// 做相应逻辑
			}
		}
	};
}

也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄漏呢?显然不是这样的!

熟悉Handler消息机制的都知道,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息队列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄漏。

给大家看一下源码,如果想学习Handler消息机制可以看看这个博客https://blog.csdn.net/sjw890821sjw/article/details/142138517写的很好:

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

java 复制代码
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<MainActivity> 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的回调和发送的消息给移除掉。

java 复制代码
@Overrideprotected void onDestroy() {
	super.onDestroy();
	mHandler.removeCallbacksAndMessages(null);
}

非静态内部类造成内存泄漏还有一种情况就是使用Thread或者AsyncTask。

比如在Activity中直接new一个子线程Thread:

java 复制代码
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();
	}
}

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

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

很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程Thread和AsynTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄漏。要避免内存泄漏的话还是需要像上面Handler一样使用静态内部类+弱引用的方式。

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

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

java 复制代码
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注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

Timer和TimerTask导致内存泄漏

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

java 复制代码
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,以避免发生内存泄漏。

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

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

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

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

属性动画造成内存泄漏

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

java 复制代码
@Override
protected void onDestroy() {
	super.onDestroy();
	mAnimator.cancel();
}

WebView造成内存泄漏

关于WebView的内存泄漏,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

另外再查阅webView内存泄漏相关资料时看到这种情况:

WebView下面的Callback持有Activity引用,造成WebView内存无法释放,即使是调用了WebView.destory()等方法都无法解决问题(Android5.1之后)

最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:https://blog.csdn.net/xygy8860/article/details/53334476?utm_source=itdadao\&utm_medium=referral)(http://blog.csdn.net/xygy8860/article/details/53334476)[WebView

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

总结:

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

构造单例的时候尽量别用Activity的引用;

静态引用时注意应用对象的置空或者少用静态引用;

使用静态内部类+软引用代替非静态内部类;

及时取消广播或者观察者注册;

耗时任务,属性动画在Activity销毁时记得Cancel;

文件流,Cursor等资源及时关闭;

Activity销毁时WebView的移除和销毁。

相关推荐
Asin²+cos²=16 小时前
关于Android Studio Koala Feature Drop | 2024.1.2下载不了插件的解决办法
android·ide·android studio
大耳猫7 小时前
Android gradle和maven国内镜像地址
android·gradle·maven
-seventy-9 小时前
Android 玩机知识储备
android
CYRUS STUDIO9 小时前
frida脚本,自动化寻址JNI方法
android·运维·自动化·逆向·移动安全·jni·frida
暮志未晚Webgl10 小时前
102. UE5 GAS RPG 实现范围技能奥术伤害
android·java·ue5
Patience to do10 小时前
Android Studio项目(算法计算器)
android·算法·android studio
我又来搬代码了13 小时前
【Android】使用TextView实现按钮开关代替Switch开关
android
新知图书16 小时前
MySQL 9从入门到性能优化-创建触发器
数据库·mysql·性能优化
江-月*夜16 小时前
uniapp vuex 搭建
android·javascript·uni-app
大数据AI人工智能培训专家培训讲师叶梓16 小时前
基于模型内部的检索增强型生成答案归属方法:MIRAGE
人工智能·自然语言处理·性能优化·大模型·微调·调优·检索增强型生成