【Android 內存優化】Bitmap 圖像尺寸縮小 ( 設置 Options 參數 | inJustDecodeBounds | inSampleSize | 工具類實現 )





一、解碼圖片參數 inJustDecodeBounds



1 . 解碼圖片參數 :


① 設置獲取參數解碼選項 : 設置解碼時的 BitmapFactory.Options 對象的 inJustDecodeBounds 爲 true ,

② 解碼圖像 : 解析器返回的 Bitmap 對象爲 null ;

③ 解碼選項 : BitmapFactory.Options 中的 outXxx 字段會被設置對應的圖片屬性值 ;

④ 解碼選項參數示例 : 如 : outWidth 輸出圖像的 寬度 , outHeight 輸出高度 , outMimeType 輸出類型 , outConfig 像素格式 , outColorSpace 輸出顏色空間 ;


2 . 代碼示例 :

// Bitmap 圖片加載選項
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, iamgeResId, options);




二、計算圖片的縮小比例



計算圖片的縮小比例 :


① 目標圖片寬高要求 : 寬度和高度只要存在一個大於限定的最大值時 , 就進行縮小操作 ; 要求指定的圖片必須能放到 maxBitmapWidth 寬度 , maxBitmapHeight 高度的矩形框中 ; 最終要求就是 寬度必須小於 maxBitmapWidth, 同時高度也要小於 maxBitmapHeight ;

② 縮小倍數要求 : 縮小倍數只能是 2 的冪次方值 , 1 , 2 , 4 , 8 , 16 , 32 , 64 ;

        /*
            計算縮小的比例
            寬度和高度只要存在一個大於限定的最大值時 , 就進行縮小操作
            要求指定的圖片必須能放到 maxBitmapWidth 寬度 , maxBitmapHeight 高度的矩形框中
            最終要求就是 寬度必須小於 maxBitmapWidth, 同時高度也要小於 maxBitmapHeight
         */
        if(imageWidth > maxBitmapWidth || imageHeight > maxBitmapHeight){
            // 如果需要啓動縮小功能 , 那麼進入如下循環 , 試探最小的縮放比例是多少
            while ( imageWidth / inSampleSize > maxBitmapWidth ||
                    imageHeight / inSampleSize > maxBitmapHeight ){
                // 注意該值必須是 2 的冪次方值 , 1 , 2 , 4 , 8 , 16 , 32 , 64
                inSampleSize = inSampleSize * 2;
            }

            // 執行到此處 , 說明已經找到了最小的縮放比例 , 打印下最小比例
            Log.w(TAG, "getResizedBitmap inSampleSize=" + inSampleSize);
        }




三、設置圖片縮小配置 inSampleSize



1 . 圖片縮小配置 inSampleSize :


① inSampleSize 設置大於 1 : 如果值大於 1 , 那麼就會縮小圖片 ;

② 解碼器操作 : 此時解碼器對原始的圖片數據進行子採樣 , 返回較小的 Bitmap 對象 ;

③ 樣本個數 : 樣本的大小是在兩個維度計算的像素個數 , 每個像素對應一個解碼後的圖片中的單獨的像素點 ;

④ 樣本個數計算示例 : 如果 inSampleSize 值爲 2 , 那麼寬度的像素個數會縮小 2 倍 , 高度也會縮小兩倍 ; 整體像素個數縮小 4 倍 , 內存也縮小了 4 倍 ;



2 . inSampleSize 取值要求 :


① 小於 1 取值 : 如果取值小於 1 , 那麼就會被當做 1 , 1 相當於 2 的 0 次方 ;

② 取值要求 : 該值必須是 2 的冪次方值 , 2 的次方值 , 如 1 , 2 , 4 , 8 , 16 , 32

③ 不合法值 : 如果出現了不合法的值 , 就會就近四捨五入到最近的 2 的冪次方值



3 . 代碼示例 :

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




四、設置圖片像素格式 inPreferredConfig



1 . 解碼像素格式 :


① 指定配置解碼 : 如果配置爲非空 , 解碼器會將 Bitmap 的像素解碼成該指定的非空像素格式 ;

② 自動匹配配置解碼 : 如果該配置爲空 , 或者像素配置無法滿足 , 解碼器會嘗試根據系統的屏幕深度 , 源圖像的特點 , 選擇合適的像素格式 ; 如果源圖像有透明度通道 , 那麼自動匹配的默認配置也有對應通道 ;

