浅谈如何避免Android内存溢出

前言:

关于android的内存溢出的文章,网络上有很多这方面的资料,所以这遍文章不算是正真意义上的创新,仅仅是一篇整合类的文章。文章从7个方面介绍一些避免内存溢出需要注意的地方和方法。当然,处理内存溢出的方法远不止这些,我们最好根据自己的项目设计合适的避免OOM的方案。

(一)在构造Adapter时,没有使用缓存用的convertView

我们在构造Adapter的时候都会重写getView方法:

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

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

由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。

下面是示例代码:
修正前:

public View getView(int position, View convertView, ViewGroup parent) {
  View view = new Xxx(...);
  ... ...
  return view;
}

修正后:

public View getView(int position, View convertView, ViewGroup parent) {
  ViewHolder viewHolder = null;
  if (convertView == null) {
  viewHolder = new ViewHolder();
  LayoutInflater mInflater = LayoutInflater.from(mContext);
  convertView = mInflater.inflate(R.layout.xxx, null);
  viewHolder.xxx = convertView.findViewById(R.id.xxx);
  viewHolder.yyy = convertView.findViewById(R.id.yyy);
  ...
  convertView.setTag(viewHolder);
  } else {
  viewHolder = (ViewHolder)convertView.getTag();
  }
  .....
  .....
  return convertView;
}  
(二)资源对象没有关闭造成的内存溢出

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor,如果我们没有关闭它,系统在回收它时也会关闭它(在SQLiteCursor的析构函数finalize()中,它自己会调close()关闭),但是这样的效率太低了。而且android数据库中对Cursor资源的是有限制个数的,如果不及时close掉,会导致别的地方无法获得,所以一定要在数据库操作完成(例如在finally()或者是Activity的onDestroy()中)之后执行:

cursor.close();

还有一种情况就是我们注册了某种监听器,当某种符合条件的事件发生时,我们就利用该监听器创建一个对象,当我们不需要的时候应该及时的取消我们之前注册的监听器。否则事件的重复发生有可能会导致大量的对象被监听器创建、没有被回收而引起OutOfMemory。

(三)引用没有及时释放造成的内存溢出
(3.1)释放对象的引用

首先我们来了解一下Java中的一个基础知识:栈内存堆内存

堆内存用来存放数组和new运算符创建出来的对象。

栈内存用来存放基本数据类型和对象的引用变量。引用变量实际上保存的是数组或者对象在堆内存中的地址(也称为对象的句柄),这样就可以
在程序中使用栈的引用变量来访问堆中的数组或对象。

Java虚拟机的自动垃圾回收器来管理堆内存。而在栈内存中,当超出变量的作用域后,Java会自动释放掉该变量所分配的内存空间。

假设有如下操作:

public class DemoActivity extends Activity {
  ......
  private Handler mHandler = ...
  private Object obj;

  public void operation() {
  obj = initObj();
  mHandler.post(new Runnable() {
       public void run() {
       useObj(obj);
       }
   });
  }

}

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

  public class DemoActivity extends Activity {
  ......
  private Handler mHandler = ...
  private Object obj;

  public void operation() {
  obj = initObj();
  //采用局部变量,由于局部变量的作用域比较小,很快会被释放掉。
  final Object o = obj;
  obj = null;
  mHandler.post(new Runnable() {
      public void run() {
      useObj(o);
      }
    });
   }

  }

因此,对于Java中不再使用的资源需要尽快的释放,即设置成null,不要总是指望垃圾回收器为你工作。如果不设置成null,那么资源回收会受到一定的影响。

Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、 onDestroy()方法中需要适当的释放资源。

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

在很多情况下我们可以采用软引用或者弱引用来代替强引用的使用,下面分别介绍Java中几种引用。

(3.1.1)强引用(StrongReference)

使用强引用时,当内存空间不足,Java虚拟机宁愿抛出OutOfMemory Error错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

Object object=new Object();   
Object newObject=object;   

