Android内存优化

jopen 12年前

   在前公司做一个图片处理的应用时, 项目交付的时候,客户的手机在运行应用的时候,一直在崩溃,而这个异常就是OutOfMemory的错误,简称为OOM, 搞得我们也是极其的崩溃,最后 ,我们是通过网上搜集资料和代码走查的方式来优化解决的,这里,我就把我们收集到资料和总结的经验分享下吧。
    Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。我们平常看到的OutOfMemory的错误,通常是堆内存溢出。移动开发和web开发的最大的区别是设备资源受限,对一般手机应用,这个资源是相当有限的,堆内存的上限值只有16M。Android的缺省值是16M(某些机型是24M),而对于普通应用这是不能改的,当应用程序处理大资源的资源,如图片或视频等媒体资源时 ,数量一多,时间一长,这个16M是很容易耗尽的,OOM是很容易出现的。
   *Android内存泄露*
   虽然JAVA有垃圾回收机制,但也存在内存泄露。如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成了内存泄露。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了。当然java的,内存泄漏和C/C++是不一样的。如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不会影响整个系统的。C/C++的内存泄露就比较糟糕了,它的内存泄露是系统级,即使该C/C++程序退出,它的泄露的内存也无法被系统回收,永远不可用了,除非重启机器。
  Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启),这是,我们的应用程序就会崩溃,我们就会看到OOM。
