天天看點

Android大圖檔處理一、在記憶體引用上做些處理,常用的有軟引用、強化引用、弱引用二、在記憶體中加載圖檔時直接在記憶體中做處理,如:邊界壓縮三、動态回收記憶體,對象銷毀四、優化Dalvik虛拟機的堆記憶體配置設定五、自定義堆記憶體大小六、Bitmap 設定圖檔尺寸,避免記憶體溢出的優化方法七、使用圖檔緩存技術八、終極解決:高清加載巨圖方案 拒絕壓縮圖檔

移動裝置對記憶體的要求還是很苛刻的,即便現在主流旗艦機動辄3G、4G的記憶體,但是對應于每個App分得的容量來說還是有限的,我們程式猿可以用各種手段來增大單個App的需求量,但是并不是完美的解決方案。最好是想辦法來提高App的性能。圖檔來說更是OOM (OutOfMemoryError)的常見引發者,比如說系統圖檔庫裡展示的圖檔大都是用手機攝像頭拍出來的,這些圖檔的分辨率會比我們手機螢幕的分辨率高得多。大家應該知道,我們編寫的應用程式都是有一定記憶體限制的,程式占用了過高的記憶體就容易出現OOM(OutOfMemory)異常。接下來針對圖檔處理常見的手段一起看!

一、在記憶體引用上做些處理,常用的有軟引用、強化引用、弱引用

在你應用程式的UI界面加載一張圖檔是一件很簡單的事情,但是當你需要在界面上加載一大堆圖檔的時候,情況就變得複雜起來。在很多情況下,(比如使用ListView, GridView 或者 ViewPager 這樣的元件),螢幕上顯示的圖檔可以通過滑動螢幕等事件不斷地增加,最終導緻OOM。

為了保證記憶體的使用始終維持在一個合理的範圍,通常會把被移除螢幕的圖檔進行回收處理。此時垃圾回收器也會認為你不再持有這些圖檔的引用,進而對這些圖檔進行GC操作。用這種思路來解決問題是非常好的,可是為了能讓程式快速運作,在界面上迅速地加載圖檔,你又必須要考慮到某些圖檔被回收之後,使用者又将它重新滑入螢幕這種情況。這時重新去加載一遍剛剛加載過的圖檔無疑是性能的瓶頸,你需要想辦法去避免這個情況的發生。

這個時候,使用記憶體緩存技術可以很好的解決這個問題,它可以讓元件快速地重新加載和處理圖檔。下面我們就來看一看如何使用記憶體緩存技術來對圖檔進行緩存,進而讓你的應用程式在加載很多圖檔的時候可以提高響應速度和流暢性。

記憶體緩存技術對那些大量占用應用程式寶貴記憶體的圖檔提供了快速通路的方法。其中最核心的類是LruCache (此類在android-support-v4的包中提供) 。這個類非常适合用來緩存圖檔,它的主要算法原理是把最近使用的對象用強引用存儲在 LinkedHashMap 中,并且把最近最少使用的對象在緩存值達到預設定值之前從記憶體中移除。

軟引用(SoftReference)、虛引用(PhantomRefrence)、弱引用(WeakReference),這三個類是對heap中java對象的應用,通過這個三個類可以和gc做簡單的互動,除了這三個以外還有一個是最常用的強引用。

1.1:強引用,例如下面代碼:

Object o=new Object();       
Object o1=o;   
           

上面代碼中第一句是在heap堆中建立新的Object對象通過o引用這個對象,第二句是通過o建立o1到new Object()這個heap堆中的對象的引用,這兩個引用都是強引用。隻要存在對heap中對象的引用,gc就不會收集該對象。如果通過如下代碼:

o=null;       
o1=null 
           

heap中對象有強可及對象、軟可及對象、弱可及對象、虛可及對象和不可到達對象。應用的強弱順序是強、軟、弱、和虛。對于對象是屬于哪種可及的對象,由他的最強的引用決定。如下:

String abc=new String("abc");  //1       
SoftReference<String> abcSoftRef=new SoftReference<String>(abc);  //2       
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //3       
abc=null; //4       
abcSoftRef.clear();//5    
           

上面的代碼中:

第一行在heap對中建立内容為“abc”的對象,并建立abc到該對象的強引用,該對象是強可及的。第二行和第三行分别建立對heap中對象的軟引用和弱引用,此時heap中的對象仍是強可及的。第四行之後heap中對象不再是強可及的,變成軟可及的。同樣第五行執行之後變成弱可及的。