上面代码中第一句是在堆内存(heap)中创建新的Object对象,并通过object引用这个对象,第二句是通过object建立newObject到new Object()这个堆内存中的对象的引用,这两个引用都是强引用。只要存在对堆内存中对象的引用,gc就不会收集该对象。如果通过如下代码:

object=null;   
newObject=null;

那么gc在合适的时候(并不是马上回收)就会回收heap中的new Object()。

四种引用的强弱顺序:
堆内存中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下:

String abc=new String("abc");  //强引用
SoftReference<String> abcSoftRef=new SoftReference<String>(abc);  //软引用  
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //弱引用   
abc=null; //强引用置空   
abcSoftRef.clear();//软引用置空

上面的代码中:

第一行在heap对中创建内容为”abc”的对象,并建立abc到该对象的强引用,该对象是强可及的。第二行和第三行分别建立对heap中对象(即第一行代码创建的内容为”abc”的对象)的软引用和弱引用,此时heap中的对象仍是强可及的。第四行之后heap中对象不再是强可及的,变成软可及的。同样第五行执行之后变成弱可及的。

对于上述代码,一旦将强引用和软引用都置空,那么gc随时会回收弱引用和虚引用。

(3.1.2)软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器(gc)就不会回收它;如果JVM报告内存空间不足了,就会回收这些对象的内存。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

当gc决定要收集软引用是执行以下过程,以上面的abcSoftRef为例:
1:首先将abcSoftRef的referent设置为null,不再引用heap中的new String(“abc”)对象。referent属性是用来指向所要引用的对象。
2:将heap中的new String(“abc”)对象设置为可结束(finalizable)。
3:当heap中的new String(“abc”)对象的finalize()方法被运行而且该对象占用的内存被释放,abcSoftRef被添加到它的ReferenceQueue中。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

注:对ReferenceQueue来说,软引用和弱引用可有可无,但是虚引用必须有,参见:

Reference(T paramT, ReferenceQueue<? super T>paramReferenceQueue)  

被SoftReference指到的对象,即使没有任何直接引用,也不会被清除。一直要到JVM内存不足且没有直接引用时才会清除,SoftReference一般用来设计object-cache之用的。如此一来SoftReference不但可以把对象cache起来,也不会造成内存不足的错误 (OutOfMemoryError)。

A obj = new A();
Refenrence sr = new SoftReference(obj);

//引用时
if(sr!=null){
obj = sr.get();
}else{//如果已经被回收了
obj = new A(); 
sr = new SoftReference(obj);
}    
(3.1.3)引用队列(ReferenceQueue)

使用ReferenceQueue清除失去了软引用对象的SoftReference

作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:

ReferenceQueue  queue  =  new   ReferenceQueue();

SoftReference   ref = new   SoftReference(myObject,  queue);

那么当这个SoftReference所软引用的myOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有与该队列相关的非强可达对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReferenc的对象已经被回收。于是我们可以把这些失去软引用对象的SoftReference对象清除掉。常用的方式为:

SoftReference ref = null ;

while  ((ref = (SoftReference)queue.poll()) !=  null ) {
 // 清除 ref
}
(3.1.4)弱引用(WeakReference)

gc收集弱可及对象的执行过程和软可及一样,只是gc不会根据内存情况来决定是不是收集该对象,而是一种急切的回收方式。如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 WeakReference来记住此对象,而不是用一般的reference。 通过如下代码可以了明了的看出它的作用:

String abc=new String("abc");   
WeakReference<String> abcWeakRef = new WeakReference<String>(abc);   
abc=null;   
System.out.println("before gc: "+abcWeakRef.get());   
System.gc();   
System.out.println("after gc: "+abcWeakRef.get());

运行结果:
before gc: abc
after gc: null

  A obj = new A();

  WeakReference wr = new WeakReference(obj);

  obj = null;

  //等待一段时间,obj对象就会被垃圾回收   
  ...

  if (wr.get()==null) {
  System.out.println("obj 已经被清除了 ");
  } else {
  System.out.println("obj 尚未被清除,其信息是 "+obj.toString());   
  }   
  ...   
}   

