1. 程式人生 > >Bitmap之點陣圖取樣和記憶體計算詳解

Bitmap之點陣圖取樣和記憶體計算詳解

原文首發於微信公眾號:躬行之(jzman-blog)

Android 開發中經常考慮的一個問題就是 OOM(Out Of Memory),也就是記憶體溢位,一方面大量載入圖片時有可能出現 OOM, 通過取樣壓縮圖片可避免 OOM,另一方面,如一張 1024 x 768 畫素的影象被縮略顯示在 128 x 96 的 ImageView 中,這種做法顯然是不值得的,可通過取樣載入一個合適的縮小版本到記憶體中,以減小記憶體的消耗,Bitmap 的優化主要有兩個方面如下,一是有效的處理較大的點陣圖,二是點陣圖的快取,其中點陣圖快取對應文章如下:

  • Bitmap之記憶體快取和磁碟快取詳解

這篇文章主要側重於如何有效的處理較大的點陣圖。

此外,在 Android 中按照點陣圖取樣的方法載入一個縮小版本到記憶體中應該考慮因素?

  1. 估計載入完整影象所需要的記憶體
  2. 載入這個圖片所需的空間帶給其程式的其他記憶體需求
  3. 載入圖片的目標 ImageView 或 UI 元件的尺寸
  4. 當前裝置的螢幕尺寸或密度

點陣圖取樣

影象有不同的形狀的和大小,讀取較大的圖片時會耗費記憶體。讀取一個位圖的尺寸和型別,為了從多種資源建立一個位圖,BitmapFactory 類提供了許多解碼的方法,根據影象資料資源選擇最合適的解碼方法,這些方法試圖請求分配記憶體來構造點陣圖,因此很容易導致 OOM 異常。每種型別的解碼方法都有額外的特徵可以讓你通過 BitMapFactory.Options 類指定解碼選項。當解碼時設定 inJustDecodeBounds 為true,可在不分配記憶體之前讀取影象的尺寸和型別,下面的程式碼實現了簡單的點陣圖取樣:

/**
  * 點陣圖取樣
  * @param res
  * @param resId
  * @return
  */
public Bitmap decodeSampleFromResource(Resources res, int resId){
    //BitmapFactory建立設定選項
    BitmapFactory.Options options = new BitmapFactory.Options();
    //設定取樣比例
    options.inSampleSize = 200;
    Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options);
    return bitmap;
}

注意:其他 decode... 方法與 decodeResource 類似,這裡都以 decodeRedource 為例。

實際使用時,必須根據具體的寬高要求計算合適的 inSampleSize 來進行點陣圖的取樣,比如,將一個解析度為 2048 x 1536 的影象使用 inSampleSize 值為 4 去編碼產生一個 512 x 384 的影象,這裡假設點陣圖配置為 ARGB_8888,載入到記憶體中僅僅是 0.75M 而不是原來的 12M,關於影象所佔記憶體的計算將在下文中介紹,下面是根據所需寬高進行計算取樣比例的計算方法:

/**
 * 1.計算點陣圖取樣比例
 *
 * @param option
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) {
    //獲得圖片的原寬高
    int width = option.outWidth;
    int height = option.outHeight;

    int inSampleSize = 1;
    if (width > reqWidth || height > reqHeight) {
        if (width > height) {
            inSampleSize = Math.round((float) height / (float) reqHeight);
        } else {
            inSampleSize = Math.round((float) width / (float) reqWidth);
        }
    }
    return inSampleSize;
}

/**
 * 2.計算點陣圖取樣比例
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) {

    //獲得圖片的原寬高
    int height = options.outHeight;
    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;
}

獲得取樣比例之後就可以根據所需寬高處理較大的圖片了,下面是根據所需寬高計算出來的 inSampleSize 對較大點陣圖進行取樣:

/**
 * 點陣圖取樣
 * @param resources
 * @param resId
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) {
    //建立一個位圖工廠的設定選項
    BitmapFactory.Options options = new BitmapFactory.Options();
    //設定該屬性為true,解碼時只能獲取width、height、mimeType
    options.inJustDecodeBounds = true;
    //解碼
    BitmapFactory.decodeResource(resources, resId, options);
    //計算取樣比例
    int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
    //設定該屬性為false,實現真正解碼
    options.inJustDecodeBounds = false;
    //解碼
    Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options);
    return bitmap;
}

在解碼過程中使用了 BitmapFactory.decodeResource() 方法,具體如下:

/**
 * 解碼指定id的資原始檔
 */
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
    ...
    /**
     * 根據指定的id開啟資料流讀取資源,同時為TypeValue進行復制獲取原始資源的density等資訊
     * 如果圖片在drawable-xxhdpi,那麼density為480dpi
     */
    is = res.openRawResource(id, value);
    //從輸入流解碼出一個Bitmap物件,以便根據opts縮放相應的點陣圖
    bm = decodeResourceStream(res, value, is, null, opts);
    ...
}

