Android 内存浅析

来源:互联网 发布:centos怎么用 编辑:程序博客网 时间:2024/05/17 01:11

一、 Android的内存机制

    Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似。程序员通过new为对象分配内存,所有对象在java堆内分配空间;然而对象的释放是由垃圾回收器来完成的。C/C++中的内存机制是“谁污染,谁治理”,java的就比较人性化了,给我们请了一个专门的清洁工(GC)。

二、GC是什么? 为什么要有GC?    
    GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

三、垃圾回收的优点和原理。并考虑2种回收机制。 
    Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。

四、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收? 
    对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
 
五、Android系统中GC什么情况下会出现内存泄露呢?  视频编解码/内存泄露 
    导致内存泄漏主要的原因是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,那么这些对象就会驻留内存,消耗内存,因为无法让垃圾回收器GC验证这些对象是否不再需要。如果存在对象的引用,这个对象就被定义为"有效的活动",同时不会被释放。要确定对象所占内存将被回收,我们就要务必确认该对象不再会被使用。典型的做法就是把对象数据成员设为null或者从集合中移除该对象。但当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

Java带垃圾回收的机制,为什么还会内存泄露呢?举例:

[java] view plaincopyprint?
  1. Vector v = new Vector(10);  
  2.         for (int i = 1; i < 100; i++) {  
  3.             Object o = new Object();  
  4.             v.add(o);  
  5.             o = null;  
  6.         }// 此时,所有的Object对象都没有被释放,因为变量v引用这些对象。  
Java 内存泄露的根本原因就是 保存了不可能再被访问的变量类型的引用

六、Android的内存溢出
        Android的内存溢出是如何发生的?
        Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。也就是说我们所能利用的内存空间是有限的。如果我们的内存占用超过了一定的水平就会出现OutOfMemory的错误。
为什么会出现内存不够用的情况呢?我想原因主要有两个:

由于我们程序的失误,长期保持某些资源(如Context)的引用,造成内存泄露,资源造成得不到释放。保存了多个耗用内存过大的对象(如Bitmap),造成内存超出限制。

继续,这张说说一些android中泄漏和溢出的细节点:

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

[java] view plaincopyprint?
  1. public class Example {    
  2.      private static Context mContext;    
  3.      //省略    
  4. }  
以上的代码一般看来似乎没什么问题。但如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放。

还没完呢,再看看下边的:

[java] view plaincopyprint?
  1. private static Drawable sBackground;    
  2.       
  3.     public void example (){  
  4.         ImageView iv = new ImageView(this);  
  5.         iv.setBackgroundDrawable(sBackground);  
  6.     }  

 我发现有很多哥们都这么写,同样看起来没什么问题,但我们来剖析一下:sBackground, 是一个静态的变量,但是我们发现,我们并没有显式的保存Contex的引用,但是,因为Imageview是View的子类,当Drawable与View连接之后,Drawable就将View设置为一个回调:

[java] view plaincopyprint?
  1. public class View implements Drawable.Callback  
  2.   
  3. /** 
  4.      * The application environment this view lives in. 
  5.      * This field should be made private, so it is hidden from the SDK. 
  6.      * {@hide} 
  7.      */  
  8.     protected Context mContext;  

由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。所以最终的引用链如下:
    Drawable->ImageView->Context

    所以,最终该Context也没有得到释放,积累就发生了内存泄露。
    如何才能有效的避免这种引用的发生呢?
    第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
    第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长(具体多次不作详细解释,各位去看文档)。
    第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef(一位前辈提起的,具体不是很清楚,也在琢磨中


二、泄漏根源之线程
    线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。先看段代码。

[java] view plaincopyprint?
  1. new Thread(new Runnable() {  
  2.             public void run() {  
  3.                 // TODO do something  
  4.             }  
  5.         }).start();  

   很平常的一段代码是吧,也是咱经常这么搞的。假设问题:假设run函数是一个很费时的操作(通常大数据的加载),在加载过程中(前提线程并没有结束),将设备的竖屏变为了横屏,一般情况下当屏幕转换时会重新创建Activity(当然你也可以设置不创建新的Activity,在此以创建新的为论点),按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。


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

有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。
    这种线程导致的内存泄露问题应该如何解决呢(引用前辈的话)?
    第一、将线程的内部类,改为静态内部类。
    第二、在线程内部采用弱引用保存Context引用。

[java] view plaincopyprint?
  1. public class ExampleAsyncTask<Params, Progress, Result, WeakTarget> extends  
  2.             AsyncTask<Params, Progress, Result> {  
  3.           
  4.         @Override  
  5.         protected final Result doInBackground(Params... params) {  
  6.             //TODO: do something  
  7.             return null;  
  8.         }  
  9.     }  

小结:事实上,线程的问题并不仅仅在于内存泄露,还会带来一些灾难性的问题。例如:多线程并发安全(推荐下载张孝祥的多线程并发视频,十分详细)等等

内存溢出之Bitmap 
    可以说出现OutOfMemory问题的绝大多数人,都是因为Bitmap的问题。因为Bitmap占用的内存实在是太多了,它是一个“超级大胖子”,特别是分辨率大的图片,如果要显示多张那问题就更显著了。
    如何解决Bitmap带给我们的内存问题?
    第一、及时销毁。

    虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。所以Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才它设置为null. 虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试结果显示它并没能立即释放内存。

[java] view plaincopyprint?
  1. if(!bitmap.isRecycled()){  
  2.             bitmap.recycle();  
  3.             bitmap = null;  
  4.         }  

    第二、设置一定的采样率。

    手机就那么屁大的屏幕,所以有时候我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:

[java] view plaincopyprint?
  1. public ImageView iv ;  
  2.       
  3.     public void example() {  
  4.         BitmapFactory.Options options = new BitmapFactory.Options();  
  5.         options.inSampleSize = 2;// 图片宽高都为原来的二分之一,即图片为原来的四分之一。数值越大图片越模糊.你懂的  
  6.         //Uri u = Uri.parse( "content://media/internal/audio/media/81" );   
  7.         // Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(Uri.parse(uriString)), null, options);  
  8.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(),  
  9.                 R.drawable.ic_launcher, options);  
  10.         iv.setImageBitmap(bitmap);  
  11.     }  

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

