安卓内存优化之--内存泄露

来源:互联网 发布:女士香水哪款好闻知乎 编辑:程序博客网 时间:2024/05/17 07:25

内存泄露的几种情况:

  • 查询数据库而没有关闭Cursor

在Android中,Cursor是很常用的一个对象,但在写代码是,经常会有人忘记调用close, 或者因为代码逻辑问题状况导致close未被调用。

通常,在Activity中,我们可以调用startManagingCursor或直接使用managedQuery让Activity自动管理Cursor对象。但需要注意的是,当Activity管理后,Cursor将不再可用!
若操作Cursor的代码和UI不同步(如后台线程),那没需要先判断Activity是否已经结束,或者在调用OnDestroy前,先等待后台线程结束。

关闭cursor的正确做法

    Cursor c = queryCursor();      try {              ......      } catch (Exception e) {      } finally {          c.close(); //在finally中调用close(), 保证其一定会被调用       }  

有些这样写,是不一定能关闭cursor的。

try {      Cursor c = queryCursor();      int a = c.getInt(1);      ......      c.close();  } catch (Exception e) {  } 

原因在于如果发生异常,那么程序退出,并不会执行c.close()。。。

  • 注册没取消造成的内存泄露

这种Android的内存泄露比纯Java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收。

比如你在使用EventBus的时候注册了,但是并未解注册。如果EventBus会被反复使用,那么可能就会有多个对象同时被发送出去。这样显然是对内存的浪费。例如如下问题

EventBus重复发送同一条广播

首先,EventBus使用时接收端需要注册, 不用时接收端需要解注册, Eventbus后台会使用集合来管理注册过的tag, 即使该tag曾被我们释放并重新创建, 但只要释放时没有解注册, 就会导致重复注册, 每一次释放创建都会导致EventBus管理的集合中多出一份该类的tag, 最终引起消息的重复发送, 这种问题在单层的activity中容易解决, 只要在生命周期的最后阶段解注册就行, 重点强调嵌套层数比较多, 关联度较高的页面, 退出时解注册一定要解注册


  • 集合中对象没清理造成的内存泄露

  我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

  • 未关闭InputStream/OutputStream

在使用文件或者访问网络资源时,使用了InputStream/OutputStream也会导致内存泄露(解决办法:类似cursor)

  • bitmap问题(参考如下文章)

参考文章:[Android 性能优化系列]内存之终极篇降低你的内存消耗

当你加载一张 Bitmap 的时候,你可以选择只在你的确需要在屏幕上显示的时候才将它加载到内存里,或者通过缩放原图的尺寸来减小内存占用。请记住随着 Bitmap 尺寸的增长,图片所消耗的内存会成平方量级的增长,因为 Bitmap 的 x 轴和 y 轴都在增长。

注意:在 Android2.3及以下的平台,Bitmap 对象在你应用堆中的大小总是一样的(因为他实际的内存被单独存放在了本地内存中)。这会使得我们分析 Bitmap 消耗的内存资源变得非常困难,因为大多数分析工具只收集你应用的 Dalvik 堆信息。但是,从 Android3.0开始,为了提升垃圾收集和调试的能力,Bitmap 的像素数据被放在了你的 Dalvik 堆里。因此如果你的应用使用了 Bitmap 并且你在老设备上无法发现应用的内存消耗问题,那么请你在 Android3.0或者更高的机型上调试他。

(以上文段均来自参考文章)

优化办法:

Bitmap没调用recycle()

  Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才它设置为null.

  虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试结果显示它并没能立即释放内存。但是我觉得它应该还是能大大的加速Bitmap的主要内存的释放。

从上面的文段我们可以发现,三点优化:

1.当图片需要在屏幕上显示的时候才将它加载到内存里

2.通过缩放原图的尺寸来减小内存占用

<span style="color:#000000;">//解决加载图片 内存溢出的问题 //Options 只保存图片尺寸大小,不保存图片到内存      <span style="color:#CC0000;"><strong> BitmapFactory.Options opts = new BitmapFactory.Options();       //缩放的比例,缩放是很难按准备的比例进行缩放的,其值表明缩放的倍数,SDK中建议其值是2的指数值,值越大会导致图片不清晰       opts.inSampleSize = 2;</strong></span>       Bitmap bmp = null;       bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);                                    ...        //回收       bmp.recycle();</span>

3.对图片采用软引用,及时地进行recyle()操作

SoftReference<Bitmap> bitmap;bitmap = new SoftReference<Bitmap>(pBitmap);if(bitmap != null){if(bitmap.get() != null && !bitmap.get().isRecycled()){bitmap.get().recycle();bitmap = null;}}

图片占用进程的内存算法简介

android中处理图片的基础类是Bitmap,顾名思义,就是位图。占用内存的算法如下:

图片的width*height*Config

如果Config设置为ARGB_8888,那么上面的Config就是4一张480*320的图片占用的内存就是480*320*4 byte

在默认情况下android进程的内存占用量为16M,因为Bitmap除了java中持有数据外,底层C++skia图形库还会持有一个SKBitmap对象,因此一般图片占用内存推荐大小应该不超过8M。这个可以调整,编译源代码时可以设置参数。


以下参考自一篇文章。博客地点不详


  • 构造Adapter时,没有使用缓存的 convertView

以构造ListViewBaseAdapter为例,在BaseAdapter中提供了方法:

public View getView(int position, View convertView, ViewGroup parent)

来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list itemview对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list itemview对象(初始化时缓存中没有view对象则convertViewnull)

由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list itemview对象的过程可以查看:

public View getView(int position, View convertView, ViewGroup parent) {  View view = null;  if (convertView != null) {  view = convertView;  populate(view, getItem(position));  ...  } else {  view = new Xxx(...);  ...  }  return view;}
  • 线程导致内存溢出

    线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。

    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();  

                //do somthing  

            }  

        }  

    }  

    这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设MyThreadrun函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一 般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。

        由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThreadrun函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

    有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。

    针对这种线程导致的内存泄露问题的解决方案:

        第一、将线程的内部类,改为静态内部类。

        第二、在线程内部采用弱引用保存Context引用。


  • static造成的内存溢出