1.2:軟引用

軟引用是主要用于記憶體敏感的高速緩存。在jvm報告記憶體不足之前會清除所有的軟引用,這樣以來gc就有可能收集軟可及的對象,可能解決記憶體吃緊問題,避免記憶體溢出。什麼時候會被收集取決于gc的算法和gc運作時可用記憶體的大小。當gc決定要收集軟引用是執行以下過程,以上面的abcSoftRef為例:

  1. 首先将abcSoftRef的referent設定為null,不再引用heap中的new String(“abc”)對象。
  2. 将heap中的new String(“abc”)對象設定為可結束的(finalizable)。
  3. 當heap中的new String(“abc”)對象的finalize()方法被運作而且該對象占用的記憶體被釋放, abcSoftRef被添加到它的ReferenceQueue中。

注:對ReferenceQueue軟引用和弱引用可以有可無,但是虛引用必須有,參見:

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

被 Soft Reference 指到的對象,即使沒有任何 Direct Reference,也不會被清除。一直要到 JVM 記憶體不足且 沒有 Direct Reference 時才會清除,SoftReference 是用來設計 object-cache 之用的。如此一來 SoftReference 不但可以把對象 cache 起來,也不會造成記憶體不足的錯誤 (OutOfMemoryError)。我覺得 Soft Reference 也适合拿來實作 pooling 的技巧。

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

//引用時    
if(sr!=null){    
    obj = sr.get();    
}else{    
    obj = new A();    
    sr = new SoftReference(obj);    
}    
           

1.3:弱引用

當gc碰到弱可及對象,并釋放abcWeakRef的引用,收集該對象。但是gc可能需要對此運用才能找到該弱可及對象。通過如下代碼可以了明了的看出它的作用:

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

gc收集弱可及對象的執行過程和軟可及一樣,隻是gc不會根據記憶體情況來決定是不是收集該對象。如果你希望能随時取得某對象的資訊,但又不想影響此對象的垃圾收集,那麼你應該用 Weak Reference 來記住此對象,而不是用一般的 reference。

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 的話,代表此對象已經被清除。這類的技巧,在設計 Optimizer 或 Debugger 這類的程式時常會用到,因為這類程式需要取得某對象的資訊,但是不可以 影響此對象的垃圾收集。

1.4:虛引用

就是沒有的意思,建立虛引用之後通過get方法傳回結果始終為null,通過源代碼你會發現,虛引用通向會把引用的對象寫進referent,隻是get方法傳回結果為null.先看一下和gc互動的過程在說一下他的作用.

1.4.1 不把referent設定為null, 直接把heap中的new String(“abc”)對象設定為可結束的(finalizable).

1.4.2 與軟引用和弱引用不同, 先把PhantomRefrence對象添加到它的ReferenceQueue中.然後在釋放虛可及的對象.

你會發現在收集heap中的new String(“abc”)對象之前,你就可以做一些其他的事情.通過以下代碼可以了解他的作用.

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());       
        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();       
        System.gc();       
        Thread.currentThread().sleep();       
        isRun = false;       
    }
} 
           

結果為

class [email protected]

二、在記憶體中加載圖檔時直接在記憶體中做處理,如:邊界壓縮

對于少量不太大的圖檔這種方式可行,但太多而又大的圖檔小馬用個笨的方式就是,先在記憶體中壓縮,再用軟引用避免OOM。

首先解析一下基本的知識:

  • 位圖模式,bitmap顔色位數是1位
  • 灰階模式,bitmap顔色位數是8位,和256色一樣
  • RGB模式,bitmap顔色位數是24位 在RGB模式下,一個像素對應的是紅、綠、藍三個位元組
  • CMYK模式,bitmap顔色位數是32位 在CMYK模式下,一個像素對應的是青、品、黃、黑四個位元組

圖像檔案的位元組數(Byte) = 圖像分辨率*顔色深度/8(bit/8)

例如:一幅640*480圖像分辨率、RGB色一般為24位真彩色,圖像未經壓縮的資料容量為:640X480X24/8=921600位元組=900KB(1KB=l千位元組=1024位元組)。

