Android 内存泄露:OOM全面分析、内存分析

本文详细探讨了Android开发中常见的内存泄露原因,包括static修饰的引用、线程Thread和AsyncTask、Bitmap处理不当、Handler使用误区、Cursor管理、Adapter构造及Universal-Image-Loader的使用注意事项。通过分析内存分析工具DDMS的使用方法,帮助开发者诊断和解决内存溢出问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         在Android实际开发中,程序员们经常会遇到内存泄露(Out Of Memory),说白了,就是内存不够用了。导致应用程序运行很卡,甚至直接闪退。为什么会出现内存溢出呢?因为在Android手机中,每个应用一般默认只能使用16M内存(当然这个16M不是固定的,一般是由手机商家设定),如果超过这个限制就是出现很快或者闪退。

       在Android中几种常的OOM包括以下几种:

       一、static修饰词引起的内存泄露

static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。 
public class ClassName {      
    private static Context mContext;     
}  
以上的代码是很危险的,如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放. 
如何才能有效的避免这种引用的发生呢? 
    第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。 
    第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。 
    第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef; 

这里说的弱引用(WeakReference)、软引用(softReference)以及强引用?我们先来简单比较下:

强引用:String str="helloworld";就是我们经常用的,当内存不足时,不会回收这个对象。

弱引用 :ObjectA a = newObjectA();  

ObjectB b = new ObjectB(a);

a = null;

在上面我们先初始化了一个ObjectA对象,再初始化了一ObjectB,需要注意的是,在初始化ObjectB,需要传递一个ObjectA对象,注意这样ObjectB就对ObjectA有一个引用。当ObjectA对象的引用a置空了,a不再指向对象ObjectA的地址(a = null) 时,我们都知道当一个对象不再被其他对象引用的时候,是会被GC回收的。在这样很显然是ObjectA是不对被回收的,因为ObjectB依然引用ObjectA,这样就造成了内存泄露。

我们稍微改动下

ObjectA a = new ObjectA();

       WeakReference wr = new WeakReference(a);//弱引用,对一些不需要长期引用的对象,可以用这个,当内存不足的时候,gc会回收这个对象

     //ObjectB b = new ObjectB(wr.get());

    if(wr.get==null){

       //对象ObjectA被回收了

}

使用WeakReference 、softReference可以有效保证在内存不足的时候,引用的对象会被gc回收。值得注意的是:当程序中需要一直保持的对象不能使用弱引用或软引用。

二、线程Thread、AsyncTask引起的内存泄露

