一、关于OOM与内存泄露的概念
我们在Android开发过程中经常会遇到OOM的错误,这是因为我们在APP中没有考虑dalvik虚拟机内存消耗的问题。
1、什么是OOM
OOM:即OutOfMemoery,顾名思义就是指内存溢出了。内存溢出是指APP向系统申请超过最大阀值的内存请求,系统不会再分配多余的空间,就会造成OOM
error。在我们Android平台下,多数情况是出现在图片不当处理加载的时候。
Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。Android
APP的所能申请的最大内存大小是多少,有人说是16MB,有人又说是24MB。其实这些答案都算对,因为Android是开源的操作系统,不同的手机厂商其实是拥有修改这部分权限能力的,所以就造成了不同品牌和不同系统的手机,对于APP的内存支持也是不一样的,不过我们可以通过Runtime这个类来获取当前设备的Android系统为每个应用所产生的内存大小。APP并不会为我们创建Runtime的实例,Java为我们提供了单例获取的方式Runtime.getRuntime()。通过maxMemory()方法获取系统可为APP分配的最大内存,totalMemory()获取APP当前所分配的内存heap空间大小。
2、什么是内存泄露
Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一个对象不被任何引用所指向,则该对象会在被GC发现的时候被回收;另外,如果一组对象中只包含互相的引用,而没有来自它们外部的引用(例如有两个对象A和B互相持有引用,但没有任何外部对象持有指向A或B的引用),这仍然属于不可到达,同样会被GC回收。
在Android程序开发中,当一个对象已经不需要再使用了,本该被回收时,而另外一个正在使用的对象持有它的引用从而导致它不能被回收,这就导致本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了。
内存泄露的危害:只有一个,那就是虚拟机占用内存过高,导致OOM(内存溢出),程序出错。对于Android应用来说,就是你的用户打开一个Activity,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制,FC。
了解了内存泄漏的原因及影响后,我们需要做的就是掌握常见的内存泄漏,并在以后的Android程序开发中,尽量避免它。
二、常见的内存泄漏及解决方案
1、单例造成的内存泄漏
Android的单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏。因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
如下这个典例:
[java] view
plain copy
- 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的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
[java] view
plain copy
- public class AppManager {
- private static AppManager instance;
- private Context context;
- private AppManager(Context context) {
- this.context = context.getApplicationContext();
- }
- public static AppManager getInstance(Context context) {
- if (instance != null) {
- instance = new AppManager(context);
- }
- return instance;
- }
- }
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
2、非静态内部类创建静态实例造成的内存泄漏
在Java 中,非静态匿名内部类会持有其外部类的隐式引用,如果你没有考虑过这一点,那么存储该引用会导致Activity被保留,而不是被垃圾回收机制回收。Activity对象持有其View层以及相关联的所有资源文件的引用,换句话说,如果你的内存泄漏发生在Activity中,那么你将损失大量的内存空间。
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,会出现这种写法:
[java] view
plain copy
- 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内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext。
3、Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
[java] view
plain copy
- Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- mImageView.setImageBitmap(mBitmap);
- }
- }
上面是一段简单的Handler的使用。当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法:
//要做的事情,这里再次调用此Runnable对象,以实现每两秒实现一次的定时器操作
handler.postDelayed(this, 2000);
该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue
-> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在Looper中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
使用Handler导致内存泄露的解决方法
方法一:通过程序逻辑来进行保护。
1).在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
2).如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
方法二:将Handler声明为静态类。
静态类不持有外部类的对象,所以你的Activity可以随意被回收。代码如下:
[java] view
plain copy
- static class MyHandler extends Handler {
- @Override
- public void handleMessage(Message msg) {
- mImageView.setImageBitmap(mBitmap);
- }
- }
但其实没这么简单。使用了以上代码之后,你会发现,由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference):
[java] view
plain copy
- static class MyHandler extends Handler {
- WeakReference<Activity > mActivityReference;
- MyHandler(Activity activity) {
- mActivityReference= new WeakReference<Activity>(activity);
- }
- @Override
- public void handleMessage(Message msg) {
- final Activity activity = mActivityReference.get();
- if (activity != null) {
- mImageView.setImageBitmap(mBitmap);
- }
- }
- }
将代码改为以上形式之后,就算完成了。
延伸:什么是WeakReference?
WeakReference弱引用,与强引用(即我们常说的引用)相对,它的特点是,GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。
4、线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:
[java] view
plain copy
- //——————test1
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- SystemClock.sleep(10000);
- return null;
- }
- }.execute();
- //——————test2
- new Thread(new Runnable() {
- @Override
- public void run() {
- SystemClock.sleep(10000);
- }
- }).start();
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成,
那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:
[java] view
plain copy
- static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
- private WeakReference<Context> weakReference;
- public MyAsyncTask(Context context) {
- weakReference = new WeakReference<>(context);
- }
- @Override
- protected Void doInBackground(Void... params) {
- SystemClock.sleep(10000);
- return null;
- }
- @Override
- protected void onPostExecute(Void aVoid) {
- super.onPostExecute(aVoid);
- MainActivity activity = (MainActivity) weakReference.get();
- if (activity != null) {
- //...
- }
- }
- }
- static class MyRunnable implements Runnable{
- @Override
- public void run() {
- SystemClock.sleep(10000);
- }
- }
- //——————
- new Thread(new MyRunnable()).start();
- new MyAsyncTask(this).execute();
通过上面的代码,新线程再也不会持有一个外部Activity 的隐式引用,而且该Activity也会在配置改变后被回收。这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。
如果我们线程做的是一个无线循环更新UI的操作,如下代码:
[java] view
plain copy
- private static class MyThread extends Thread {
- @Override
- public void run() {
- while (true) {
- SystemClock.sleep(1000);
- }
- }
- }
这样虽然避免了Activity无法销毁导致的内存泄露,但是这个线程却发生了内存泄露。在Java中线程是垃圾回收机制的根源,也就是说,在运行系统中DVM虚拟机总会使硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。因此,你必须为你的后台线程实现销毁逻辑!下面是一种解决办法:
[java] view
plain copy
- private static class MyThread extends Thread {
- private boolean mRunning = false;
- @Override
- public void run() {
- mRunning = true;
- while (mRunning) {
- SystemClock.sleep(1000);
- }
- }
- public void close() {
- mRunning = false;
- }
- }
我们在Activity退出时,可以在 onDestroy()方法中显示调用mThread.close();以此来结束该线程,这就避免了线程的内存泄漏问题。
5、资源对象没关闭造成的内存泄漏
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
[java] view
plain copy
- Cursor cursor = getContentResolver().query(uri...);
- if (cursor.moveToNext()) {
- ... ...
- }
- 修正示例代码:
- [java]
- Cursor cursor = null;
- try {
- cursor = getContentResolver().query(uri...);
- if (cursor != null &&cursor.moveToNext()) {
- ... ...
- }
- } finally {
- if (cursor != null) {
- try {
- cursor.close();
- } catch (Exception e) {
- //ignore this
- }
- }
- }
6、Bitmap没有回收导致的内存溢出
Bitmap的不当处理极可能造成OOM,绝大多数情况都是因这个原因出现的。Bitamp位图是Android中当之无愧的胖小子,所以在操作的时候当然是十分的小心了。由于Dalivk并不会主动的去回收,需要开发者在Bitmap不被使用的时候recycle掉。使用的过程中,及时释放是非常重要的。同时如果需求允许,也可以去BItmap进行一定的缩放,通过BitmapFactory.Options的inSampleSize属性进行控制。如果仅仅只想获得Bitmap的属性,其实并不需要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。最后建议大家在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存,推荐使用android-universal-imageloader或者xUtils,牛人出品,必属精品。
7、构造Adapter时,没有使用缓存的convertView
以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
[java] view
plain copy
- <span style="font-size:14px;">public View getView(int position, ViewconvertView, ViewGroup parent)</span>
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list
item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View
convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list
item的view对象的过程可以查看:
Android.widget.AbsListView.java --> voidaddScrapView(View scrap)方法。
示例代码:
[java] view
plain copy
- public View getView(int position, ViewconvertView, ViewGroup parent) {
- View view = new Xxx(...);
- ... ...
- return view;
- }
修正示例代码:
[java] view
plain copy
- public View getView(int position, ViewconvertView, ViewGroup parent) {
- View view = null;
- if (convertView != null) {
- view = convertView;
- populate(view, getItem(position));
- ...
- } else {
- view = new Xxx(...);
- ...
- }
- return view;
- }
三、预防OOM的几点建议
Android开发过程中,在 Activity的生命周期里协调耗时任务可能会很困难,你一不小心就会导致内存泄漏问题。下面是一些小提示,能帮助你预防内存泄漏问题的发生:
1、合理使用static:
每一个非静态内部类实例都会持有一个外部类的引用,若该引用是Activity 的引用,那么该Activity在被销毁时将无法被回收。如果你的静态内部类需要一个相关Activity的引用以确保功能能够正常运行,那么你得确保你在对象中使用的是一个Activity的弱引用,否则你的Activity将会发生意外的内存泄漏。但是要注意,当此类在全局多处用到时在这样干,因为static声明变量的生命周期其实是和APP的生命周期一样的,有点类似与Application。如果大量的使用的话,就会占据内存空间不释放,积少成多也会造成内存的不断开销,直至挂掉。static的合理使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。
2、善用SoftReference/WeakReference/LruCache
Java、Android中有没有这样一种机制呢,当内存吃紧或者GC扫过的情况下,就能及时把一些内存占用给释放掉,从而分配给需要分配的地方。答案是肯定的,java为我们提供了两个解决方案。如果对内存的开销比较关注的APP,可以考虑使用WeakReference,当GC回收扫过这块内存区域时就会回收;如果不是那么关注的话,可以使用SoftReference,它会在内存申请不足的情况下自动释放,同样也能解决OOM问题。同时Android自3.0以后也推出了LruCache类,使用LRU算法就释放内存,一样的能解决OOM,如果兼容3.0一下的版本,请导入v4包。关于第二条的无关引用的问题,我们传参可以考虑使用WeakReference包装一下。
3、谨慎handler
在处理异步操作的时候,handler + thread是个不错的选择。但是相信在使用handler的时候,大家都会遇到警告的情形,这个就是lint为开发者的提醒。handler运行于UI线程,不断处理来自MessageQueue的消息,如果handler还有消息需要处理但是Activity页面已经结束的情况下,Activity的引用其实并不会被回收,这就造成了内存泄漏。解决方案,一是在Activity的onDestroy方法中调用
handler.removeCallbacksAndMessages(null);取消所有的消息的处理,包括待处理的消息;二是声明handler的内部类为static。
4、不要总想着Java 的垃圾回收机制会帮你解决所有内存回收问题
就像上面的示例,我们以为垃圾回收机制会帮我们将不需要使用的内存回收,例如:我们需要结束一个Activity,那么它的实例和相关的线程都该被回收。但现实并不会像我们剧本那样走。Java线程会一直存活,直到他们都被显式关闭,抑或是其进程被Android系统杀死。所以,为你的后台线程实现销毁逻辑是你在使用线程时必须时刻铭记的细节,此外,你在设计销毁逻辑时要根据Activity的生命周期去设计,避免出现Bug。
考虑你是否真的需要使用线程。Android应用的框架层为我们提供了很多便于开发者执行后台操作的类。例如:我们可以使用Loader 代替在Activity 的生命周期中用线程通过注入执行短暂的异步后台查询操作,考虑用Service将结构通知给UI的BroadcastReceiver。最后,记住,这篇博文中对线程进行的讨论同样适用于AsyncTask(因为AsyncTask使用ExecutorService执行它的任务)。然而,虽说ExecutorService只能在短暂操作(文档说最多几秒)中被使用,那么这些方法导致的Activity内存泄漏应该永远不会发生。
5、ListView和GridView的item缓存
对于移动设备,尤其硬件参差不齐的android生态,页面的绘制其实是很耗时的,findViewById也是蛮慢的。所以不重用View,在有列表的时候就尤为显著了,经常会出现滑动很卡的现象,所以我们要善于重复利用创建好的控件。这里主要注意两点:
1)convertView重用
ListView中的每一个Item显示都需要Adapter调用一次getView()的方法,这个方法会传入一个convertView的参数,这个方法返回的View就是这个Item显示的View。Android提供了一个叫做Recycler(反复循环)的构件,就是当ListView的Item从滚出屏幕视角之外,对应Item的View会被缓存到Recycler中,相应的会从生成一个Item,而此时调用的getView中的convertView参数就是滚出屏幕的缓存Item的View,所以说如果能重用这个convertView,就会大大改善性能。
2)使用ViewHolder重用
我们都知道在getView()方法中的操作是这样的:先从xml中创建view对象(inflate操作,我们采用了重用convertView方法优化),然后在这个view去findViewById,找到每一个item的子View的控件对象,如:ImageView、TextView等。这里的findViewById操作是一个树查找过程,也是一个耗时的操作,所以这里也需要优化,就是使用ViewHolder,把每一个item的子View控件对象都放在Holder中,当第一次创建convertView对象时,便把这些item的子View控件对象findViewById实例化出来并保存到ViewHolder对象中。然后用convertView的setTag将viewHolder对象设置到Tag中,
当以后加载ListView的item时便可以直接从Tag中取出复用ViewHolder对象中的,不需要再findViewById找item的子控件对象了。这样便大大提高了性能。