顯然真正解碼的方法應該是 decodeResourceStream() 方法,具體如下:

/**
 * 從輸入流中解碼出一個Bitmap,並對該Bitmap進行相應的縮放
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
                     InputStream is, Rect pad, BitmapFactory.Options opts) {

    if (opts == null) {
        //建立一個預設的Option物件
        opts = new BitmapFactory.Options();
    }

    /**
     * 如果設定了inDensity的值,則按照設定的inDensity來計算
     * 否則將資原始檔夾所表示的density設定inDensity
     */
    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    /**
     * 同理,也可以通過BitmapFactory.Option物件設定inTargetDensity
     * inTargetDensity 表示densityDpi,也就是手機的density
     * 使用DisplayMetrics物件.densityDpi獲得
     */
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    //decodeStream()方法中呼叫了native方法
    return decodeStream(is, pad, opts);
}

設定完 inDensity 和 inTargetDensity 之後呼叫了 decodeStream() 方法,該方法返回完全解碼後的 Bitmap 物件,具體如下:

/**
 * 返回解碼後的Bitmap,
 */
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
    ...
    bm = nativeDecodeAsset(asset, outPadding, opts);
    //呼叫了native方法:nativeDecodeStream(is, tempStorage, outPadding, opts);
    bm = decodeStreamInternal(is, outPadding, opts);
    Set the newly decoded bitmap's density based on the Options
    //根據Options設定最新解碼的Bitmap
    setDensityFromOptions(bm, opts);
    ...
    return bm;
}

顯然,decodeStream() 方法主要呼叫了本地方法完成 Bitmap 的解碼,跟蹤原始碼發現 nativeDecodeAsset() 和 nativeDecodeStream() 方法都呼叫了 dodecode() 方法,doDecode 方法關鍵程式碼如下:

/**
 * BitmapFactory.cpp 原始碼
 */
static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) {
    ...
    if (env -> GetBooleanField(options, gOptions_scaledFieldID)) {
        const int density = env -> GetIntField(options, gOptions_densityFieldID);
        const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID);
        const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
            //計算縮放比例
            scale = (float) targetDensity / density;
        }
    }
    ...
    //原始Bitmap
    SkBitmap decodingBitmap;
    ...

    //原始點陣圖的寬高
    int scaledWidth = decodingBitmap.width();
    int scaledHeight = decodingBitmap.height();

    //綜合density和targetDensity計算最終寬高
    if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
        scaledWidth = int(scaledWidth * scale + 0.5f);
        scaledHeight = int(scaledHeight * scale + 0.5f);
    }
    ...
    //x、y方向上的縮放比例,大概與scale相等
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    ...
    //將canvas放大scale,然後繪製Bitmap
    SkCanvas canvas (outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint);
}