一般而言,android中常见的原因主要有以下几个:
1.数据库的cursor没有关闭。
2.构造adapter没有使用缓存contentview。
3.调用registerReceiver()后未调用unregisterReceiver().
4.未关闭InputStream/OutputStream。
5.Bitmap使用后未调用recycle()。
6.Context泄漏。
7.static关键字等。
下面我们就来逐一说明这些吧:
*1、首先,我们先来说明static,这个是万恶之源*
   static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。   不少程序员喜欢用static这个关键字修饰变量,因为他使得变量的生命周期大大延长啦,并且访问的时候,也极其的方便,用类名就能直接访问,各个资源间传值也极其的方便,所以,它经常被我们使用。但如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。


 public class ClassName {           private static Context mContext;           //省略     }   


以上的代码是很危险的,如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放,并且,如果该activity里面再持有一些资源,那就糟糕了。
    上面是直接的引用泄露,我们再看google文档中的一个例子。
 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的引用,但是,当Drawable与View连接之后,Drawable就将View设置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:
    Drawable->TextView->Context
所以,最终该Context也没有得到释放,也发生了内存泄露。
那我们如何的避免这种泄露的发生呢?
   第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
   第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
   第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;
该部分的详细内容也可以参考Android文档中Article部分。
2、 Context泄漏
   第一条说的static泄露中,已经概括了大部分的context泄露,出了这种static的泄露context的方式外,还有一种就是内部类持有外部对象造成的内存泄露,常见是内部线程造成的。
   public class BasicActivity extends Activity {          @Override          public void onCreate(Bundle savedInstanceState) {              super.onCreate(savedInstanceState);              setContentView(R.layout.main);              new MyThread().start();          }              private class OneThread extends Thread{              @Override              public void run() {                  super.run();                  //do somthing              }          }       }         

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

    由于我们的线程是Activity的内部类,所以OneThread中保存了Activity的一个引用,当OneThread的run函数没有结束时,OneThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
   有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题,故一般不建议将AsyncTask作为内部类使用。
   那么上述内存泄露问题应该如何解决呢?
   第一、将线程的内部类,改为静态内部类。并且注意第二条。
   第二、在线程内部采用弱引用保存Context引用。
3、bitmap内存泄露
   可以说出现OutOfMemory问题的绝大多数人,都是因为Bitmap的问题。因为Bitmap占用的内存实在是太多了,它是一个“超级大胖子”,特别是分辨率大的图片,如果要显示多张那问题就更显著了。
   如何解决Bitmap带给我们的内存问题?
   第一、及时的销毁。
虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”,  还有就是, 虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试表明它并没能立即释放内存。故我们还需手动设置为NULL这样还能大大的加速Bitmap的主要内存的释放。。
如下:

  if(!bitmap.isRecycled()){               bitmap.recycle()     }  
       

   第二、设置一定的采样率。
   有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:
    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在内存快不足时得到有效的释放。如下例:   private class MyAdapter extends BaseAdapter {              private ArrayList> mBitmapRefs = new ArrayList>();         public View getView(int i, View view, ViewGroup viewGroup) {             View newView = null;             if(view != null) {                 newView = view;             } else {                 newView =(View)mInflater.inflate(R.layout.image_view, false);             }                  Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName);             mBitmapRefs.add(new SoftReference(bitmap));     //此处加入ArrayList             ((ImageView)newView).setImageBitmap(bitmap);                  return newView;         }     }   

   开源社区上有一个SoftHashMap工具类,就很好的采用了这种思想,所有,我们可以采用该容器来保存这些大内存资源。
4.未关闭InputStream/OutputStream 
   这个就不多说了,我们操作完输入输出流都要关闭流
5、调用registerReceiver()后未调用unregisterReceiver().
    广播接收者(BroadcastReceiver)经常在应用中用到,可以在多线程任务完成后发送广播通知UI更新,也可以接收系统广播实现一些功能 
    可以通过代码的方式注册: 
    IntentFilter postFilter = new IntentFilter();        postFilter.addAction(getPackageName() + ".background.job");        this.registerReceiver(receiver, postFilter);  


    当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册 
    也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现,通常我们可以重写Activity的onDestory().
6、 构造adapter没有使用缓存contentview
   当一个listview的子项有成千上万个时,如果我们没有采用一定的策略来重用这些资源,那应用的那点对内存,是远远不够使用的。
   在继承BaseAdapter时会让我们重写getView(int position, View   convertView, ViewGroup parent)方法, 
   第二个参数convertView就是我们要用到的重用的对象。
   这里只讲使用方法,具体性能测试文章请见: 
    ListView中getView的原理+如何在ListView中放置多个item 
    http://www.cnblogs.com/xiaowenji/archive/2010/12/08/1900579.html&nbsp;
    Android开发之ListView适配器(Adapter)优化 
    http://shinfocom.iteye.com/blog/1231511
7、数据库的cursor没有关闭
   Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。
   然而如果Cursor的数据量特表大,特别是如果里面有Blob信息时,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。
   所以我们使用Cursor的方式一般如下:

  Cursor cursor = null;       try {         cursor = mContext.getContentResolver().query(uri,null, null,null,null);         if(cursor != null) {             cursor.moveToFirst();             //do something         }       } catch (Exception e) {         e.printStackTrace();         } finally {         if (cursor != null) {            cursor.close();         }      }   


   有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。
@Override     protected void onDestroy() {              if (mAdapter != null && mAdapter.getCurosr() != null) {            mAdapter.getCursor().close();        }        super.onDestroy();      }   

   CursorAdapter中的changeCursor函数,会将原来的Cursor释放掉,并替换为新的Cursor,所以你不用担心原来的Cursor没有被关闭。
   你可能会想到使用Activity的managedQuery来生成Cursor,这样Cursor就会与Acitivity的生命周期一致了,多么完美的解决方法!然而事实上managedQuery也有很大的局限性。
   managedQuery生成的Cursor必须确保不会被替换,因为可能很多程序事实上查询条件都是不确定的,因此我们经常会用新查询的Cursor来替换掉原先的Cursor。因此这种方法适用范围也是很小。
总结
   要减小内存的使用,其实还有很多方法和要求。比如不要使用整张整张的图,尽量使用9path图片。Adapter要使用convertView等等,好多细节都可以节省内存。这些都需要我们去挖掘,谁叫Android的内存不给力来着。其实,最后说一句,最最重要的就是:正确使用,规范编程。