staticJava中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。

public class ClassName {  

     private static Context mContext;  

     //省略  

}  

以上的代码是很危险的,如果将Activity赋值到mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放。

我们举Android文档中的一个例子。

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);  

}

    sBackground, 是 一个静态的变量,但是我们发现,我们并没有显式的保存Contex的引用,但是,当DrawableView连接之后,Drawable就将View设 置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:

    Drawable->TextView->Context

    所以,最终该Context也没有得到释放,发生了内存泄露。

针对static的解决方案:

第一、应该尽量避免static成员变量引用资源耗费过多的实例,比如Context

    第二、Context尽量使用Application Context,因为ApplicationContext的生命周期比较长,引用它不会出现内存泄露的问题。

    第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;

    该部分的详细内容也可以参考Android文档中Article部分。


  • 界面切换造成的内存溢出

    方法1:单个页面,横竖屏切换N次后OOM

    1看看页面布局当中有没有大的图片,比如背景图之类的。

    去除xml中相关设置,改在程序中设置背景图(放在onCreate()方法中):

              Drawable bg = getResources().getDrawable(R.drawable.bg);

              XXX.setBackgroundDrawable(rlAdDetailone_bg);

              Activity destory时注意,bg.setCallback(null);防止Activity得不到及时的释放

     2. 跟上面方法相似,直接把xml配置文件加载成view再放到一个容器里

    然后直接调用 this.setContentView(View view);方法,避免xml的重复加载

     

    方法2: 在页面切换时尽可能少地重复使用一些代码

    比如:重复调用数据库,反复使用某些对象等等.....

  • 未释放对象的引用造成的内存溢出

当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。

比如Handler,在Activity的onDestroy()方法中要将其释放。

if (handlerOfHome != null) {    handlerOfHome.removeCallbacksAndMessages(null);}

示例A

public class DemoActivity extends Activity {

  ... ...

  private Handler mHandler = ...

  private Object obj;

  public void operation() {

  obj = initObj();

  ...

  [Mark]

  mHandler.post(new Runnable() {

   public void run() {

   useObj(obj);

   }

  });

  }

}

  我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。 所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:

... ...

public void operation() {

  obj = initObj();

  ...

  final Object o = obj;

  obj = null;

  mHandler.post(new Runnable() {

  public void run() {

  useObj(o);

  }

  }

}

... ...

 示例B:

  假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界 面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

  但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃 圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。


关于内存使用优化,我强烈推荐:[Android 性能优化系列]内存之终极篇降低你的内存消耗




0 0
原创粉丝点击