注:一個圖像檔案占的磁盤空間大小還和磁盤的檔案格式有關。如:NTFS最小機關為4KB 是以圖像檔案大小肯定是4KB的倍數。但是有圖圖檔壓縮算法的存在,圖檔檔案在儲存時,體積要比在記憶體的大小小得多,如640x480的圖檔檔案大小一般隻在200K~300K。這也是為什麼,加載幾MB的圖檔檔案,會導緻JVM記憶體溢出,導緻OutofMemoryException的原因。

由上面的公式,我們可以得出,加載的圖檔所占的記憶體大小,取決于其分辨率和顔色數。

方式一代碼如下:

@SuppressWarnings("unused") 
private Bitmap copressImage(String imgPath){ 
    File picture = new File(imgPath); 
    Options bitmapFactoryOptions = new BitmapFactory.Options(); 
    //下面這個設定是将圖檔邊界不可調節變為可調節 
    bitmapFactoryOptions.inJustDecodeBounds = true; 
    bitmapFactoryOptions.inSampleSize = ; 
    int outWidth  = bitmapFactoryOptions.outWidth; 
    int outHeight = bitmapFactoryOptions.outHeight; 
    bmap = BitmapFactory.decodeFile(picture.getAbsolutePath(), 
         bitmapFactoryOptions); 
    float imagew = ; 
    float imageh = ; 
    int yRatio = (int) Math.ceil(bitmapFactoryOptions.outHeight 
            / imageh); 
    int xRatio = (int) Math 
            .ceil(bitmapFactoryOptions.outWidth / imagew); 
    if (yRatio >  || xRatio > ) { 
        if (yRatio > xRatio) { 
            bitmapFactoryOptions.inSampleSize = yRatio; 
        } else { 
            bitmapFactoryOptions.inSampleSize = xRatio; 
        } 

    }  
    bitmapFactoryOptions.inJustDecodeBounds = false; 
    bmap = BitmapFactory.decodeFile(picture.getAbsolutePath(), 
            bitmapFactoryOptions); 
    if(bmap != null){                
        //ivwCouponImage.setImageBitmap(bmap); 
        return bmap; 
    } 
    return null; 
} 
           

方式二代碼如下:

package com.lvguo.scanstreet.activity; 

