1. 程式人生 > >Android 開發繞不過的坑:你的 Bitmap 究竟佔多大記憶體?

Android 開發繞不過的坑:你的 Bitmap 究竟佔多大記憶體?

0、寫在前面

本文涉及到螢幕密度的討論,這裡先要搞清楚 DisplayMetrics 的兩個變數,摘錄官方文件的解釋:


  • density:The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5”x2” screen), providing the baseline of the system’s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc. This value does not exactly follow the real screen size (as given by xdpi and ydpi, but rather is used to scale the size of the overall UI in steps based on gross changes in the display dpi. For example, a 240x320 screen will have a density of 1 even if its width is 1.8”, 1.3”, etc. However, if the screen resolution is increased to 320x480 but the screen size remained 1.5”x2” then the density would be increased (probably to 1.5).
  • densityDpi:The screen density expressed as dots-per-inch.
簡單來說,可以理解為 density 的數值是 1dp=density px;densityDpi 是螢幕每英寸對應多少個點(不是畫素點),在 DisplayMetrics 當中,這兩個的關係是線性的:
density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640
為了不引起混淆,本文所有提到的密度除非特別說明,都指的是 densityDpi,當然如果你願意,也可以用 density 來說明問題。 另外,本文的依據主要來自 android 5.0 的原始碼,其他版本可能略有出入。文章難免疏漏,歡迎指正~
1、佔了多大記憶體?

做移動客戶端開發的朋友們肯定都因為圖頭疼過,說起來曾經還有過 leader 因為組裡面一哥們在工程裡面加了一張 jpg 的圖發脾氣的事兒,哈哈。 為什麼頭疼呢?吃記憶體唄,時不時還給你來個 OOM 沖沖喜,讓你的每一天過得有滋有味(真是沒救了)。那每次工程裡面增加一張圖片的時候,我們都需要關心這貨究竟要佔多大的坑,佔多大呢?Android API 有個方便的方法,
public final int getByteCount() {
    // int result permits bitmaps up to 46,340 x 46,340
    return getRowBytes() * getHeight();
}
通過這個方法,我們就可以獲取到一張 Bitmap 在執行時到底佔用多大記憶體了。
舉個例子 一張 522x686 的 PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上載入,佔用記憶體2547360B,就可以用這個方法獲取到。
2、給我一張圖我告訴你佔多大記憶體
每次都問 Bitmap 你到底多大啦。。感覺怪怪的,畢竟我們不能總是去問,而不去搞清楚它為嘛介麼大吧。能不能給它算個命,算算它究竟多大呢?當然是可以的,很簡單嘛,我們直接順藤摸瓜,找出真凶,哦不,找出答案。
2.1 getByteCount
getByteCount 的原始碼我們剛剛已經認識了,當我們問 Bitmap 大小的時候,這孩子也是先拿到出生年月日,然後算出來的,那麼問題來了,getHeight 就是圖片的高度(單位:px),getRowBytes 是什麼?
public final int getrowBytes() {
   if(mRecycled) {
          Log.w(TAG,"Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
   }
   return nativeRowBytes(mFinalizer.mNativeBitmap);
}


額,感覺太對了啊,要 JNI 了。由於在下 C++ 實在用得少,每次想起 JNI 都請想象腦門磕牆的場景,不過呢,毛爺爺說過,一切反動派都是紙老虎~與 
nativeRowBytes 對應的函式如下: Bitmap.cpp
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
     SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
     return static_cast<jint>(bitmap->rowBytes());
}
等等,我們好像發現了什麼,原來 Bitmap 本質上就是一個 SkBitmap。。而這個 SkBitmap 也是大有來頭,不信你瞧:Skia。啥也別說了,趕緊瞅瞅 SkBitmap。 SkBitmap.h
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const{ returnfRowBytes; }
SkBitmap.cpp
size_t SkBitmap::ComputeRowBytes(Config c, intwidth) {
    returnSkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
SkImageInfo.h
 
staticint SkColorTypeBytesPerPixel(SkColorType ct) {
   staticconst uint8_t gSize[] = {
    0, // Unknown
    1, // Alpha_8
    2, // RGB_565
    2, // ARGB_4444
    4, // RGBA_8888
    4, // BGRA_8888
    1, // kIndex_8
  };
  SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
                size_mismatch_with_SkColorType_enum);
 
   SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
   return gSize[ct];
}
 
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, intwidth) {
    returnwidth * SkColorTypeBytesPerPixel(ct);
}