上面程式碼能看到縮放比例的計算,以及 density 與 targetDensity 對 Bitmap 寬高的影響,實際上間接影響了 Bitmap 在所佔記憶體的大小,這個問題會在下文中舉例說明,注意 density 與當前 Bitmap 所對應資原始檔(圖片)的目錄有關,如有一張圖片位於 drawable-xxhdpi 目錄中,其對應的 Bitmap 的 density 為 480dpi,而 targetDensity 就是 DisPlayMetric 的 densityDpi,也就是手機螢幕代表的 density。那麼怎麼檢視 Android 中本地的 native 方法的實現呢,連結如下:
BitmapFactory.cpp,直接搜尋 native 方法的方法名即可,可以試一下咯。

Bitmap 記憶體計算

首先貢獻一張大圖 6000 x 4000 ,圖片接近 12M,【可在公眾號零點小築索要】 當直接載入這張圖片到記憶體中肯定會發生 OOM,當然通過適當的點陣圖取樣縮小圖片可避免 OOM,那麼 Bitmap 所佔記憶體又如何計算呢,一般情況下這樣計算:

Bitmap Memory = widthPix * heightPix * 4

可使用 bitmap.getConfig() 獲取 Bitmap 的格式,這裡是 ARGB_8888 ,這種 Bitmap 格式下一個畫素點佔 4 個位元組,所以要 x 4,如果將圖片放置在 Android 的資原始檔夾中,計算方式如下:

scale = targetDensity / density
widthPix = originalWidth * scale
heightPix = orignalHeight * scale
Bitmap Memory = widthPix * scale * heightPix * scale * 4

上述簡單總結了一下 Bitmap 所佔記憶體的計算方式,驗證時可使用如下方法獲取 Bitmap 所佔記憶體大小:

BitmapMemory = bitmap.getByteCount()

由於選擇的這張圖片直接載入會導致 OOM,所以下文的事例中都是先採樣壓縮,然後在進行 Bitmap 所佔記憶體的計算。

直接取樣

這種方式就是直接指定取樣比例 inSampleSize 的值,然後先採樣然後計算取樣後的記憶體,這裡指定 inSampleSize 為200。

  1. 將該圖片放在 drawable-xxhdpi 目錄中,此時 drawable-xxhdpi 所代表的 density 為 480(density),我的手機螢幕所代表的 density 是 480(targetDensity),顯然,此時 scale 為1,當然首先對圖片進行取樣,然後將圖片載入到記憶體中, 此時 Bitmap 所佔記憶體記憶體為:
inSampleSize = 200
scale = targetDensity / density} = 480 / 480 = 1
widthPix = orignalScale * scale = 6000 / 200 * 1 = 30 
heightPix = orignalHeight * scale = 4000 / 200 * 1 = 20
Bitmap Memory =  widthPix * heightPix * 4 = 30 * 20 * 4 = 2400(Byte)
  1. 將圖片放在 drawable-xhdpi 目錄中,此時 drawable-xhdpi 所代表的 density 為 320,我的手機螢幕所代表的 density 是 480(targetDensity),將圖片載入到記憶體中,此時 Bitmap 所代表的記憶體為:
inSampleSize = 200
scale = targetDensity / density = 480 / 320
widthPix = orignalWidth * scale = 6000 / 200 * scale = 45
heightPix = orignalHeight * scale = 4000 / 200 * 480 / 320 = 30
Bitmap Memory =  widthPix * scale * heightPix * scale * 4 = 45 * 30 * 4 = 5400(Byte) 

計算取樣

這種方式就是根據請求的寬高計算合適的 inSampleSize,而不是隨意指定 inSampleSize,實際開發中這種方式最常用,這裡請求寬高為100x100,具體 inSampleSize 計算在上文中已經說明。

  1. 將圖片放在 drawable-xxhdpi 目錄中,此時 drawable-xxhdpi 所代表的 density 為 480,我的手機螢幕所代表的 density 是 480(targetDensity),將圖片載入到記憶體中,此時 Bitmap 所代表的記憶體為:
inSampleSize = 4000 / 100 = 40
scale = targetDensity / density = 480 / 480 = 1
widthPix = orignalWidth * scale = 6000 / 40 * 1 = 150      
heightPix = orignalHeight * scale = 4000 / 40 * 1 = 100
BitmapMemory = widthPix * scale * heightPix * scale * 4 = 60000(Byte)
  1. 將圖片放在 drawable-xhdpi 目錄中,此時 drawable-xhdpi 所代表的 density 為 320,我的手機螢幕所代表的 density 是 480(targetDensity),將圖片載入到記憶體中,此時 Bitmap 所代表的記憶體為:
inSampleSize = 4000 / 100 = 40
scale = targetDensity / density = 480 / 320
widthPix = orignalWidth * scale = 6000 / 40 * scale = 225
heightPix = orignalHeight * scale = 4000 / 40 * scale = 150
BitmapMemory = widthPix * heightPix * 4 = 225 * 150 * 4 = 135000(Byte)

點陣圖取樣及 Bitmap 在不同情況下所佔記憶體的計算大概過程如上所述。

測試效果

測試效果圖參考如下:

drawable-xhdpi drawable-xxhdpi

如果感興趣,可以關注公眾號:jzman-blog,一起交流學習。

相關推薦

Bitmap點陣取樣記憶體計算

原文首發於微信公眾號:躬行之(jzman-blog) Android 開發中經常考慮的一個問題就是 OOM(Out Of Memory),也就是記憶體溢位,一方面大量載入圖片時有可能出現 OOM, 通過取樣壓縮圖片可避免 OOM,另一方面,如一張 1024 x 768 畫素的影象被縮略顯示在 128 x

前端學習筆記js中apply()call()方法

經過網上的大量搜尋,漸漸明白了apply()和call方法的使用,為此寫一篇文章記錄一下。 定義 apply()方法: Function.apply(obj,args)

WatchOS開發教程三: 導航方式控制元件

導航方式 Watch App中導航樣式分為兩種:分頁樣式(Page based) 和分層樣式(Hierarchical), 這兩種樣式是互斥的,所以不能混合使用只能選擇其一。Hierarchical方式可以通過pushController或者prese

MongoDB常用命令彙總插入、更新刪除操作

Insurt操作 insurt操作是MongoDB插入資料的基本方法,對目標集合使用Insert操作,會將該文件新增到MongoDB並自動生成相應的ID鍵。文件結果採用類似JSON的BSON格式。常見的插入操作主要有單挑插入和批量插入兩種形式。插入時只是簡單地將文件存入資

Android進階——效能優化佈局渲染原理底層機制(四)

引言 UI 全稱User Interaction,我第一次聽到這個名詞是在大學的時候,當時候上人機互動課,我們教授說他認為iPhone的i 就是代表Interaction的意思,暫且不必爭辯是非。回到我們軟體開發中來,UI是使用者感知與互動的第一且唯一的途徑,

大量資料去重:Bitmap點陣演算法布隆過濾器(Bloom Filter)

Bitmap演算法 與其說是演算法,不如說是一種緊湊的資料儲存結構。是用記憶體中連續的二進位制位(bit),用於對大量整型資料做去重和查詢。其實如果並非如此大量的資料,有很多排重方案可以使用,典型的就是雜湊表。 實際上,雜湊表為每一個可能出現的數字提供了一個一一對映的關係,每個元素都相當於有

資料結構點陣bitmap (轉)

1.  概述 點陣圖(bitmap)是一種非常常用的結構,在索引,資料壓縮等方面有廣泛應用。本文介紹了點陣圖的實現方法及其應用場景。 2. 點陣圖實現 (1)自己實現 在點陣圖中,每個元素為“0”或“1”,表示其對應的元素不存在或者存在。 複製程式碼程式碼如

django框架系統路由系統

沒有 *args mat rap 其他 自己 XML func quest 內容回顧:     1. tags 1. for循環 {% for name in name_list %} {{ name }}

.NET基礎型別語法基礎記憶體管理基礎

轉自:http://www.cnblogs.com/edisonchou/p/4787775.html   型別語法基礎和記憶體管理基礎  Index : (1)型別語法、記憶體管理和垃圾回收基礎 (2)面向物件的實現和異常的處理 (3)字串

