主頁 > 移動端開發 > Android Bitmap的使用及優化

Android Bitmap的使用及優化

2021-06-09 20:11:31 移動端開發

Bitmap記憶體模型

  • 在 Android 2.2(API 8)及更低版本上,當發生垃圾回收時,應用的執行緒會停止(stop the world),這會導致延遲,從而降低性能,Android 2.3 添加了并發GC功能,這意味著系統不再參考位圖后,很快就會回收記憶體,
  • 在 Android 2.3.3(API 10)及更低版本上,bitmap 的像素資料存盤在 native 記憶體(native memeory)中,它與存盤在 Dalvik 堆中的 bitmap 物件本身是分開的,native 記憶體中的像素資料并不以可預測的方式釋放,可能會導致應用短暫超出其記憶體限制并崩潰,
  • 從 Android 3.0(API 11)到 Android 7.1(API 級別 25),像素資料會與關聯的 bitmap 物件一起存盤在 Dalvik 堆上,因此其 bitmap 的使用的記憶體會隨著 bitmap 物件一起回收,
  • 在 Android 8.0(API 26)及更高版本中,位影像素資料存盤在native堆(native heap)中,當然,盡管位影像素資料又放回了 native 堆中,但其會跟隨 Java 物件的釋放而被釋放,

無論是 Api 26 前還是之后的回收實作,釋放 Native 層的 Bitmap 物件的思想都是去監聽 Java 層的 Bitmap 是否被釋放,一旦當 Java 層的 Bitmap 物件被釋放則立即去釋放 Native 層的 Bitmap ,只不過 Api 26 以前是基于 Java 的 GC 機制,而 Api 26 后是注冊 native 的 Finalizer 方法,更詳細的分析可查看: 圖形影像處理 - 我們所不知道的 Bitmap,

BitmapFactory.Options

BitmapFactory.Options 是 BitmapFactory 從不同的輸入源中創建 Bitmap 物件的控制引數,

  • inBitmap
      Android 3.0 (API level 11) 引入了 BitmapFactory.Options.inBitmap欄位
      如果設定了這個值,則使用了這個 Options 物件的 decode 方法在 decode 時將會嘗試去復用 bitmap,如果失敗了,將會拋出java.lang.IllegalArgumentException例外,對于被復用的 bitmap 要求其是可修改的(mutable),并且對于被復用的 bitmap 將會保持其可修改的屬性,即使 decode 的資源將會導致 bitmap 變成不可修改的(immutable),由于上述的限制存在,因此可能導致 decode 失敗,因此不應該假定復用的 bitmap 是始終有效的,通過 decode 回傳的 bitmap,檢查其 inBitmap 欄位可以確定 bitmap 是否被復用了,
      從 KITKAT 版本開始,BitmapFactory 可以復用任何支持修改并且其getAllocationByteCount()大于等于要解碼資源的getByteCount() 的bitmap,
      在 KITKAT 版本之前,對于要復用的 bitmap 還存在其他限制:
    • 只支持jpegpng格式的圖片
    • 復用的 bitmap 其大小要與 decode 得到的 bitmap 大小一致,并且其inSampleSize欄位設定為1,也就是不支持采樣,
    • 復用的 bitmap 的 android.graphics.Bitmap.Config 將會覆寫設定的inPreferredConfig
@Nullable
public static Bitmap decodeFile(@NonNull String pathName) {
    Bitmap bitmap;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(pathName, options);
    options.inJustDecodeBounds = false;
    options.inSampleSize = 1;
    Bitmap inBitmap = AndroidBitmapPool.getInstance().get(options.outWidth, options.outHeight, options.inPreferredConfig);
    try {
        // 判斷是否可以使用 inBitmap,因為 inBitmap 在不同 Android 版本存在一些不同的限制
        if (inBitmap != null && Util.canUseInBitmap(inBitmap, options)) {
            // 復用需要把可修改的開關打開
            options.inMutable = true;
            options.inBitmap = inBitmap;
        } else {
            AndroidBitmapPool.getInstance().putBitmap(inBitmap);
        }
        bitmap = BitmapFactory.decodeFile(pathName, options);
        // 檢查是否復用成功
        if (bitmap == options.inBitmap) {
            Log.i(TAG, "decodeFile: inBitmap reuse successfully");
        }
    } catch (Exception e) {
        Log.e(TAG, "decodeFile", e);
        bitmap = BitmapFactory.decodeFile(pathName);
    }
    return bitmap;
}

