【Android 内存优化】Bitmap 图像尺寸缩小 ( 考虑像素密度、针对从不同像素密度资源中解码对应的 Bitmap 对象 | inDensity | inTargetDensity )
文章目錄
- 一、像素密度對解碼圖片的影響
- 二、不考慮像素密度會導致圖片縮小尺寸不準確
- 三、DisplayMetrics 源碼閱讀、研究手機資源獲取規則
- 四、像素密度參數設置取值 ( inDensity | inTargetDensity | setDensity )
- 五、inDensity 與 inTargetDensity 設置
- 六、新的圖片縮小工具類代碼 ( 在原基礎上添加了像素密度控制 )
- 七、GitHub 地址
一、像素密度對解碼圖片的影響
在之前講內存占用的博客中 【Android 內存優化】Bitmap 內存占用計算 ( Bitmap 圖片內存占用分析 | Bitmap 內存占用計算 | Bitmap 不同像素密度間的轉換 ) , 講到從不同的像素密度資源中獲取圖片 , 其解碼后的大小不同 ;
在上述博客最后從不同像素密度 , 加載 1990 x 1020 大小的圖片 , 解碼出來分別是如下結果 :
- hdpi : 寬 3483 , 高 1785 , 占用內存 24868620 字節 ;
- mdpi : 寬 5224 , 高 2678 , 占用內存 55959488 字節 ; 從 drawable 默認目錄中讀取也是這個配置 ;
- xhdpi : 寬 2612 , 高 1339 , 占用內存 13989872 字節 ;
- xxhdpi : 寬 1741 , 高 893 , 占用內存 6218852 字節 ;
- xxxhdpi : 寬 1306 , 高 669 , 占用內存 3494856 字節 ;
詳細的計算過程查看上述博客 , 這里不再詳述 ;
Bitmap 解碼尺寸計算公式如下 :
加載到內存中的寬或高像素值=實際寬或高像素值×本機像素密度圖片存放的目錄對應的像素密度加載到內存中的寬或高像素值 = 實際寬或高像素值 \times \dfrac{本機像素密度}{圖片存放的目錄對應的像素密度}加載到內存中的寬或高像素值=實際寬或高像素值×圖片存放的目錄對應的像素密度本機像素密度?
二、不考慮像素密度會導致圖片縮小尺寸不準確
目前 R.drawable.blog 圖片在 drawable 目錄中存放 , 其代表的像素密度前綴是 mdpi ;
從該 drawable 目錄中讀取的資源 densityDpi 值為 DENSITY_MEDIUM = 160, 當前的 Pixel 2 手機屏幕密度 density = 2.625 , 屏幕像素密度 densityDpi = 420 ;
在博客 【Android 內存優化】Bitmap 圖像尺寸縮小 ( 設置 Options 參數 | inJustDecodeBounds | inSampleSize | 工具類實現 ) 中出現如下問題 :
明明在代碼中設置了寬高最大值時 100 x 100 , 解碼出來的圖片居然是 , 程序解析錯了 ?
Bitmap reduceSizeBitmap = BitmapSizeReduce.getResizedBitmap(this, R.drawable.blog,100, 100 , false , null);解碼結果 : 解碼出來的寬度 163 像素 , 高度 81 像素 , 明顯出現問題了 ;
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原因說明 :
在設置了 options.inJustDecodeBounds = true 選項后 , 使用 BitmapFactory.decodeResource(resources, iamgeResId, options) 解碼出的圖片參數 , 是圖片的實際參數 , 即 1990 x 1020 , 此時按照該實際參數進行了圖片解碼 , 計算圖片縮小值 inSampleSize = 32 , 此時是可以將圖片寬高都縮小到 100 的 , 縮小后的圖片寬高是 62 x 32 ;
如果從真實的圖像解碼 , 會將像素密度解碼考慮進去 , 這里從 mdpi 資源中解碼圖片 , 實際的解碼出來的大小是 5224 x 2678 , 如果將該值縮小 32 倍 , 肯定無法到達寬高都小于 100 像素 , 這里得到的圖片大小事 163 x 81 ;
三、DisplayMetrics 源碼閱讀、研究手機資源獲取規則
仔細閱讀 DisplayMetrics 中的代碼 , 可以看到不同像素密度的手機的資源來源 , 基本上是獲取其向上取整屏幕密度的資源 , 如果當前手機 densityDpi = 420 , 其處于 DENSITY_XHIGH 與 DENSITY_XXHIGH 之間 , 那么就會優先讀取 DENSITY_XXHIGH 對應 xxhdpi 中的資源 , 這也是為了保證圖片清晰度設定的策略 ;
規則 : 當手機的屏幕像素密度處于兩個標準量化值之間 , 那么會自動選取高的標準量化值對應的資源縮小后使用 ;
public class DisplayMetrics {// 低密度屏幕標準量化值 , 對應 ldpi , 現在基本不使用public static final int DENSITY_LOW = 120;// DENSITY_LOW 與 DENSITY_MEDIUM 之間的密度 // 應用程序中不用考慮為這些像素密度準備資源 // 該密度的手機由系統自動縮放 DENSITY_MEDIUM 對應的資源使用public static final int DENSITY_140 = 140;// 中等密度屏幕標準量化值 , 對應 mdpi public static final int DENSITY_MEDIUM = 160; // DENSITY_MEDIUM 與 DENSITY_HIGH 之間的密度 // 應用程序中不用考慮為這些像素密度準備資源 // 該密度的手機由系統自動縮放 DENSITY_HIGH 對應的資源使用public static final int DENSITY_180 = 180; public static final int DENSITY_200 = 200;public static final int DENSITY_TV = 213;public static final int DENSITY_220 = 220;// 高密度屏幕標準量化值 , 對應 hdpi public static final int DENSITY_HIGH = 240;// DENSITY_HIGH 與 DENSITY_XHIGH 之間的密度 // 應用程序中不用考慮為這些像素密度準備資源 // 該密度的手機由系統自動縮放 DENSITY_XHIGH 對應的資源使用public static final int DENSITY_260 = 260;public static final int DENSITY_280 = 280;public static final int DENSITY_300 = 300;// 超高密度屏幕標準量化值 , 對應 xhdpi public static final int DENSITY_XHIGH = 320;// DENSITY_XHIGH 與 DENSITY_XXHIGH 之間的密度 // 應用程序中不用考慮為這些像素密度準備資源 // 該密度的手機由系統自動縮放 DENSITY_XXHIGH 對應的資源使用public static final int DENSITY_340 = 340;public static final int DENSITY_360 = 360;public static final int DENSITY_400 = 400;public static final int DENSITY_420 = 420;public static final int DENSITY_440 = 440;public static final int DENSITY_450 = 450;// 超超高密度屏幕標準量化值 , 對應 xxhdpi public static final int DENSITY_XXHIGH = 480;// DENSITY_XXHIGH 與 DENSITY_XXXHIGH 之間的密度 // 應用程序中不用考慮為這些像素密度準備資源 // 該密度的手機由系統自動縮放 DENSITY_XXXHIGH 對應的資源使用public static final int DENSITY_560 = 560;public static final int DENSITY_600 = 600;// 超超超高密度屏幕標準量化值 , 對應 xxxhdpi public static final int DENSITY_XXXHIGH = 640;public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;public static int DENSITY_DEVICE = getDeviceDensity();public static final int DENSITY_DEVICE_STABLE = getDeviceDensity();// 省略一萬行代碼 ... }
四、像素密度參數設置取值 ( inDensity | inTargetDensity | setDensity )
在 Bitmap 操作過程中 , 需要設置一系列與像素密度相關的取值 , 如 inDensity , inTargetDensity , setDensity 等值 ;
這些值設置的是 densityDpi 值 , 定義在 DisplayMetrics 中 ;
就是上述的 DENSITY_LOW 到 DENSITY_XXXHIGH 之間的一系列常量值 , 取值范圍 120 ~ 640 ;
五、inDensity 與 inTargetDensity 設置
這兩個值都是 BitmapFactory.Options 中設置的值 ;
① inDensity 像素密度值 : 設置該值會導致被返回的圖像會被強制設置一個像素密度值 , 相當于設置了圖片來自于哪個像素密度的資源 ;
② inTargetDensity 目標像素密度值 : 表示要縮放到的目標圖像像素密度值 , 該值需要結合 inScaled 值使用 , 如果同時設置了 inScaled = true , 和 inDensity 像素密度值 , 在圖像返回時 , 會自動將圖像按照 inDensity 向 inTargetDensity 縮放 ;
// 設置圖片的來源方向的像素密度 , 如設置 options.inDensity = decodeDensityDpi;// 設置圖片的目標方向的像素密度options.inTargetDensity = decodeDensityDpi;// 設置圖片解碼可縮放 , 該配置與上述兩個配置結合使用options.inScaled = true;六、新的圖片縮小工具類代碼 ( 在原基礎上添加了像素密度控制 )
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();// 為圖像指定解碼的 像素密度int decodeDensityDpi = resources.getDisplayMetrics().densityDpi;// 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;// 設置圖片的來源方向的像素密度 , 如設置options.inDensity = decodeDensityDpi;// 設置圖片的目標方向的像素密度options.inTargetDensity = decodeDensityDpi;// 設置圖片解碼可縮放 , 該配置與上述兩個配置結合使用options.inScaled = true;/*由于設置了 inJustDecodeBounds = true , 該方法返回值為空 ;但是傳入的 BitmapFactory.Options 對象中的 outXxx 字段都會被賦值 ;如 outWidth , outHeight , outConfig , outColorSpace 等 ;可以獲取該圖片的寬高 , 像素格式 , 顏色空間等信息*/BitmapFactory.decodeResource(resources, iamgeResId, options);// 獲取 iamgeResId 圖片資源對應的圖片寬度imageWidth = options.outWidth;// 獲取 iamgeResId 圖片資源對應的圖片高度imageHeight = options.outHeight;// 打印解碼后的寬高值Log.w(TAG, "getResizedBitmap options.outWidth=" + options.outWidth +" , options.outHeight=" + 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 , 64inSampleSize = 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);}}
執行結果 :
2020-07-01 11:17:31.389 12350-12350/kim.hsl.bm I/Bitmap: blog : 5224 , 2678 , 55959488 2020-07-01 11:17:31.390 12350-12350/kim.hsl.bm W/BitmapSizeReduce: getResizedBitmap options.outWidth=1990 , options.outHeight=1020 2020-07-01 11:17:31.390 12350-12350/kim.hsl.bm W/BitmapSizeReduce: getResizedBitmap inSampleSize=32 2020-07-01 11:17:31.413 12350-12350/kim.hsl.bm I/Bitmap: reduceSizeBitmap : 62 , 31 , 3844順利將圖片的寬高都縮小為 100 像素以下 ;
七、GitHub 地址
BitmapMemory
總結
以上是生活随笔為你收集整理的【Android 内存优化】Bitmap 图像尺寸缩小 ( 考虑像素密度、针对从不同像素密度资源中解码对应的 Bitmap 对象 | inDensity | inTargetDensity )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Android 内存优化】Bitmap
- 下一篇: 【错误记录】Android 内存泄漏 错