圖片壓縮和緩衝高效載入規避oom總結,圖片壓縮oom
高效載入大圖片
查看每個應用程式最高可用記憶體是多少:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); Log.d("TAG", "Max memory is " + maxMemory + "KB");
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;
設定BitmapFactory.Options中inSampleSize的值就可以實現圖片壓縮,比如我們有一張2048*1536像素的圖片,將inSampleSize的值設定為4,就可以把這張圖片壓縮成512*384像素。原本載入這張圖片需要佔用12M的記憶體,壓縮後就只需要佔用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 = 1; 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, 100, 100));
以上的方法適合使用在讀取一個未知來源的圖片時使用,因為你不知道這個未知來源圖片的大小,那麼還有一種方法是用在已經載入記憶體的圖片,對已經載入記憶體的圖片做壓縮以後重新儲存到本地,從而可以把一張原本1M大小的圖片變成一張10K的圖片。
這種方法的核心思想是首先將圖片轉成一個輸出資料流,並記錄輸出資料流的byte數組大小,通過調用bitmap對象的compress方法,對圖片做一次壓縮以及格式化,並將byte數組大小與期望壓縮的目標大小比對,得出壓縮比率,並調用Bitmap的縮放方法,縮放計算出的壓縮比率,從而得到壓縮後的方法。
代碼如下:
/** * 圖片壓縮方法:(使用compress的方法) * * @explain 如果bitmap本身的大小小於maxSize,則不作處理 * @param bitmap * 要壓縮的圖片 * @param maxSize * 壓縮後的大小,單位kb */ public static void imageZoom(Bitmap bitmap, double maxSize) { // 將bitmap放至數組中,意在獲得bitmap的大小(與實際讀取的原檔案要大) ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 格式、品質、輸出資料流 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] b = baos.toByteArray(); // 將位元組換成KB double mid = b.length / 1024; // 擷取bitmap大小 是允許最大大小的多少倍 double i = mid / maxSize; // 判斷bitmap佔用空間是否大於允許最大空間 如果大於則壓縮 小於則不壓縮 if (i > 1) { // 縮放圖片 此處用到平方根 將寬頻和高度壓縮掉對應的平方根倍 // (保持寬高不變,縮放後也達到了最大佔用空間的大小) bitmap = scale(bitmap, bitmap.getWidth() / Math.sqrt(i), bitmap.getHeight() / Math.sqrt(i)); } }/*** * 圖片的縮放方法 * * @param src * :源圖片資源 * @param newWidth * :縮放後寬度 * @param newHeight * :縮放後高度 */ public static Bitmap scale(Bitmap src, double newWidth, double newHeight) { // 記錄src的寬高 float width = src.getWidth(); float height = src.getHeight(); // 建立一個matrix容器 Matrix matrix = new Matrix(); // 計算縮放比例 float scaleWidth = ((float) newWidth) / width; float scaleHeight = ((float) newHeight) / height; // 開始縮放 matrix.postScale(scaleWidth, scaleHeight); // 建立縮放後的圖片 return Bitmap.createBitmap(src, 0, 0, (int) width, (int) height, matrix, true); }
壓縮以後就可高效載入bitmap了。這種方法可以有效避免大圖造成的oom,但是當你需要在介面上載入一大堆圖片的時候,在很多情況下,(比如使用ListView, GridView 或者 ViewPager 這樣的組件),螢幕上顯示的圖片可以通過滑動螢幕等事件不斷地增加,最終也會導致OOM。
這就需要緩衝技術避免oom了
圖片緩衝
為了保證記憶體的使用始終維持在一個合理的範圍,通常會把被移除螢幕的圖片進行回收處理。此時記憶體回收行程也會認為你不再持有這些圖片的引用,從而對這些圖片進行GC操作。用這種思路來解決問題是非常好的,可是為了能讓程式快速運行,在介面上迅速地載入圖片,你又必須要考慮到某些圖片被回收之後,使用者又將它重新滑入螢幕這種情況。這時重新去載入一遍剛剛載入過的圖片無疑是效能的瓶頸,你需要想辦法去避免這個情況的發生。
這個時候,使用記憶體緩衝技術可以很好的解決這個問題,它可以讓組件快速地重新載入和處理圖片。下面我們就來看一看如何使用記憶體緩衝技術來對圖片進行緩衝,從而讓你的應用程式在載入很多圖片的時候可以提高響應速度和流暢性。
其中最核心的類是LruCache (此類在android-support-v4的包中提供) 。這個類非常適合用來緩衝圖片,它的主要演算法原理是把最近使用的對象用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的對象在緩衝值達到預設定值之前從記憶體中移除。
從 Android 2.3 (API Level 9)開始,記憶體回收行程會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。
另外,Android 3.0 (API Level 11)中,圖片的資料會儲存在本地的記憶體當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程式的記憶體溢出並崩潰。
下面是一個使用 LruCache 來緩衝圖片的例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // 擷取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。 // LruCache通過建構函式傳入緩衝值,以KB為單位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用記憶體值的1/8作為緩衝的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重寫此方法來衡量每張圖片的大小,預設返回圖片數量。 return bitmap.getByteCount() / 1024; } }; } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
當向 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[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
更多資料:這裡寫連結內容