private class MyThread extends Thread{         
@Override         
  public void run() {             
  super.run();             
  //do somthing         
}     

new AsyncTask<Void, Void, UserModel>() {
@Override
protected UserModel doInBackground(Void... params) {
do somthing  
}

相信这两个线程大家在熟悉不过了,但是注意是错的,很容易造成内存泄露。假设在线程中需要处理很耗时的操作

将设备的竖屏变为了横屏,当屏幕转换时会重新创建Activity(Activity应该会被销毁并重新初始化,执行oncreate方法),然而事实上并非如此。
由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

如何避免线程造成的内存泄露呢?

将线程变成静态的内部类,同事,在线程内部采用弱引用当前Context。 如下

class MainActivity extends Activity{

private Textview tv;

.......

private static class MyThread extends Thread {  
                WeakReference<MainActivity> mThreadActivityRef;  
          
                public MyThread(MainActivityactivity) {  
                    mThreadActivityRef = new WeakReference<MainActivity>(  
                            activity);  
                }  
          
                @Override  
                public void run() {  
                    super.run();  
                    if (mThreadActivityRef == null)  
                        return;  
                    if (mThreadActivityRef.get() != null)  
                      mThreadActivityRef.get(). tv.setText("sssss");// 不能将Textview 变成static ,上面已经说过了
                }  
            }  

}

三、bitmap 引起的内存泄露:

在使用bitmap要注意两点

 第一、及时的销毁。 
    虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。 
    第二、设置一定的采样率。 
   
有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码: 
private ImageView preview;  
BitmapFactory.Options options = new BitmapFactory.Options();  
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一  
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
 preview.setImageBitmap(bitmap);

四、Handler 引起的内存泄露

handler 大家肯定不会陌生,经常来用来更新主UI,但是用的不好,同样会造成内存溢出。比如Handler.sendEmptyMessageDelayed 的时候,由于该动作不会立即执行,此时如果activity被关闭或者横竖屏切换的时候,handler里面有对activity的引用,所以activity不会被内存回收,造成内存溢出。

正确的做法是在ondestory方法中调用 Handler.removeCallbacksAndMessages(null);在这里顺便说一下,Handler.sendEmptyMessage(0);不断发空消息,可以实现循环的效果,效率比for循环、while循环高。

五、Cursor引起的内存泄露

Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。 
    然而如果Cursor的数据量特表大,特别是如果里面有Blob信息时,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉 。

六、构造Adapter时,没有使用缓存的 convertView 而造成内存泄露:

 以构造ListView的BaseAdapter为例,在BaseAdapter中提高了方法:初始时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对象的过程可以查看: 
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vh = null;
if (convertView == null) {
vh = new ViewHolder();
convertView = inflater.inflate(R.layout.mysearchtuijian, null);
vh.tv = (TextView) convertView.findViewById(R.id.mtext);
convertView.setTag(vh);
} else {
vh = (ViewHolder) convertView.getTag();
}

public class ViewHolder {
public TextView tv;
}
//内部类

}

七、universal-image-loader 造成的内存泄露:

universal-image-loader是用于图片异步加载,使用universal-image-loader要注意几点避免内存溢出:

1、threadPoolSize 配置线程池的大小:1~5 建议用3

2、禁用在内存中缓存 cacheInMemory()

3、在显示选项中使用 .bitmapConfig(Bitmap.Config.RGB_565) . RGB_565模式消耗的内存比ARGB_8888模式少两倍.

4、在显示选项中使用.imageScaleType(ImageScaleType.EXACTLY) 或 .imageScaleType(ImageScaleType.IN_SAMPLE_INT)

5、避免使用 RoundedBitmapDisplayer. 调用的时候它使用ARGB-8888模式创建了一个新的Bitmap对象来显示,对于内存缓存模式 (ImageLoaderConfiguration.memoryCache(...)) 你可以使用已经实现好的方法.

以上7个都是比较常见的。在实际开发中,遇到OOM了,要学会借助工具去分析。

内存分析工具 DDMS --> Heap 

一、打开DDMS,选择你的项目名称,点击 uodate Heap,在点击 Cause GC:当于向虚拟机请求了一次gc操作。

Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,一般情况下Total Size的值2.2~2.8之间,而当其值超过3.55后进程就会被kill。 

运行程序,观察Total Size的值,发生内存泄露,Total Size的值越来越大时,按下“Dump HPROF file”按钮,如下

这个时候会提示设置hprof文件的保存路径。

二、转化hprof文件

刚才导出的文件需要用工具转化下。使用AndrodiSDK/tools/hprof-conv转化hprof文件, 

首先,要通过控制台进入到你的 android sdk tools 目录下。这里out.hprof文件和tools在同一目录下

cmd:hprof-conv out.hprof     out777.hprof

三、打开MAT工具(这个工具要先下载,网上有)。

打开转换后的 hprof 文件,就能看到完整的内存使用分析报告了。

点击左侧的Histogram,在输入你的包名进行过滤

这里我们看到DiQuModel在内存中135个,右键单击,选择Merge Shortest Paths to GC Root ------> exclud all phantom/weak/soft etc .referencer 可以看到内存中这个model的引用情况

得到这些后,在进行分析。 

另外:

   private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;

        VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //设置最小heap内存为6MB大小。

当然对于内存吃紧来说还可以通过手动干涉GC去处理

     注意了,这个设置dalvik虚拟机的配置的方法对Android4.0 设置无效。

     这就是我对Android的oom的一点看法,希望能帮到你。




.



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值