在此例中,透过get()可以取得此Reference的所指到的对象,如果返回值为 null 的话,代表此对象已经被清除。用这种方法可以取得某个对象的信息,而且又不影响此对象的垃圾收集。

(3.1.5)虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

建立虚引用之后通过get方法返回结果始终为null,通过源代码你会发现,虚引用通常会把引用的对象写进referent,只是get方法返回结果为null。先看一下和gc交互的过程再说一下他的作用:

(1)不把referent设置为null,直接把heap中的new String(“abc”)对象设置为可结束的(finalizable)。
(2)与软引用和弱引用不同,先把PhantomRefrence对象添加到它的ReferenceQueue中。最后再释放虚可及的对象

你会发现在收集heap中的new String(“abc”)对象之前,你就可以做一些其他的事情。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。说简单一点,虚引用可以帮助我们在对象被回收之前做一些操作。

通过以下代码可以了解他的作用。

import java.lang.ref.PhantomReference;   
import java.lang.ref.Reference;   
import java.lang.ref.ReferenceQueue;   
import java.lang.reflect.Field;   

public class Test {   

public static boolean isRun = true;   

public static void main(String[] args) throws Exception {   

String abc = new String("abc");   
System.out.println(abc.getClass() + "@" + abc.hashCode());   

//初始化存放String的引用队列
final ReferenceQueue referenceQueue = new ReferenceQueue<String>();   

 new Thread() {   
    public void run() {   
    while (isRun) {   
    Object o = referenceQueue.poll();   
    if (o != null) {   

    try {   
    Field rereferent = Reference.class.getDeclaredField ("referent");   
    rereferent.setAccessible(true);   
    Object result = rereferent.get(o);   
    System.out.println("gc will collect:"+  result.getClass() + "@" + result.hashCode());   
    } catch (Exception e) {   
       e.printStackTrace();   
    }   

   }   
  }   
 }   
}.start();   

PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc,   
referenceQueue);   
abc = null;   
Thread.currentThread().sleep(3000);   
System.gc();   
Thread.currentThread().sleep(3000);   
isRun = false;   
}   
} 

结果为:
class java.lang.String@96354
gc will collect:class java.lang.String@96354

(3.2)static引起的内存溢出

static是Java中的一个关键字,当用它来修饰成员变量时,该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。
同时,如果static的方法或成员被外部使用的话,而外部的牵引对象没有对其进行释放的话那么整个static的类都不会被释放,也就造成内存泄漏。

public class ClassName {  
   private static Context mContext;  
   ...
}  

以上的代码是很危险的,如果把Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用(例如ClassName中的mContext就是保留个某个Activity的引用),因此该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的引用。但是,当由static修饰的Drawable与View连接之后,Drawable就将View设置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:

Drawable->TextView->Context

所以,最终该Context也没有得到释放,容易发生内存泄露。
如何才能有效的避免这种引用的发生呢?

(1)应该尽量避免static成员变量引用资源耗费过多的实例,比如Context、Drawable。

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

首先我们要看对象的使用周期是否在Activity周期内,如果超出,必须用Application;常见的情景包括:AsyncTask,Thread,第三方库初始化等等。
另外还有些情景,只能用Activity:比如,对话框,各种View,需要startActivity的等等。
总之,尽可能使用Application的Context。

(3)使用WeakReference代替强引用。比如可以使用WeakReference mContextRef;

(3.3)集合中的对象没有及时清理造成的内存溢出

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

(3.4)线程(内部类的使用)

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

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也不会被销毁,因此就出现了内存泄露的问题。

如果非静态内部类的方法中,有生命周期大于其所在类的,那就有问题了。比如:AsyncTask、Handler,这两个类都是方便开发者执行异步任务的,但是,这两个都跳出了Activity/Fragment的生命周期。

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

这种线程导致的内存泄露问题应该如何解决呢?

(1)将线程的内部类,改为静态内部类。

因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会一直持有其主体的引用,也就使主体不能被释放,亦即内存泄露。

静态内部类编译后和非内部类是一样的,有自己独立的类名,相对比较独立。不会悄悄引用所属类的实例,所以就不容易泄露。