public static boolean canUseInBitmap(@NonNull Bitmap inBitmap, @NonNull BitmapFactory.Options options) {
    //{@link android.graphics.BitmapFactory.Options.inBitmap} prior to KITKAT has some constraints
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        int width = options.outWidth / options.inSampleSize;
        int height = options.outHeight / options.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(inBitmap.getConfig());
        int inBitmapByteCount = getBitmapByteSize(inBitmap);
        return inBitmapByteCount >= byteCount;
    }

    return options.inSampleSize == 1 && options.outWidth == inBitmap.getWidth() && options.outHeight == inBitmap.getHeight();
}
  • inMutable
      如果設定了這個值,那么 decode 方法將會回傳一個可修改的 bitmap 物件,這個屬性不能與inPreferredConfig設為android.graphics.Bitmap.Config#HARDWARE時候一同設定,因為硬體位圖是不可變的,

  • inJustDecodeBounds
      如果設定了這個值,那么 decode 將會回傳 null,即 bitmap 不會被加載進記憶體,但是對于 Options 的out*欄位將會被設定,如outWidthoutHeightoutMimeType,這對于只想知道圖片寬高資訊非常有用,

  • inSampleSize
      圖片采樣的控制選項,當其值大于1時便會進行下采樣,通過這個標志位,在加載圖片時可有效節省記憶體,需要注意的是,這個值必須是2的冪次方,如果不是,將向下舍入為最接近的2的冪次方的值(根據實際測驗,inSampleSize并非是2的冪次方,測驗環境為 Android 10,MIUI 12 Xiaomi 9Pro, 在原始碼 BitmapFactory.cpp 中也沒有找到相關的代碼),設定 inSampleSize 之后,解碼得到的 bitmap 的寬、高都會縮小 inSampleSize 倍,如inSampleSize = 4 ,那么寬和高都會變為原來的1/4,整個大小會變為原來的1/16,對于 inSampleSize 的確定,在Loading Large Bitmaps Efficiently給出了示例,

  inSampleSize測驗實體

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(IMAGE_PATH, options)
Log.i(TAG, "width = ${options.outWidth}, height = ${options.outHeight}, mimeType = ${options.outMimeType}")
val imageWidth = options.outWidth
val imageHeight = options.outHeight
options.inJustDecodeBounds = false
for (i in 1 until 6) {
    options.inSampleSize = i
    val bitmap = BitmapFactory.decodeFile(IMAGE_PATH, options)
    Log.i(TAG, "bitmap width = ${bitmap.width}, height = ${bitmap.height}, width for inSampleSize = ${imageWidth / bitmap.width}, height for inSampleSize = ${imageHeight / bitmap.height}")
}
/*
width = 4000, height = 3000, mimeType = image/jpeg
bitmap width = 4000, height = 3000, width for inSampleSize = 1, height for inSampleSize = 1
bitmap width = 2000, height = 1500, width for inSampleSize = 2, height for inSampleSize = 2
bitmap width = 1333, height = 1000, width for inSampleSize = 3, height for inSampleSize = 3
bitmap width = 1000, height = 750, width for inSampleSize = 4, height for inSampleSize = 4
bitmap width = 800, height = 600, width for inSampleSize = 5, height for inSampleSize = 5
*/

  確定 inSampleSize 的大小

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height > reqHeight || width > reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}
  • inPreferredConfig
      設定解碼圖片的像素格式,默認使用ARGB_8888進行解碼,對于不同的配置,其每個像素需要的位元組數也不一樣,通常在不需要 alpha 通道的場景下,選擇RGB_565進行解碼,這樣能比選擇ARGB_8888節省一半的記憶體,

ALPHA_8 -> 1個位元組
RGB_565 -> 2個位元組(每個像素需要16個bit來表示)
ARGB_4444 -> 2個位元組
RGBA_F16 -> 8個位元組
ARGB_8888 -> 4個位元組

  • inDensity
      圖片所在drawable檔案夾對應的密度,當這個值為0時,decodeResource會根據資源所在drawable檔案夾填充這個值,各檔案夾對應的 density 關系如下:
檔案夾 density
drawable 0
ldpi 120
mdpi 160
hdpi 240
xhdpi 320
xxhdpi 480
xxxhdpi 640

  將圖片放入默認 drawable 檔案夾(不指定解析度),則最侄訓使用默認的 Density(DisplayMetrics.DENSITY_DEFAULT=160)

  • inTargetDensity
      bitmap 將會繪制到的目標像素密度,也就是螢屏密度,這個值通常跟inDensityinScaled配合使用,來決定是否縮放以及如何縮放 bitmap 的大小,當這個值為0時,decodeResource會根據Resources物件的DisplayMetrics來設定其值,

  • inScreenDensity

  • inScaled
      當被設定為 true 時,如果inDensityinTargetDensity都不為0,那么加載的 bitmap 會被縮放到符合inTargetDensity的值,.9圖不受這個標志位的影響,始侄訓被縮放,

Bitmap 記憶體占用計算

計算公式:

(width / inSampleSize * inTargetDensity / inDensity) * (height / inSampleSize * inTargetDensity / inDensity) * bytesPerPixel

  其中bytesPerPixel的值根據解碼圖片傳入的 Bitmap.Config 決定,可參考inPreferredConfig,如果不是 drawable 檔案夾下的資源的話,計算公式中 inTargetDensity / inDensity 當作1來處理,也就是不需要理會inTargetDensityinDensity導致的縮放影響,
  對于 bitmap 的記憶體占用大小,可以通過getByteCount方法獲取,在 Api 19 (Build.VERSION_CODES#KITKAT)及以后,新增了一個方法getAllocationByteCount,其表示分配給 bitmap 的記憶體大小,這個值大于等于getByteCount的數值,一般情況下,二者的回傳值相當,當 bitmap 復用的時候,則可能大于getByteCount的值,

支持解碼的圖片格式

注:對于 BitmapRegionDecoder 只支持 JPEG 和 PNG 格式的圖片

Format Encoder Decoder Details File Types Container Formats
BMP YES BMP (.bmp)
GIF YES GIF (.gif)
JPEG YES YES Base+progressive JPEG (.jpg)
PNG YES YES PNG (.png)
WebP Android 4.0+ Lossless: Android 10+ Transparency: Android 4.2.1+ Android 4.0+ Lossless: Android 4.2.1+ Transparency: Android 4.2.1+ Lossless encoding can be achieved on Android 10 using a quality of 100. WebP (.webp)
HEIF Android 8.0+ HEIF (.heic; .heif)

Bitmap 記憶體優化

   Bitmap 在應用中一般是導致 OOM 的幾大原因之一,如何減少解碼圖片導致的 OOM 及 Bitmap 的創建回收導致的記憶體抖動就顯得尤為重要,Bitmap 記憶體優化一般有以下幾個手段:

  • 使用Options.inSampleSize對圖片進行采樣,一般圖片的寬高都比我們顯示圖片的區域大很多,因此我們不必以原圖尺寸解碼圖片,通過采樣演算法,計算一個合理的采樣值,在解碼時對圖片進行下采樣,
    可參考Glide Downsampler
  • 使用Options.inBitmap對圖片進行復用,圖片復用有兩個好處,一個是加快圖片解碼速度,減少 Bitmap 創建耗時;另一個則是減少頻繁申請和銷毀 Bitmap 導致的記憶體抖動,在實際使用中,可建立 BitmapPool,每次需要使用 Bitmap 時,從 BitmapPool 申請符合要求的 Bitmap 記憶體,當 Bitmap 不需要使用的,放回 BitmapPool,詳細實作可參考Glide BitmapPool,
  • 對于不需要 alpha 通道的圖片, Options.inPreferredConfig可選擇Bitmap.Config.RGB_565,相比較于默認的Bitmap.Config.ARGB_8888,一個像素只需要兩個位元組,整體記憶體可節省一半,
  • 建立 Bitmap 記憶體快取,對于已經解碼的圖片,當下次需要再次使用時,可從記憶體快取中,直接取出,減少二次解碼的耗時,詳細實作可參考Glide MemoryCache

參考鏈接

  • Loading Large Bitmaps Efficiently
  • Caching Bitmaps
  • Managing Bitmap Memory
  • Android DisplayingBitmaps Sample
  • Glide bitmap_recycle

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286123.html

標籤:Android

上一篇:flutter 實作 有洗掉影片的 listview

下一篇:實體 -自定義繪制滑動解鎖

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more