Android的内存泄漏和调试

孤者浪人 提交于 2019-12-02 08:54:36

Android的内存泄漏和调试 

一、 Android的内存机制 
Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似。程序员通过new为对象分配内存,所有对象在java堆内分配空间;然而对象的释放是由垃圾回收器来完成的. 
那么GC怎么能够确认某一个对象是不是已经被废弃了呢?Java采用了有向图的原理。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,该图就是从起始顶点开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。 

二、Android的内存溢出 
Android的内存溢出是如何发生的? 
Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。因此我们所能利用的内存空间是有限的。如果我们的内存占用超过了一定的水平就会出现OutOfMemory的错误。 
为什么会出现内存不够用的情况呢?我想原因主要有两个: 
由于我们程序的失误,长期保持某些资源(如Context)的引用,造成内存泄露,资源造成得不到释放。 
保存了多个耗用内存过大的对象(如Bitmap),造成内存超出限制。 

三、常见的内存泄漏 
1.万恶的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; 

2.线程惹的祸 
线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。 
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         
}     

}  
    这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。 
    由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。 

这种线程导致的内存泄露问题应该如何解决呢? 
    第一、将线程的内部类,改为静态内部类。 
    第二、在线程内部采用弱引用保存Context引用。 
    
    另外,我们都知道Hanlder是线程与Activity通信的桥梁,我们在开发好多应用中会用到线程,有些人处理不当,会导致当程序结束时,线程并没有被销毁,而是一直在后台运行着,当我们重新启动应用时,又会重新启动一个线程,周而复始,你启动应用次数越多,开启的线程数就越多,你的机器就会变得越慢。 
package com.tutor.thread;  
import android.app.Activity;  
import android.os.Bundle;  
import android.os.Handler;  
import android.util.Log;  
public class ThreadDemo extends Activity {  
    private static final String TAG = "ThreadDemo";  
    private int count = 0;  
    private Handler mHandler =  new Handler();  
      
    private Runnable mRunnable = new Runnable() {  
          
        public void run() {  
            //为了方便 查看,我们用Log打印出来   
            Log.e(TAG, Thread.currentThread().getName() + " " +count);  
            count++;  
            setTitle("" +count);  
            //每2秒执行一次   
            mHandler.postDelayed(mRunnable, 2000);  
        }  
          
    };  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.main);   
        //通过Handler启动线程   
        mHandler.post(mRunnable);  
    }  
      

所以我们在应用退出时,要将线程销毁,我们只要在Activity中的,onDestory()方法处理一下就OK了,如下代码所示: 
@Override  
  protected void onDestroy() {  
    mHandler.removeCallbacks(mRunnable);  
    super.onDestroy();  
  } 

3.超级大胖子Bitmap 
可以说出现OutOfMemory问题的绝大多数人,都是因为Bitmap的问题。因为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); 

第三、巧妙的运用软引用(SoftRefrence) 
    有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放。 
   

4.行踪诡异的Cursor 
    Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。 
    然而如果Cursor的数据量特表大,特别是如果里面有Blob信息时,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉 
    
5.构造Adapter时,没有使用缓存的 convertView 
描述: 
  以构造ListView的BaseAdapter为例,在BaseAdapter中提高了方法: 
public View getView(int position, View convertView, ViewGroup parent) 
来向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 --> void addScrapView(View scrap) 方法。 
示例代码: 
public View getView(int position, View convertView, ViewGroup parent) { 
  View view = new Xxx(...); 
  ... ... 
  return view; 

修正示例代码: 
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; 


小结: 
static:引用了大对象如context 
线程:切屏时Activity因为线程引用而没有如期被销毁;handler有关,Activity意外终止但线程还在 
Bitmap:要及时recycle,降低采样率 
Cursor:要及时关闭 
Adapter:没有使用缓存的convertView 

四、内存泄漏调试: 
(1).内存监测工具 DDMS --> Heap 
无论怎么小心,想完全避免bad code是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。Android tools中的DDMS就带有一个很不错的内存监测工具Heap(这里我使用eclipse的ADT插件,并以真机为例,在模拟器中的情况类似)。用 Heap监测应用进程使用内存情况的步骤如下: 
1. 启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的; 
2. 将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“Mass Storage”; 
3. 链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息; 
4. 点击选中想要监测的进程,比如system_process进程; 
5. 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标; 
6. 点击Heap视图中的“Cause GC”按钮; 
7. 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。 
说明: 
a) 点击“Cause GC”按钮相当于向虚拟机请求了一次gc操作; 
b) 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化; 
c) 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。 
  如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断: 
a) 不断的操作当前应用,同时注意观察data object的Total Size值; 
b) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平; 
c) 反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大, 
  直到到达一个上限后导致进程被kill掉。 
d) 此处已system_process进程为例,在我的测试环境中system_process进程所占用的内存的data object的Total Size正常情况下会稳定在2.2~2.8之间,而当其值超过3.55后进程就会被kill。 
  
  总之,使用DDMS的Heap视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性。 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!