(2)在线程内部采用弱引用保存Activity的Context引用。

(四)Bitmap引起的内存溢出
(4.1)Bitmap没有回收

有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,最好可以手动调用Bitmap.recycle()方法回收此对象的像素所占用的内存,然后将该Bitmap设置为null。

另外,图片还要尽量使用软引用方式,这样可以加快垃圾回收。

虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。

SoftReference<Bitmap> bitmap;
bitmap = new SoftReference<Bitmap>(pBitmap);
.....
if(bitmap != null){ 
   if(bitmap.get() != null && !bitmap.get().isRecycled()){
      bitmap.get().recycle();
      bitmap = null; 
   }
}
(4.2)Bitmap占用内存过大引起的内存溢出

Bitmap占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android分配给Bitmap的大小只有8M。

(4.2.1)从解析资源方面减少Bitmap所占用的内存

首先,尽量不用setImageBitmapsetImageResourceBitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。

因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间。

另外,decodeStream直接拿的图片来读取字节码了,不会根据机器的各种分辨率来自动适应,
使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。

同时,如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛OutOfMemory异常。

 InputStream is = this.getResources().openRawResource(R.drawable.pic1);
 BitmapFactory.Options options=new BitmapFactory.Options();
 options.inJustDecodeBounds = false;  //inJustDecodeBounds参数设为true表示不生成Bitmap,而是只测量参数
 options.inSampleSize = 10;   //width,hight的 像素 设为原来的十分一
 Bitmap btp =BitmapFactory.decodeStream(is,null,options);
 ......
 if(!bmp.isRecycle()){
     bmp.recycle()   //回收图片所占的内存
     system.gc()     //提醒系统及时回收
 }
(4.2.2)改变Bitmap的大小来减少所占用的内存

首先请看一行代码:

mImageView.setImageResource(R.drawable.my_image);

这是一行从资源文件中加载图片到ImageView的代码。通常这段代码没什么问题,但有些情况下,你需要对这段代码进行优化。例如当图片的尺寸远远大于ImageView的尺寸时,或者当你要在一个ListView或GridView中批量加载一些大小未知的图片时。实际上,以上这行代码会在运行时使用BitmapFactory.decodeStream()方法将资源图片生成一个Bitmap,然后由这个Bitmap生成一个Drawable,最后再将这个Drawable设置到ImageView。由于在过程中生成了Bitmap,因此如果你使用的图片过大,就会导致性能和内存占用的问题。

下面我们看一下如何将Bitmap缩小:

(1)获取原图片尺寸:

例如我们使用BitmapFactory.decodeResource()方法来从资源文件中读取一张图片并生成一个Bitmap。但如果使用一个BitmapFactory.Options对象,并把该对象的inJustDecodeBounds属性设置为true,decodeResource()方法就不会生成Bitmap对象,而仅仅是读取该图片的尺寸和类型信息:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myImage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;//测量出该Bitmap的长、宽、类型。

(2)根据原图尺寸和目标区域的尺寸计算出合适的Bitmap尺寸

BitmapFactory.Options类有一个参数inSampleSize,该参数为int型,他的值指示了在解析图片为Bitmap时在长宽两个方向上像素缩小的倍数。inSampleSize的默认值和最小值为1(当小于1时,解码器将该值当做1来处理),且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)。例如,当inSampleSize为2时,一个2000乘以1000的图片,将被缩小为1000乘以500,相应地,它的像素数和内存占用都被缩小为了原来的1/4:

//计算应该缩小的倍数
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//原始图片的宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

//与目标需求宽高进行比较
if (height > reqHeight || width > reqWidth) {

    final int halfHeight = height / 2;
    final int halfWidth = width / 2;

    // 在保证解析出的bitmap宽高分别大于目标尺寸宽高的前提下,取可能的inSampleSize的最大值
    while ((halfHeight / inSampleSize) > reqHeight
            && (halfWidth / inSampleSize) > reqWidth) {
        inSampleSize *= 2;
    }
}
  return inSampleSize;
}