③ 默認配置 : 默認使用 ARGB_8888 進行解碼


2 . 代碼示例 :

options.inPreferredConfig = Bitmap.Config.RGB_565;




五、設置圖片複用機制



1 . 圖片複用機制 :


① 圖片複用 : 如果設置了一個 Bitmap 對象給 inBitmap 參數 , 解碼方法會獲取該 Bitmap 對象 , 當加載圖片內容時 , 會嘗試複用該 Bitmap 對象的內存

② 無法複用拋出異常 : 如果解碼方法無法複用該 Bitmap 對象 , 解碼方法可能會拋出 IllegalArgumentException 異常 ;

③ 圖片可變性 : 當前的實現是很有必要的 , 被複用的圖片必須是可變的 , 解碼後的 Bitmap 對象也是可變的 , 即使當解碼一個資源圖片時 , 經常會得到一個不可變的 Bitmap 對象 ;



2 . 解碼結果判定 :


① 解碼可能失敗 : 該解碼方法返回的 Bitmap 對象是可以使用的 , 鑑於上述約束情況 和 可能發生的失敗故障 , 不能假定該圖片解碼操作是成功的 ;

② 檢查複用是否成功 : 解碼檢查解碼返回的 Bitmap 對象是否與設置給 Options 對象的 inBitmap 相匹配 , 來判斷該 inBitmap 是否被複用 ;

③ 後續操作 : 不管有沒有複用成功 , 你應該使用解碼函數返回的 Bitmap 對象 , 保證程序的正常運行 ;



3 . 與 BitmapFactory 配合使用 :


① Android 4.4 以後的複用機制 : 在 KITKAT 以後的代碼中 , 只要被解碼生成的 Bitmap 對象的字節大小 ( 縮放後的 ) , 小於等於 inBitmap 的字節大小 , 就可以複用成功 ;

② Android 4.4 之前的複用機制 : 在 KITKAT ( Android 4.4 系統 , android-19 平臺 ) 之前的代碼中 , 被解碼的圖像必須是

  • JPEG 或 PNG 格式 ,
  • 並且 圖像大小必須是相等的 ,
  • inssampleSize 設置爲 1 ,

才能複用成功 , 另外被複用的圖像的 像素格式 Config ( 如 RGB_565 ) 會覆蓋設置的 inPreferredConfig 參數



4 . 代碼示例 :

options.inBitmap = inBitmap;




六、Bitmap 圖像尺寸縮小代碼示例





1、圖片縮小工具類


圖片縮小工具類 :

package kim.hsl.bm.utils;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

/**
 * Bitmap 尺寸縮小
 */
public class BitmapSizeReduce {
    private static final String TAG = "BitmapSizeReduce";