import java.io.File; 
import java.lang.ref.SoftReference; 
import java.util.ArrayList; 
import java.util.HashMap; 
import java.util.List; 
import android.app.Activity; 
import android.app.AlertDialog; 
import android.content.Context; 
import android.content.DialogInterface; 
import android.content.Intent; 
import android.content.res.TypedArray; 
import android.graphics.Bitmap; 
import android.graphics.BitmapFactory; 
import android.graphics.BitmapFactory.Options; 
import android.os.Bundle; 
import android.view.View; 
import android.view.ViewGroup; 
import android.view.WindowManager; 
import android.widget.AdapterView; 
import android.widget.AdapterView.OnItemLongClickListener; 
import android.widget.BaseAdapter; 
import android.widget.Gallery; 
import android.widget.ImageView; 
import android.widget.Toast; 
import com.lvguo.scanstreet.R; 
import com.lvguo.scanstreet.data.ApplicationData; 
/**   
* @Title: PhotoScanActivity.java 
* @Description: 照片預覽控制類 
* @author XiaoMa   
*/ 
public class PhotoScanActivity extends Activity { 
    private Gallery gallery ; 
    private List<String> ImageList; 
    private List<String> it ; 
    private ImageAdapter adapter ;  
    private String path ; 
    private String shopType; 
    private HashMap<String, SoftReference<Bitmap>> imageCache = null; 
    private Bitmap bitmap = null; 
    private SoftReference<Bitmap> srf = null; 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,  
        WindowManager.LayoutParams.FLAG_FULLSCREEN);  
        setContentView(R.layout.photoscan); 
        Intent intent = this.getIntent(); 
        if(intent != null){ 
            if(intent.getBundleExtra("bundle") != null){ 
                Bundle bundle = intent.getBundleExtra("bundle"); 
                path = bundle.getString("path"); 
                shopType = bundle.getString("shopType"); 
            } 
        } 
        init(); 
    } 

    private void init(){ 
        imageCache = new HashMap<String, SoftReference<Bitmap>>(); 
         gallery = (Gallery)findViewById(R.id.gallery); 
         ImageList = getSD(); 
         if(ImageList.size() == ){ 
            Toast.makeText(getApplicationContext(), "無照片,請傳回拍照後再使用預覽", Toast.LENGTH_SHORT).show(); 
            return ; 
         } 
         adapter = new ImageAdapter(this, ImageList); 
         gallery.setAdapter(adapter); 
         gallery.setOnItemLongClickListener(longlistener); 
    } 


    /** 
     * Gallery長按事件操作實作 
     */ 
    private OnItemLongClickListener longlistener = new OnItemLongClickListener() { 

        @Override 
        public boolean onItemLongClick(AdapterView<?> parent, View view, 
                final int position, long id) { 
            //此處添加長按事件删除照片實作 
            AlertDialog.Builder dialog = new AlertDialog.Builder(PhotoScanActivity.this); 
            dialog.setIcon(R.drawable.warn); 
            dialog.setTitle("删除提示"); 
            dialog.setMessage("你确定要删除這張照片嗎?"); 
            dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { 
                @Override 
                public void onClick(DialogInterface dialog, int which) { 
                    File file = new File(it.get(position)); 
                    boolean isSuccess; 
                    if(file.exists()){ 
                        isSuccess = file.delete(); 
                        if(isSuccess){ 
                            ImageList.remove(position); 
                            adapter.notifyDataSetChanged(); 
                            //gallery.setAdapter(adapter); 
                            if(ImageList.size() == ){ 
                                Toast.makeText(getApplicationContext(), getResources().getString(R.string.phoSizeZero), Toast.LENGTH_SHORT).show(); 
                            } 
                            Toast.makeText(getApplicationContext(), getResources().getString(R.string.phoDelSuccess), Toast.LENGTH_SHORT).show(); 
                        } 
                    } 
                } 
            }); 
            dialog.setNegativeButton("取消",new DialogInterface.OnClickListener() { 
                @Override 
                public void onClick(DialogInterface dialog, int which) { 
                    dialog.dismiss(); 
                } 
            }); 
            dialog.create().show(); 
            return false; 
        } 
    }; 

    /** 
     * 擷取SD卡上的所有圖檔檔案 
     * @return 
     */ 
    private List<String> getSD() { 
        /* 設定目前所在路徑 */ 
        File fileK ; 
        it = new ArrayList<String>(); 
        if("newadd".equals(shopType)){  
             //如果是從檢視本人新增清單項或商戶清單項進來時 
            fileK = new File(ApplicationData.TEMP); 
        }else{ 
            //此時為純粹新增 
            fileK = new File(path); 
        } 
        File[] files = fileK.listFiles(); 
        if(files != null && files.length>){ 
            for(File f : files ){ 
                if(getImageFile(f.getName())){ 
                    it.add(f.getPath()); 


                    Options bitmapFactoryOptions = new BitmapFactory.Options(); 

                    //下面這個設定是将圖檔邊界不可調節變為可調節 
                    bitmapFactoryOptions.inJustDecodeBounds = true; 
                    bitmapFactoryOptions.inSampleSize = ; 
                    int outWidth  = bitmapFactoryOptions.outWidth; 
                    int outHeight = bitmapFactoryOptions.outHeight; 
                    float imagew = ; 
                    float imageh = ; 
                    int yRatio = (int) Math.ceil(bitmapFactoryOptions.outHeight 
                            / imageh); 
                    int xRatio = (int) Math 
                            .ceil(bitmapFactoryOptions.outWidth / imagew); 
                    if (yRatio >  || xRatio > ) { 
                        if (yRatio > xRatio) { 
                            bitmapFactoryOptions.inSampleSize = yRatio; 
                        } else { 
                            bitmapFactoryOptions.inSampleSize = xRatio; 
                        } 

                    }  
                    bitmapFactoryOptions.inJustDecodeBounds = false; 

                    bitmap = BitmapFactory.decodeFile(f.getPath(), 
                            bitmapFactoryOptions); 

                    //bitmap = BitmapFactory.decodeFile(f.getPath());  
                    srf = new SoftReference<Bitmap>(bitmap); 
                    imageCache.put(f.getName(), srf); 
                } 
            } 
        } 
        return it; 
    } 

    /** 
     * 擷取圖檔檔案方法的具體實作  
     * @param fName 
     * @return 
     */ 
    private boolean getImageFile(String fName) { 
        boolean re; 

        /* 取得擴充名 */ 
        String end = fName 
                .substring(fName.lastIndexOf(".") + , fName.length()) 
                .toLowerCase(); 

        /* 按擴充名的類型決定MimeType */ 
        if (end.equals("jpg") || end.equals("gif") || end.equals("png") 
                || end.equals("jpeg") || end.equals("bmp")) { 
            re = true; 
        } else { 
            re = false; 
        } 
        return re; 
    } 

    public class ImageAdapter extends BaseAdapter{ 
        /* 聲明變量 */ 
        int mGalleryItemBackground; 
        private Context mContext; 
        private List<String> lis; 

        /* ImageAdapter的構造符 */ 
        public ImageAdapter(Context c, List<String> li) { 
            mContext = c; 
            lis = li; 
            TypedArray a = obtainStyledAttributes(R.styleable.Gallery); 
            mGalleryItemBackground = a.getResourceId(R.styleable.Gallery_android_galleryItemBackground, ); 
            a.recycle(); 
        } 

        /* 幾定要重寫的方法getCount,傳回圖檔數目 */ 
        public int getCount() { 
            return lis.size(); 
        } 

        /* 一定要重寫的方法getItem,傳回position */ 
        public Object getItem(int position) { 
            return lis.get(position); 
        } 

        /* 一定要重寫的方法getItemId,傳并position */ 
        public long getItemId(int position) { 
            return position; 
        } 

        /* 幾定要重寫的方法getView,傳并幾View對象 */ 
        public View getView(int position, View convertView, ViewGroup parent) { 
            System.out.println("lis:"+lis); 
            File file = new File(it.get(position)); 
            SoftReference<Bitmap> srf = imageCache.get(file.getName()); 
            Bitmap bit = srf.get(); 
            ImageView i = new ImageView(mContext); 
            i.setImageBitmap(bit); 
            i.setScaleType(ImageView.ScaleType.FIT_XY); 
            i.setLayoutParams( new Gallery.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, 
                    WindowManager.LayoutParams.WRAP_CONTENT)); 
            return i; 
        } 
    } 
} 
           