内存溢出之Adapter
        构造 Adapter时,没有使用缓存的 convertView 
        以构造ListView的 BaseAdapter 为例,在BaseAdapter 中提共了方public View getView(int position, View convertView, ViewGroup pare来向ListView提供每一个item所需要的view对象。初始时ListBaseAdapter 中根据当前的屏幕布局实例化一定数量的view对象,同些view对象缓存起来。当向上滚动ListView时,原先位于最上面的象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程方法完成的,getView()的第二个形参 View convertView就是被缓存起view对象(初始化时缓存中没有view对象则convertView是null)。
        由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。构造 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)。 

所以综合以上的理论,写了一个小例子,表达一下:

[java] view plaincopyprint?
  1. /** 
  2.      *  
  3.      ****************************************** 
  4.      * @author 老牛比 
  5.      * 文件名称 :  ExampleAdapter.java 
  6.      * 创建时间 : 2012-10-1 下午11:11:28 
  7.      * 文件描述 : 优化小例子 
  8.      ****************************************** 
  9.      */  
  10.     public class ExampleAdapter extends BaseAdapter {  
  11.   
  12.         private ArrayList<SoftReference<Bitmap>> mBitmaps = new ArrayList<SoftReference<Bitmap>>();  
  13.         private ArrayList<Object> mValues;  
  14.         private LayoutInflater mInflater;  
  15.   
  16.         public ExampleAdapter(ArrayList<SoftReference<Bitmap>> mBitmaps,  
  17.                 ArrayList<Object> mValues, Context mContext,  
  18.                 LayoutInflater mInflater) {  
  19.             super();  
  20.             this.mValues = mValues;  
  21.             this.mInflater = (LayoutInflater) mContext  
  22.                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  23.         }  
  24.   
  25.         public int getCount() {  
  26.             return mValues.size();  
  27.         }  
  28.   
  29.         public Object getItem(int position) {  
  30.             return mValues.get(position);  
  31.         }  
  32.   
  33.         public long getItemId(int position) {  
  34.             return position;  
  35.         }  
  36.   
  37.         public View getView(int position, View convertView, ViewGroup parent) {  
  38.             ViewHolder holder;  
  39.             Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(position).XXX);  
  40.             mBitmaps.add(new SoftReference<Bitmap>(bitmap)); // 此处加入ArrayList  
  41.             if (convertView == null) {  
  42.                 convertView = mInflater.inflate(R.layout.example, false);  
  43.                 holder = new ViewHolder();  
  44.                 holder.text = (TextView) convertView.findViewById(R.id.text);  
  45.                 holder.icon = (ImageView) convertView.findViewById(R.id.icon);  
  46.                 convertView.setTag(holder);  
  47.             } else {  
  48.                 holder = (ViewHolder) convertView.getTag();  
  49.             }  
  50.   
  51.             holder.text.setText("xxx");  
  52.             holder.icon.setImageBitmap(bitmap);  
  53.             return convertView;  
  54.         }  
  55.   
  56.         /** 
  57.          *  
  58.          ******************************************  
  59.          * @author 老牛比 
  60.          * 文件名称 : ViewHolder.java  
  61.          * 创建时间 : 2012-10-1 下午10:35:25  
  62.          * 文件描述 : 使用 ViewHolder 模式, 效率再提高 50% 
  63.          ******************************************  
  64.          */  
  65.         class ViewHolder {  
  66.             TextView text;  
  67.             ImageView icon;  
  68.         }  
  69.     }  

内存溢出之Cursor
    Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,操作小数据查询时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。 然而如果Cursor的数据量特别大,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android 文档中提倡开发者手动的关闭Cursor。

    所以我们最好这样使用Cursor: 

[java] view plaincopyprint?
  1. public void example() {  
  2.         Cursor mCursor = null;  
  3.         try {  
  4.             mCursor = mContext.getContentResolver().query(uri, nullnull,  
  5.                     nullnull);  
  6.             if (mCursor != null) {  
  7.                 mCursor.moveToFirst();  
  8.                 // TODO:do something  
  9.             }  
  10.         } catch (Exception e) {  
  11.             e.printStackTrace();  
  12.         } finally {  
  13.             if (mCursor != null) {  
  14.                 mCursor.close();  
  15.             }  
  16.         }  
  17.     }  

但在CursorAdapter中应用的情况下,必须注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭:

[java] view plaincopyprint?
  1. @Override    
  2.     protected void onDestroy() {          
  3.         if (mAdapter != null && mAdapter.getCurosr() != null) {    
  4.             mAdapter.getCursor().close();    
  5.         }    
  6.         super.onDestroy();     
  7.     }   


转自http://blog.csdn.net/lnb333666/article/details/8031770

原创粉丝点击