    /**
     * 獲取指定尺寸於鏊求的 Bitmap 對象
     * 該方法有缺陷 , 計算值的時候沒有考慮像素密度
     * 如果從不同像素密度的資源文件中加載
     * 可能計算出的值與指定的 maxBitmapWidth maxBitmapHeight 略有出入
     *
     * @param context           上下文對象
     * @param iamgeResId        要解析的圖片資源 id
     * @param maxBitmapWidth    Bitmap 的最大寬度
     * @param maxBitmapHeight   Bitmap 的最大高度
     * @param hasAlphaChannel   是否包含 ALPHA 通道, 即透明度信息
     * @param inBitmap          複用的 Bitmap, 將新的 Bitmap 對象解析到該 Bitmap 內存中
     * @return  返回新的 Bitmap 對象
     */
    public static Bitmap getResizedBitmap(Context context,
                                          int iamgeResId, int maxBitmapWidth, int maxBitmapHeight,
                                          boolean hasAlphaChannel, Bitmap inBitmap){

        // 0. 聲明方法中使用的局部變量

        // 用於解析資源
        Resources resources = context.getResources();
        // Bitmap 圖片加載選項
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 圖片寬度
        int imageWidth;
        // 圖片高度
        int imageHeight;
        /*
            根據 圖片寬度 imageWidth , 圖片高度 imageHeight ,
            最大寬度 maxBitmapWidth , 最大高度 maxBitmapHeight ,
            計算出的圖片縮放係數 , 該值最終要設置到 BitmapFactory.Options 對象中
         */
        int inSampleSize = 1;

        // 1. 解析圖片參數 : 該階段不解析所有的數據 , 否則會將實際的圖片數據解析到內存中 , 這裏只解析圖片的寬高信息

        /*
            設置 inJustDecodeBounds 爲 true , 解析器會返回 null
            但是 outXxx 字段會被設置對應的圖片屬性值 ,
            如 : outWidth 輸出圖像的 寬度 , outHeight 輸出高度 , outMimeType 輸出類型 ,
            outConfig 像素格式 , outColorSpace 輸出顏色空間
         */
        options.inJustDecodeBounds = true;

        /*
            由於設置了 inJustDecodeBounds = true , 該方法返回值爲空 ;
            但是傳入的 BitmapFactory.Options 對象中的 outXxx 字段都會被賦值 ;
            如 outWidth , outHeight , outConfig , outColorSpace 等 ;
            可以獲取該圖片的寬高 , 像素格式 , 顏色空間等信息
         */
        BitmapFactory.decodeResource(resources, iamgeResId, options);

        // 獲取 iamgeResId 圖片資源對應的圖片寬度
        imageWidth = options.outWidth;
        // 獲取 iamgeResId 圖片資源對應的圖片高度
        imageHeight = options.outHeight;


        // 2. 計算圖片縮小比例

        /*
            計算縮小的比例
            寬度和高度只要存在一個大於限定的最大值時 , 就進行縮小操作
            要求指定的圖片必須能放到 maxBitmapWidth 寬度 , maxBitmapHeight 高度的矩形框中
            最終要求就是 寬度必須小於 maxBitmapWidth, 同時高度也要小於 maxBitmapHeight
         */
        if(imageWidth > maxBitmapWidth || imageHeight > maxBitmapHeight){
            // 如果需要啓動縮小功能 , 那麼進入如下循環 , 試探最小的縮放比例是多少
            while ( imageWidth / inSampleSize > maxBitmapWidth ||
                    imageHeight / inSampleSize > maxBitmapHeight ){
                // 注意該值必須是 2 的冪次方值 , 1 , 2 , 4 , 8 , 16 , 32 , 64
                inSampleSize = inSampleSize * 2;
            }

            // 執行到此處 , 說明已經找到了最小的縮放比例 , 打印下最小比例
            Log.w(TAG, "getResizedBitmap inSampleSize=" + inSampleSize);
        }


        // 3. 設置圖像解碼參數

        /*
            inSampleSize 設置大於 1 : 如果值大於 1 , 那麼就會縮小圖片 ;
            解碼器操作 : 此時解碼器對原始的圖片數據進行子採樣 , 返回較小的 Bitmap 對象 ;

            樣本個數 : 樣本的大小是在兩個維度計算的像素個數 , 每個像素對應一個解碼後的圖片中的單獨的像素點 ;
            樣本個數計算示例 :
            如果 inSampleSize 值爲 2 , 那麼寬度的像素個數會縮小 2 倍 , 高度也會縮小兩倍 ;
            整體像素個數縮小 4 倍 , 內存也縮小了 4 倍 ;

            小於 1 取值 : 如果取值小於 1 , 那麼就會被當做 1 , 1 相當於 2 的 0 次方 ;
            取值要求 : 該值必須是 2 的冪次方值 , 2 的次方值 , 如 1 , 2 , 4 , 8 , 16 , 32
            如果出現了不合法的值 , 就會就近四捨五入到最近的 2 的冪次方值
         */
        options.inSampleSize = inSampleSize;

        // 用戶設置的是否保留透明度選項 , 如果不保留透明度選項 , 設置像素格式爲 RGB_565
        // 每個像素佔 2 字節內存
        if (!hasAlphaChannel){
            /*
                指定配置解碼 : 如果配置爲非空 , 解碼器會將 Bitmap 的像素解碼成該指定的非空像素格式 ;
                自動匹配配置解碼 : 如果該配置爲空 , 或者像素配置無法滿足 , 解碼器會嘗試根據系統的屏幕深度 ,
                源圖像的特點 , 選擇合適的像素格式 ;
                如果源圖像有透明度通道 , 那麼自動匹配的默認配置也有對應通道 ;
                默認配置 : 默認使用 ARGB_8888 進行解碼
             */
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        /*
            注意解碼真實圖像的時候 , 要將 inJustDecodeBounds 設置爲 false
            否則將不會解碼 Bitmap 數據 , 只會將
            outWidth , outHeight , outConfig , outColorSpace 等 outXxx 圖片參數解碼出來
         */
        options.inJustDecodeBounds = false;

        /*
            設置圖片可以被複用
         */
        options.inMutable = true;

        /*
            如果設置了一個 Bitmap 對象給 inBitmap 參數
            解碼方法會獲取該 Bitmap 對象 , 當加載圖片內容時 , 會嘗試複用該 Bitmap 對象的內存

            如果解碼方法無法複用該 Bitmap 對象 , 解碼方法可能會拋出 IllegalArgumentException 異常 ;
            當前的實現是很有必要的 , 被複用的圖片必須是可變的 , 解碼後的 Bitmap 對象也是可變的 ,
            即使當解碼一個資源圖片時 , 經常會得到一個不可變的 Bitmap 對象 ;

            確保是否解碼成功 :
            該解碼方法返回的 Bitmap 對象是可以使用的 ,
            鑑於上述約束情況 和 可能發生的失敗故障 , 不能假定該圖片解碼操作是成功的 ;

            檢查解碼返回的 Bitmap 對象是否與設置給 Options 對象的 inBitmap 相匹配 ,
            來判斷該 inBitmap 是否被複用 ;

            不管有沒有複用成功 , 你應該使用解碼函數返回的 Bitmap 對象 , 保證程序的正常運行 ;

            與 BitmapFactory 配合使用 :

            在 KITKAT 以後的代碼中 , 只要被解碼生成的 Bitmap 對象的字節大小 ( 縮放後的 )
            小於等於 inBitmap 的字節大小 , 就可以複用成功 ;

            在 KITKAT 之前的代碼中 , 被解碼的圖像必須是
            JPEG 或 PNG 格式 ,
            並且 圖像大小必須是相等的 ,
            inssampleSize 設置爲 1 ,
            才能複用成功 ;
            另外被複用的圖像的 像素格式 Config ( 如 RGB_565 ) 會覆蓋設置的 inPreferredConfig 參數
         */
        options.inBitmap = inBitmap;


        // 4. 解碼圖片 , 並返回被解碼的圖片

        return BitmapFactory.decodeResource(resources, iamgeResId, options);
    }

}



2、Activity 調用工具類代碼


Activity 代碼 :

package kim.hsl.bm;

import androidx.appcompat.app.AppCompatActivity;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import kim.hsl.bm.utils.BitmapSizeReduce;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
        