上面兩種方式第一種直接使用邊界壓縮,第二種在使用邊界壓縮的情況下間接的使用了軟引用來避免OOM,但大家都知道,這些函數在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體,如果圖檔多且大,這種方式還是會引用OOM異常的,不着急,有的是辦法解決,繼續看,以下方式也大有妙用的:

InputStream is = this.getResources().openRawResource(R.drawable.pic1); 
BitmapFactory.Options options=new BitmapFactory.Options(); 
options.inJustDecodeBounds = false; 
options.inSampleSize = ;   //width,hight設為原來的十分一 
Bitmap btp =BitmapFactory.decodeStream(is,null,options); 

if(!bmp.isRecycle() ){ 
    bmp.recycle()   //回收圖檔所占的記憶體 
    system.gc()  //提醒系統及時回收 
} 
           

上面代碼與下面代碼大家可分開使用,也可有效緩解記憶體問題哦…吼吼…

/** 這個地方大家别搞混了,為了友善小馬把兩個貼一起了,使用的時候記得分開使用
 * 以最省記憶體的方式讀取本地資源的圖檔 
 */   
public static Bitmap readBitMap(Context context, int resId){   
    BitmapFactory.Options opt = new BitmapFactory.Options();   
    opt.inPreferredConfig = Bitmap.Config.RGB_565;    
    opt.inPurgeable = true;   
    opt.inInputShareable = true;   
    //擷取資源圖檔   
    InputStream is = context.getResources().openRawResource(resId);   
    return BitmapFactory.decodeStream(is,null,opt);   
} 
           

再了解一下,android讀取解析圖檔的方式,基本與Java的方式類型,通過檔案輸入流,然後進行解碼,再轉成圖檔格式;當然google的android也為我們封裝好了若幹方法,來友善快捷地完成這項工作,如ImageView的setImageBitmap,setImageResource,BitmapFactory的decodeResource等,但是盡量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設定一張大圖,因為這些函數在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體;

是以,改用先通過BitmapFactory.decodeStream方法,建立出一個bitmap,再将其設為ImageView的source,加載顯示。decodeStream最大的秘密在于其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,進而節省了java層的空間。

在使用decodeStream讀取圖檔時,再加上Config參數,就可以更有效地控制加載目标的記憶體大小,進而更有效阻止抛OutofMemoryException異常,下面用一段代碼說明:

public static Bitmap readBitmapAutoSize(String filePath, int outWidth, int outHeight) {    
                //outWidth和outHeight是目标圖檔的最大寬度和高度,用作限制  
        FileInputStream fs = null;  
        BufferedInputStream bs = null;  
        try {  
            fs = new FileInputStream(filePath);  
            bs = new BufferedInputStream(fs);  
            BitmapFactory.Options options = setBitmapOption(filePath, outWidth, outHeight);  
            return BitmapFactory.decodeStream(bs, null, options);  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            try {  
                bs.close();  
                fs.close();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
        return null;  
    }  

private static BitmapFactory.Options setBitmapOption(String file, int width, int height) {  
        BitmapFactory.Options opt = new BitmapFactory.Options();  
        opt.inJustDecodeBounds = true;            
                //設定隻是解碼圖檔的邊距,此操作目的是度量圖檔的實際寬度和高度  
        BitmapFactory.decodeFile(file, opt);  

        int outWidth = opt.outWidth; //獲得圖檔的實際高和寬  
        int outHeight = opt.outHeight;  
        opt.inDither = false;  
        opt.inPreferredConfig = Bitmap.Config.RGB_565;      
                //設定加載圖檔的顔色數為16bit,預設是RGB_8888,表示24bit顔色和透明通道,但一般用不上  
        opt.inSampleSize = ;                            
                //設定縮放比,1表示原比例,2表示原來的四分之一....  
                //計算縮放比  
        if (outWidth !=  && outHeight !=  && width !=  && height != ) {  
            int sampleSize = (outWidth / width + outHeight / height) / ;  
            opt.inSampleSize = sampleSize;  
        }  

        opt.inJustDecodeBounds = false;//最後把标志複原  
        return opt;  
    }  
           

另外,decodeStream直接拿的圖檔來讀取位元組碼了, 不會根據機器的各種分辨率來自動适應, 使用了decodeStream之後,需要在hdpi和mdpi,ldpi中配置相應的圖檔資源, 否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。 可參考下面的代碼:

BitmapFactory.Options opts = new BitmapFactory.Options();  
//設定圖檔的DPI為目前手機的螢幕dpi  
opts.inTargetDensity = ctx.getResources().getDisplayMetrics().densityDpi;    
opts.inScaled = true;  
           

三、動态回收記憶體,對象銷毀

大家可以選擇在合适的地方使用以下代碼動态并自行顯式調用GC來回收記憶體

if(bitmapObject.isRecycled()==false) //如果沒有回收   
    bitmapObject.recycle();   
           

四、優化Dalvik虛拟機的堆記憶體配置設定

這個就好玩了,優化Dalvik虛拟機的堆記憶體配置設定,聽着很強大,來看下具體是怎麼一回事。

對于Android平台來說,其托管層使用的Dalvik JavaVM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型遊戲或耗資源的應用中可能考慮手動幹涉GC處理,使用 dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程式堆記憶體的處理效率。當然具體原理我們可以參考開源工程,這裡我們僅說下使用方法:

代碼如下:

private final static float TARGET_HEAP_UTILIZATION = f;  

private final static int CWJ_HEAP_SIZE = * *  ;
           

在程式onCreate時就可以調用

VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 

//設定最小heap記憶體為6MB大小。當然對于記憶體吃緊來說還可以通過手動幹涉GC去處理
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); 
           

即可 。

五、自定義堆記憶體大小

我們可以通過下面的代碼看出每個應用程式最高可用記憶體是多少。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / );   
Log.d("TAG", "Max memory is " + maxMemory + "KB"); 
           

是以在展示高分辨率圖檔的時候,最好先将圖檔進行壓縮。壓縮後的圖檔大小應該和用來展示它的控件大小相近,在一個很小的ImageView上顯示一張超大的圖檔不會帶來任何視覺上的好處,但卻會占用我們相當多寶貴的記憶體,而且在性能上還可能會帶來負面影響。下面我們就來看一看,如何對一張大圖檔進行适當的壓縮,讓它能夠以最佳大小顯示的同時,還能防止OOM的出現。

自定義我們的應用需要多大的記憶體,這個好暴力哇,強行設定最小記憶體大小,代碼如下:

private final static int CWJ_HEAP_SIZE = * *  ; 
 //設定最小heap記憶體為6MB大小 
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); 
           