好,跟蹤到這裡,我們發現 ARGB_8888(也就是我們最常用的 Bitmap 的格式)的一個畫素佔用 4byte,那麼 rowBytes 實際上就是 4*width bytes。那麼結論出來了,一張 ARGB_8888 的 Bitmap 佔用記憶體的計算公式 bitmapInRam = bitmapWidth*bitmapHeight *4 bytes 說到這兒你以為故事就結束了麼?有本事你拿去試,算出來的和你獲取到的總是會差個倍數,為啥呢? 還記得我們最開始給出的那個例子麼? 一張522*686的 PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上載入,佔用記憶體2547360B,就可以用這個方法獲取到。 然而公式計算出來的可是1432368B。。。
2.2 Density
知道我為什麼在舉例的時候那麼費勁的說放到xxx目錄下,還要說用xxx手機麼?你以為 Bitmap 載入只跟寬高有關麼?Naive。 還是先看程式碼,我們讀取的是 drawable 目錄下面的圖片,用的是 decodeResource 方法,該方法本質上就兩步:


  • 讀取原始資源,這個呼叫了 Resource.openRawResource 方法,這個方法呼叫完成之後會對 TypedValue 進行賦值,其中包含了原始資源的 density 等資訊;
  • 呼叫 decodeResourceStream 對原始資源進行解碼和適配。這個過程實際上就是原始資源的 density 到螢幕 density 的一個對映。
原始資源的 density 其實取決於資源存放的目錄(比如 xxhdpi 對應的是480),而螢幕 density 的賦值,請看下面這段程式碼:
BitmapFactory.java
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
    InputStream is, Rect pad, Options opts) {
 
//實際上,我們這裡的opts是null的,所以在這裡初始化。
if(opts == null) {
    opts = newOptions();
}
 
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; //這裡density的值如果對應資源目錄為hdpi的話,就是240
    }
}
 
if(opts.inTargetDensity == 0&& res != null) {
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
    opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
 
return decodeStream(is, pad, opts);
}

我們看到 opts 這個值被初始化,而它的構造居然如此簡單:
public Options() {
   inDither = false;
   inScaled = true;
   inPremultiplied = true;
}


所以我們就很容易的看到,Option.inScreenDensity 這個值沒有被初始化,而實際上後面我們也會看到這個值根本不會用到;我們最應該關心的是什麼呢?是 inDensity 和 inTargetDensity,這兩個值與下面 cpp 檔案裡面的 density 和 targetDensity 相對應——重複一下,inDensity 就是原始資源的 density,inTargetDensity 就是螢幕的 density。 緊接著,用到了 nativeDecodeStream 方法,不重要的程式碼直接略過,直接給出最關鍵的 doDecode 函式的程式碼:
BitmapFactory.cpp [Java] 純文字檢視 複製程式碼 ?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 staticjobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

相關推薦

Android 開發不過Bitmap 究竟記憶體

0、寫在前面 本文涉及到螢幕密度的討論,這裡先要搞清楚 DisplayMetrics 的兩個變數,摘錄官方文件的解釋: density:The logical density of the display. This is a scalin

Bitmap 究竟記憶體

reased (probably to 1.5). densityDpi:The screen density expressed as dots-per-inch. 簡單來說,可以理解為 density 的數值是 1dp=density px;densityDp

一張圖片記憶體的計算-android

DisplayMetrics 的兩個變數,摘錄官方文件的解釋:     density:The logical density of the display. This is a scaling factor for the Density Independent Pixe

高端面試必備一個Java物件佔用記憶體

這個問題一般會出現在稍微高階一點的 Java 面試環節。要求面試者不僅對 Java 基礎知識熟悉,更重要的是要了解記憶體模型。 #### Java 物件模型 HotSpot JVM 使用名為 oops (Ordinary Object Pointers) 的資料結構來表示物件。這些 oops 等同於本地

Android開發入門的正確姿勢,get到了嗎?

開源 如何 com 正常 它的 接收 應用 切換 角度 在進行Android開發之前,我們先了解一下Android的生態圈現狀。Android系統是開源的,任何手機廠商和開發者都有權限去修改系統源代碼,定制專屬的系統。 這就產生了一個問題,不同手機廠商之間的ROM可能無法安

Android開發之旅3android架構

通過 圖集 例如 sqlit 組件 mil 大小 簡化 .html 引言 通過前面兩篇: Android 開發之旅:環境搭建及HelloWorld Android 開發之旅:HelloWorld項目的目錄結構 我們對android有了個大