        // 縮小圖像尺寸
        sizeReduce();


    }

    /**
     * 圖像尺寸縮小
     */
    private void sizeReduce(){
        // 從資源文件中加載內存
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blog);
        // 打印 Bitmap 對象的寬高, 字節大小
        Log.i("Bitmap", "blog : " + bitmap.getWidth() + " , " +
                bitmap.getHeight() + " , " +
                bitmap.getByteCount());

        // 從資源文件中加載內存
        Bitmap reduceSizeBitmap = BitmapSizeReduce.getResizedBitmap(this, R.drawable.blog,
                100, 100 , false , null);
        // 打印 Bitmap 對象的寬高, 字節大小
        Log.i("Bitmap", "reduceSizeBitmap : " + reduceSizeBitmap.getWidth() + " , " +
                reduceSizeBitmap.getHeight() + " , " +
                reduceSizeBitmap.getByteCount());
    }

    public native String stringFromJNI();
}



3、執行結果


執行結果 :

2020-06-30 22:04:22.959 3766-3766/? I/Bitmap: blog : 5224 , 2678 , 55959488
2020-06-30 22:04:22.960 3766-3766/? W/BitmapSizeReduce: getResizedBitmap inSampleSize=32
2020-06-30 22:04:22.980 3766-3766/? I/Bitmap: reduceSizeBitmap : 163 , 81 , 26406

分析結果 :

① 源圖像分析 : 從資源中加載 , 普通情況下寬度 5224 像素 , 高度 2678 像素 , ARGB_8888 像素格式 , 每個像素 44 字節 , 計算公式爲

5224×2678×4=55,959,4885224 \times 2678 \times 4 = 55,959,488


② 縮小後的圖像分析 : 從資源中加載 , 普通情況下寬度 163 像素 , 高度 81 像素 , RGB_565 像素格式 , 每個像素 22 字節 , 計算公式爲

163×81×2=26,406163 \times 81 \times 2 = 26,406‬

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章