HTML5繪製文字

1.繪製圖像的方法 context.drawImage(imageObj,x,y);             此方法需要一個影象物件和一個起始點作為引數,其中起始點座標是相對於canvas的左上角的位置

【深入Java虛擬機器】記憶體區域(Eden Space、Survivor Space、Old Gen、Code CachePerm Gen)

1.記憶體區域劃分 限定商用虛擬機器基本都採用分代收集演算法進行垃圾回收。根據物件的生命週期的不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適當的收集演算法。大批物件死去、少量物件存活的,使用複製演算法,複製成本低;物件存活率高、沒有額外空間進行分配擔保的,採用標記-清除演算法

數字影象處理點陣在計算機中的儲存結構

點陣圖是windows中廣泛應用的一種影象格式,其後綴名為.bmp.點陣圖也稱為位對映圖片. 一張點陣圖包含了許許多多的畫素點,每個畫素點有不同的顏色。由此構成了五彩斑斕的點陣圖影象, 然而正是因為由畫素點這一基本元素構成,所以在放大圖片的時候,人眼便能夠區分一個一

Android 繪圖基礎:Bitmap點陣)與Matrix(矩陣)實現圖片5種操作(平移、旋轉、錯切、縮放、對稱)

Android的Matrix利用數學原理實現圖片平移、旋轉等操作詳解   很慶幸自己的線性代數學的還可以,理解Matrix的矩陣變換完全沒有問題。Matrix矩陣實現圖片的平移旋轉等操作涉及到線性代數問題,感興趣它的具體實現的話可以參考一下上面的部落格,非常詳

點陣引起的記憶體溢位OutOfMemory解決方案

點陣圖引起的記憶體溢位OutOfMemory解決方案作者:老帥一、問題描述:Android下的相機在獨自使用時,拍照沒有問題,通過我們的程式碼呼叫時,也正常,但是更換了不同廠商的平板,ROM由Andro

bitmap點陣

1.什麼是點陣圖? 來自於《程式設計珠璣》。所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit為單位來儲存資料,因此在儲存空間方面,可以大大節省。 比如: 申請一個int型的空間,則有4Byte,3

Android Bitmap點陣

一、背景 在Android開發中,任何一個APP都離不開圖片的載入和顯示問題。這裡的圖片來源分為三種:專案圖片資原始檔(一般為res/drawable目錄下的圖片檔案)、手機本地圖片檔案、網路圖片資源等。圖片的顯示我們一般採用ImageView作為載體,通過ImageView的相應API即可設定其顯示的圖片

Sql Server參數化查詢where inlike實現

blog charindex 語句 pan 建議 ack rop for 臨時表 文章導讀 拼SQL實現where in查詢 使用CHARINDEX或like實現where in 參數化 使用exec動態執行SQl實現where in 參數化 為每一個參數生成一個參數

C#網絡編程基礎進程線程

詳解 面試 ring develop -a display asp.net codes frame 在C#的網絡編程中,進程和線程是必備的基礎知識,同時也是一個重點,所以我們要好好的掌握一下。 一:概念 首先我們要知道什麽是”進程”,什麽是“線程”,好,查一下baik

JDK8版本日期時間API

nds 可變 nan r.java 允許 應該 之一 常用 技巧 伴隨lambda表達式、streams以及一系列小優化,Java 8 推出了全新的日期時間API,在教程中我們將通過一些簡單的實例來學習如何使用新API。Java處理日期、日歷和時間的方式一直為社區所詬病,將

[Java]Java類物件記憶體分配

描述 程式碼說明: 一.當Person p1 = new Person();第一次被呼叫時需要做兩件事: 1.先判斷類載入器是否載入過Person類,如果沒有則載入到Person型別到方法區 2.在堆中開闢記憶體空間,在棧中物件名引用(指向)堆的相應記憶體空間 二. p1.name = '皓皓': 將堆中n