(3)然后根据计算出的inSampleSize,利用decodeResource生成合适大小的Bitmap

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {

// 首先设置 inJustDecodeBounds=true 来获取图片尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);

// 计算 inSampleSize 的值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// 根据计算出的 inSampleSize 来解码图片生成Bitmap
options.inJustDecodeBounds = false; //false表示要生成图片了
return BitmapFactory.decodeResource(res, resId, options);
}

这里有一点要注意,就是要在第二遍decode之前把inJustDecodeBounds设置回false。

现在我们可以调用以上的decodeSampledBitmapFromResource方法,使用自定尺寸的Bitmap。

如果你要将一张大图设置为一个100*100的缩略图,执行以下代码:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

到此,使用decodeResource()方法将一个大图解析为小尺寸bitmap的应用就完成了。同理,还可以使用decodeStream(),decodeFile()等方法做相同的事,原理是一样的。

(五)onLowMemory()与onTrimMemory()的研究

应用内存优化之OnLowMemory&OnTrimMemory

从OnTrimMemory角度谈Android代码内存优化

OnLowMemory和OnTrimMemory的比较

(1)在引入OnTrimMemory之前都是使用OnLowMemory回调,需要知道的是,OnLowMemory大概和 OnTrimMemory中的TRIM_MEMORY_COMPLETE级别相同,如果你想兼容api<14的机器,那么可以用 OnLowMemory来实现,否则你可以忽略OnLowMemory,直接使用OnTrimMemory即可.

(2)OnLowMemory被回调时,已经没有后台进程;而onTrimMemory被回调时,还有后台进程。

(3)OnLowMemory是在最后一个后台进程被杀时调用,一般情况是low memory killer杀进程后触发;而OnTrimMemory的触发更频繁,每次计算进程优先级时,只要满足条件,都会触发。

(4)通过一键清理后,OnLowMemory不会被触发,而OnTrimMemory会被触发一次。

(六)手动设置Android堆内存的大小和优化Dalvik虚拟机的内存

对于Android平台来说,其托管层使用的Dalvik Java VM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法:
private final static float TARGET_HEAP_UTILIZATION = 0.75f;在程序onCreate时就可以调用VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 即可。

Android堆内存也可自己定义大小

对于一些Android项目,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:

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

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

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

Android堆内存也可以自己定义大小和优化Dalvik虚拟机的内存

注意:若使用这种方法:project build target 只能选择 <= 2.2 版本,否则编译将通不过。所以不建议用这种方式。

其实对于更改app的heapsize大小。个人觉得这也不可取。这根本不是从本质上解决问题,因为OOM可能是你某处导致的内存泄露,这样做只是推迟了OOM发生,一定程度上降低了OOM发生率(因为可能在还没有达到heapsize最大值的时候就发生了GC)。android系统他有默认的heapsize的值,至于我们重新设置一个更大的会对整个系统或者其他应用造成什么样的影响其实都是不可知的,所以还是用默认的。

(七)其他避免OOM的小细节
(7.1)使用ArrayMap/SparseArray代替HashMap

这里有几篇介绍ArrayMap和SparseArray的文章:

SparseArray基本介绍(一)

ArrayMap基本介绍(一)

ArrayMap和SparseArray基本介绍(二)

Android性能优化典范(三)的第一个知识点介绍ArrayMap

(7.2)避免在Android里面使用Enum

Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考Android性能优化典范(三),所以请避免在Android里面使用到枚举。

(7.3)StringBuilder

在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

(7.4)资源文件需要选择合适的文件夹进行存放

我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。

(7.5)尽量避免内存抖动

Memory Churn(内存抖动),内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

比较常见的导致内存抖动的原因就是在for循环中不停的new对象。

(7.6)LeakCanary

最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况,更多关于LeakCanary的介绍,请看这里
(中文使用说明)

最后附上Android性能优化典范系列的四篇文章:
Android性能优化典范(一)
Android性能优化典范(二)
Android性能优化典范(三)
Android性能优化典范(四)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值