BitmapFactory這個類提供了多個解析方法(decodeByteArray, decodeFile, decodeResource等)用于建立Bitmap對象,我們應該根據圖檔的來源選擇合适的方法。比如SD卡中的圖檔可以使用decodeFile方法,網絡上的圖檔可以使用decodeStream方法,資源檔案中的圖檔可以使用decodeResource方法。這些方法會嘗試為已經建構的bitmap配置設定記憶體,這時就會很容易導緻OOM出現。為此每一種解析方法都提供了一個可選的BitmapFactory.Options參數,将這個參數的inJustDecodeBounds屬性設定為true就可以讓解析方法禁止為bitmap配置設定記憶體,傳回值也不再是一個Bitmap對象,而是null。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會被指派。這個技巧讓我們可以在加載圖檔之前就擷取到圖檔的長寬值和MIME類型,進而根據情況對圖檔進行壓縮。如下代碼所示:

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; 
           

為了避免OOM異常,最好在解析每張圖檔的時候都先檢查一下圖檔的大小,除非你非常信任圖檔的來源,保證這些圖檔都不會超出你程式的可用記憶體。

現在圖檔的大小已經知道了,我們就可以決定是把整張圖檔加載到記憶體中還是加載一個壓縮版的圖檔到記憶體中。以下幾個因素是我們需要考慮的:

預估一下加載整張圖檔所需占用的記憶體。

為了加載這一張圖檔你所願意提供多少記憶體。

用于展示這張圖檔的控件的實際大小。

目前裝置的螢幕尺寸和分辨率。

比如,你的ImageView隻有128*96像素的大小,隻是為了顯示一張縮略圖,這時候把一張1024*768像素的圖檔完全加載到記憶體中顯然是不值得的。

那我們怎樣才能對圖檔進行壓縮呢?

通過設定BitmapFactory.Options中inSampleSize的值就可以實作。比如我們有一張2048*1536像素的圖檔,将inSampleSize的值設定為4,就可以把這張圖檔壓縮成512*384像素。原本加載這張圖檔需要占用13M的記憶體,壓縮後就隻需要占用0.75M了(假設圖檔是ARGB_8888類型,即每個像素點占用4個位元組)。下面的方法可以根據傳入的寬和高,計算出合适的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options,   
        int reqWidth, int reqHeight) {   
    // 源圖檔的高度和寬度   
    final int height = options.outHeight;   
    final int width = options.outWidth;   
    int inSampleSize = ;   
    if (height > reqHeight || width > reqWidth) {   
        // 計算出實際寬高和目标寬高的比率   
        final int heightRatio = Math.round((float) height / (float) reqHeight);   
        final int widthRatio = Math.round((float) width / (float) reqWidth);   
        // 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖檔的寬和高   
        // 一定都會大于等于目标的寬和高。   
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;   
    }   
    return inSampleSize;   
} 
           

使用這個方法,首先你要将BitmapFactory.Options的inJustDecodeBounds屬性設定為true,解析一次圖檔。然後将BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之後再解析一次圖檔,使用新擷取到的inSampleSize值,并把inJustDecodeBounds設定為false,就可以得到壓縮後的圖檔了。

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值再次解析圖檔   
    options.inJustDecodeBounds = false;   
    return BitmapFactory.decodeResource(res, resId, options);   
}    
           

下面的代碼非常簡單地将任意一張圖檔壓縮成100*100的縮略圖,并在ImageView上展示。

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

六、Bitmap 設定圖檔尺寸,避免記憶體溢出的優化方法

bitmap 設定圖檔尺寸,避免 記憶體溢出 OutOfMemoryError的優化方法

主要是加上這段:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = ;
           

通過Uri取圖檔

private ImageView preview;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = ;//圖檔寬高都為原來的二分之一,即圖檔為原來的四分之一
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
preview.setImageBitmap(bitmap);
           

以上代碼可以優化記憶體溢出,但它隻是改變圖檔大小,并不能徹底解決記憶體溢出。

通過路徑去圖檔

private ImageView preview;
private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
                options.inSampleSize = ;//圖檔寬高都為原來的二分之一,即圖檔為原來的四分之一
                        Bitmap b = BitmapFactory.decodeFile(fileName, options);
                        preview.setImageBitmap(b);
                        filePath.setText(fileName);
           

七、使用圖檔緩存技術

在過去,我們經常會使用一種非常流行的記憶體緩存技術的實作,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向于回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖檔的資料會存儲在本地的記憶體當中,因而無法用一種可預見的方式将其釋放,這就有潛在的風險造成應用程式的記憶體溢出并崩潰。