Android開發之旅1環境搭建及HelloWorld

lan 及其 其它 ply 新項目 bsp 驗證 for 對話框 ——工欲善其事必先利其器 引言 本系列適合0基礎的人員,因為我就是從0開始的,此系列記錄我步入Android開發的一些經驗分享,望與君共勉!作為Android隊伍中的一個新人的

Android開發 adb命令提示Permission denied (轉)

模擬 ont lang rmi title fontsize fcm hbm ssi 如題:模擬器版本->android 7.1.1 遇到這樣的情況把模擬器root一下就好了:su root =============2017年4月3日20:57:33========

Android開發遇到的-----融雲2.8.+版本修改插件列表

rsa 圖片 項目需求 tex 顯示 根據 -- 移除 pre 簡介   融雲在2.8.+的時候,對輸入區域進行了重構,輸入區域整個為RongExtension,插件為RongExtension區域的Plugin模塊 List<IPluginModule&g

記錄Android開發一個小,佈局檔案TextView中新增onClick後,點選無效問題

自己寫東西的時候,在TextView上添加了onClick去增加點選事件,去跳轉另一個Activity,執行後結果點選無效,新增Toast,Toast也不顯示,程式碼如下: <TextView android:layout_width="wrap_content"

Android開發(2)資料儲存之一SharedPrefrences和檔案讀寫

一、資料儲存 本文主要講前兩種儲存方式,其中檔案讀寫只記錄Internal Storage方式 1. SharedPrefrences方式 輕量級NVP方式儲存,以XML的檔案方式儲存,適合少量資料的儲存。 NVP:Name/Value pair, 名稱/值 對。 2.

Android開發丶一步步教實現okhttp帶進度的列表下載檔案功能

大家好,我又回來了! 標題好像又起的不知所云,但是貌似也想不起更好的標題,話不多少,先來張效果圖 根據上圖就很明顯標題的含義了,每個列表標籤都有一個下載的按鈕,點選以下載對應的檔案,如果已下載則顯示“已下載”,反之顯示“點選下載”。 首先我們使用okhttp框架下載

Android開發小工具之Chrome Custom Tabs

參考文章 官方文件 官方原始碼 http://qq157755587.github.io/2016/08/12/custom-tabs-best-practices/ https://juejin.im/entry/586f089c61ff4b006d29f9c0 一

【 專欄 】- Android開發與進階從0到1構建Android專案

Android開發與進階:從0到1構建Android專案 介紹瞭如何使用新技術從零構建一個生產環境的Android專案,包括了架構搭建、Gradle的使用實踐、設計模式、依賴注入、資料層倉庫搭建、Data Binding實踐、後臺

Android開發遇到的

最近在開發中總會遇到各種坑,由於記憶力不好,先記錄在這裡。 以前看過的一些總結,基本上很齊全了: http://jcodecraeer.com/plus/view.php?aid=3773 https://www.zhihu.com/question/27140400 ht

android studio3.2一個 Failed to process resources, see aapt output above for details.

最近升級了android studio 3.2,然後我使用的引用是: api 'com.android.support:appcompat-v7:28.0.0' 然後就報錯了,編譯不通過: org.gradle.api.tasks.TaskExecution

Android開發常用開源框架圖片處理

1. 圖片載入,快取,處理 框架名稱 功能描述 一個強大的圖片下載與快取的庫 一個用於管理影象和他們使用的記憶體的庫 Glide 一個圖片載入和快取的庫,使用的App有:網易新聞 Andr

android開發(0)android studio的下載安裝與簡單使用 | sdk的安裝與編譯

ger 準備 開發環境 view 選擇 集成開發環境 alt 尋找 control android studio,簡稱AS,是集成開發環境,所謂集成,就是集編輯、編譯、調試、打包等於一體。簡單來說,通過AS,就可以開發出在android系統上運行的APP。 我使用的是mac

android開發(1)底部導航條的實現 | navigation tab

tom git 這一 cocoapod https android中 rip launcher href 底部導航條,在iOS中叫tabbar,在android中叫bottombar或bottom navigation,是一個常用的切換頁面的導航條。 同樣,如果有良好的第三

android開發(2)頁面的實現 | Fragment的創建與使用

界面 準備 導致 android開發 效果 found 之前 自動 -c APP中出現多個頁面再常見不過了。使用activity與fragment都能實現多頁面,這裏使用fragment來實現。延續“知音”這個APP的開發,之前已經創建了底部導航條與mainactivity