為了能夠選擇一個合适的緩存大小給LruCache, 有以下多個因素應該放入考慮範圍内,例如:

你的裝置可以為每個應用程式配置設定多大的記憶體?

裝置螢幕上一次最多能顯示多少張圖檔?有多少圖檔需要進行預加載,因為有可能很快也會顯示在螢幕上?

你的裝置的螢幕大小和分辨率分别是多少?一個超高分辨率的裝置(例如 Galaxy Nexus) 比起一個較低分辨率的裝置(例如 Nexus S),在持有相同數量圖檔的時候,需要更大的緩存空間。

圖檔的尺寸和大小,還有每張圖檔會占據多少記憶體空間。

圖檔被通路的頻率有多高?會不會有一些圖檔的通路頻率比其它圖檔要高?如果有的話,你也許應該讓一些圖檔常駐在記憶體當中,或者使用多個LruCache 對象來區分不同組的圖檔。

你能維持好數量和品質之間的平衡嗎?有些時候,存儲多個低像素的圖檔,而在背景去開線程加載高像素的圖檔會更加的有效。

并沒有一個指定的緩存大小可以滿足所有的應用程式,這是由你決定的。你應該去分析程式記憶體的使用情況,然後制定出一個合适的解決方案。一個太小的緩存空間,有可能造成圖檔頻繁地被釋放和重新加載,這并沒有好處。而一個太大的緩存空間,則有可能還是會引起 java.lang.OutOfMemory 的異常。

下面是一個使用 LruCache 來緩存圖檔的例子:

private LruCache<String, Bitmap> mMemoryCache;   

@Override   
protected void onCreate(Bundle savedInstanceState) {   
    // 擷取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。   
    // LruCache通過構造函數傳入緩存值,以KB為機關。   
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / );   
    // 使用最大可用記憶體值的1/8作為緩存的大小。   
    int cacheSize = maxMemory / ;   
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {   
        @Override   
        protected int sizeOf(String key, Bitmap bitmap) {   
            // 重寫此方法來衡量每張圖檔的大小,預設傳回圖檔數量。   
            return bitmap.getByteCount() / ;   
        }   
    };   
}   

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {   
    if (getBitmapFromMemCache(key) == null) {   
        mMemoryCache.put(key, bitmap);   
    }   
}   

public Bitmap getBitmapFromMemCache(String key) {   
    return mMemoryCache.get(key);   
} 
           

在這個例子當中,使用了系統配置設定給應用程式的八分之一記憶體來作為緩存大小。在中高配置的手機當中,這大概會有4兆(32/8)的緩存空間。一個全螢幕的 GridView 使用4張 800x480分辨率的圖檔來填充,則大概會占用1.5兆的空間(800*480*4)。是以,這個緩存大小可以存儲2.5頁的圖檔。

當向 ImageView 中加載一張圖檔時,首先會在 LruCache 的緩存中進行檢查。如果找到了相應的鍵值,則會立刻更新ImageView ,否則開啟一個背景線程來加載這張圖檔。

public void loadBitmap(int resId, ImageView imageView) {   
    final String imageKey = String.valueOf(resId);   
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);   
    if (bitmap != null) {   
        imageView.setImageBitmap(bitmap);   
    } else {   
        imageView.setImageResource(R.drawable.image_placeholder);   
        BitmapWorkerTask task = new BitmapWorkerTask(imageView);   
        task.execute(resId);   
    }   
}   
           

BitmapWorkerTask 還要把新加載的圖檔的鍵值對放到緩存中。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {   
    // 在背景加載圖檔。   
    @Override   
    protected Bitmap doInBackground(Integer... params) {   
        final Bitmap bitmap = decodeSampledBitmapFromResource(   
                getResources(), params[], , );   
        addBitmapToMemoryCache(String.valueOf(params[]), bitmap);   
        return bitmap;   
    }   
} 
           

八、終極解決:高清加載巨圖方案 拒絕壓縮圖檔

BitmapRegionDecoder主要用于顯示圖檔的某一塊矩形區域,如果你需要顯示某個圖檔的指定區域,那麼這個類非常合适。

具體使用詳看郭霖大神部落格

文章來源

1、Android之解決太大太多圖檔造成的oom

2、有效解決Android加載大圖檔時記憶體溢出的問題

3、Android高效加載大圖、多圖解決方案,有效避免程式OOM

4、Android有效解決加載大圖檔時記憶體溢出的問題