Android圖片載入記憶體佔用分析

cc復CC發表於2017-12-13

作為一名Android開發人員,你見得最多的大概就是res/drawable-[density]/ 資料夾了,現在又大概多了 res/mipmap-[density]/ 資料夾,這些資料夾通常用來存放圖片資原始檔,大家可能再熟悉不過了,現在我問你,一張大小為376.16K的480x800且位數為8的圖片放在res/drawable-xxhdpi/ 資料夾下,在解析度為1920*1080的手機上這張圖片佔用的記憶體是多少?

#1 概念釐清 如果對此比較有了解的小傻逼們,後面其實不需要看了,純粹來掃一下盲,在正式分析之前,先來釐清一下相關的概念。

  • 1.1螢幕尺寸:按螢幕對角測量的實際物理尺寸,例如5.5英寸。Android 將所有實際螢幕尺寸分組為四種通用尺寸:小、 正常、大和超大;
  • 1.2解析度:螢幕上物理畫素的總數,新增對多種螢幕的支援時, 應用不會直接使用解析度,而只應關注通用尺寸和密度組指定的螢幕尺寸及密度
  • 1.3螢幕密度:螢幕物理區域中的畫素量,通常稱為 dpi(每英寸點數)。螢幕密度越低在給定物理區域的畫素就會較少。Android 將所有螢幕密度分為六組通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高);
  • 1.4密度無關畫素 (dp):在定義 UI 佈局時應使用的虛擬畫素單位。密度無關畫素等於 160 dpi 螢幕上的一個物理畫素,這是系統為mdpi(中)密度螢幕假設的基線密度。在執行時,系統根據使用中螢幕的實際密度按需要以透明方式處理dp單位的任何縮放 。dp單位轉換為螢幕畫素很簡單: px = dp * (dpi / 160)。 例如,在 240 dpi螢幕上,1 dp等於1.5 物理畫素。

對於我們的分析比較重要的就是螢幕密度。

#2 螢幕密度(dpi)對應關係

通用密度 ldpi mdpi(基線密度) hdpi xhdpi xxhdpi xxxhdpi
描述 超高 超超高 超超超高
大小(單位dpi) 120 160 240 320 480 640
縮放係數 0.75 1 1.5 2 3 4

六種通用密度之間遵循 3:4:6:8:12:16 的縮放比率,要注意的一點是xxxhdpi僅限啟動器圖示

#3 具體分析實現程式碼 程式碼很簡單,就是用一個ImageView包含一張背景圖片,然後通過轉換為Bitmap檢視佔用記憶體大小。 佈局檔案activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xishuang.imagesizetest.MainActivity">

    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/bg2" />

</FrameLayout>
複製程式碼

佈局檔案,就是一個ImageView控制元件,包含一張背景圖。

MainAcivity.java

private void printBitmapSize(ImageView imageView) {
        Drawable drawable = imageView.getDrawable();
        if (drawable != null) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            Bitmap bitmap = bitmapDrawable.getBitmap();
            //API 19
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
                size = bitmap.getAllocationByteCount();
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1){
                //API 12
                size = bitmap.getByteCount();
            } else {
                //earlier version
                size = bitmap.getRowBytes() * bitmap.getHeight();
            }
            Log.d(TAG, " size = " + size);
        } else {
            Log.d(TAG, "Drawable is null !");
        }
    }
複製程式碼

getAllocationByteCount()方法可以獲取圖片的實際佔用記憶體大小,在此之前得介紹一種特殊的res/drawable-[density]/資料夾,就是res/drawable-nodpi/,不管當前螢幕的密度如何,系統都不會縮放以此限定符標記的資源。意思就是在這個資料夾中的圖片按原樣進行展示,不會像其它的res/drawable-[density]/那樣改變檔案的大小,我們就以此為基準進行分析。

#4 圖片的實際記憶體佔用 以實際例子作為分析,把一張大小為376.16K的480x800且位數為8的圖片為例 圖片的畫素總數 480x800 = 384000 先使用壓縮前的圖片為例,分析圖片佔用的記憶體大小並列印出來

壓縮前磁碟佔用大小
壓縮前記憶體大小
對圖片進行壓縮後進行同樣得操作

壓縮後磁碟佔用大小
壓縮後記憶體佔用大小

很明顯,壓縮前後記憶體的佔用大小同樣為1536000(Byte),說明圖片的磁碟佔用大小與圖片的記憶體或視訊記憶體佔用沒有必然關係。從而說明壓縮圖片可以減少我們得apk大小,但是記憶體的佔用是不會變小的,那麼圖片的記憶體佔用與什麼有關係呢?繼續。。。

圖片的記憶體佔用大小為1536000(Byte),而圖片的原始圖片畫素總數為384000,一眼看過去好像沒啥關係,但是真相是384000 * 4 = 1536000(Byte),原始圖片尺寸大小與最終的記憶體佔用大小呈倍數的關係,所以在這裡與記憶體佔用大小有直接關係的就是原始圖片尺寸大小(例如:480x800),道理我都懂,但是倍數關係是從哪裡來的呢,這就要談論到Bitmap的畫素格式了。

Android系統支援4種格式的畫素格式,原始碼在Bitmap.Config中

/**
     * 可用的bitmap配置, 一個bitmap配置描述的是每個畫素的儲存格式,這將會影響到圖片的質量 (顏色深
     * 度) 以及顯示透明/半透明顏色的能力
     */
    public enum Config {
        // 這些列舉中的值必須要與Skia影象引擎的SkBitmap.h中對應值一一對應

        /**
         * 只有一個alpha通道 
         * 每個畫素佔1個位元組
         */
        ALPHA_8     (1),

        /**
         *每個畫素佔用2個位元組,只有RGB 3個通道,沒有alpha 通道
         * 紅色的精度是5 bits, 綠色精度是6 bits,藍色精度是5
         */
        RGB_565     (3),

        /**
         * 每個畫素佔用2個位元組. 
         * (雖然佔用記憶體只有 ARGB8888 的一半,不過已經被官方嫌棄)
         */
        @Deprecated
        ARGB_4444   (4),

        /**
         * 每個畫素佔用4個位元組. 每個通道 (RGB的3個通道和alpha
         * 的1個透明度通道) 的進度是8bit (256個可能值)
         * 這種配置是最靈活的, 質量最好,儘量使用這種格式.
         */
        ARGB_8888   (5);
    }
複製程式碼

由於官方預設使用ARGB_8888格式,導致圖片的每個畫素會佔用4個Byte大小,所以最終的圖片佔用記憶體大小就是畫素總數*畫素格式,放到例子裡頭就是384000 * 4 = 1536000(Byte),成功接上去了,哈哈哈。。。

小結論:圖片的直接記憶體佔用和圖片的畫素總數和系統的畫素格式相關,與磁碟儲存的圖片大小無關,其實與磁碟儲存的圖片位數也無關。

#5 Android對在res/drawable-[density]/ 資料夾中圖片進行的騷操作 前面提到的圖片實際佔用記憶體大小,是很合理的,但是圖片是放置在

nodpi.png
前面也已經提到過res/drawable-nodpi/資料夾,在這個資料夾中的圖片按原樣進行展示,不會像其它的res/drawable-[density]/那樣改變檔案的大小,類似於從SD卡或者網路直接載入一張圖片。 但是如果把圖片放在其它的res/drawable-[density]/ 資料夾中的話,事情就會變得有些不一樣了,系統會根據手機的螢幕密度來縮放對應資料夾中的圖片。

下面就是測試結果,測試手機為360 vizza,手機解析度為1920*1080,螢幕密度為480dpi,測試圖片為480x800的圖片。 先把圖片放置drawable-ldpi中看佔用記憶體大小,然後依次類比,得出最終的對比資料。

資料夾 資料夾dpi size(Byte)
drawable-ldpi 120 24576000
drawable-mdpi 160 13824000
drawable-hdpi 240 6144000
drawable-xhdpi 320 3456000
drawable-xxhdpi 480 1536000
drawable-xxxhdpi 640 864000

看到這個結果先不要慌,穩住,我們能贏...

經過前面的分析,我們知道在res/drawable-nodpi/下圖片的佔用記憶體為1536000(Byte),發現沒有,我加粗的那一行資料中,也就在當圖片放置在res/drawable-xxhdpi/資料夾下面時,圖片所佔用的記憶體也是1536000(Byte),而我們得測試機的螢幕密度就是480dpi,說明在對應螢幕密度的檔案下獲取圖片時記憶體佔用不會有變化。 而在把圖片放置其他對應dpi資料夾下時,會出現圖片記憶體佔用出現不同程度的縮放,我們稱與手機螢幕密度一致的資料夾稱之為目標資料夾,當圖片放置的資料夾對應密度比目標資料夾越小時,圖片佔用記憶體越大,當圖片放置的資料夾對應密度比目標資料夾越大時,圖片佔用記憶體越小。

還記得這個表嗎

通用密度 ldpi mdpi(基線密度) hdpi xhdpi xxhdpi xxxhdpi
描述 超高 超超高 超超超高
大小(單位dpi) 120 160 240 320 480 640
縮放係數 0.75 1 1.5 2 3 4

六種通用密度之間遵循 3:4:6:8:12:16 的縮放比率,記憶體佔用縮放的祕密其實就是在這個縮放比率當中,最終的圖片佔用記憶體大小為: 圖片最終記憶體=圖片原始記憶體 * (手機螢幕密度/資源圖片檔案密度) ^ 2 其實就是圖片寬和高都按縮放比率進行對應的縮放。

舉個栗子: 當圖片放置在res/drawable-ldpi/資料夾下時,圖片記憶體為1536000*(480/120)^2=153600016=24576000(Byte); 當圖片放置在res/drawable-xxhdpi/資料夾下時,圖片記憶體為1536000(480/480)^2=15360001=1536000(Byte); 當圖片放置在res/drawable-xxxhdpi/資料夾下時,圖片記憶體為1536000(480/640)^2=1536000*0.5625=864000(Byte); 注:res/drawable-xxxhdpi/資料夾官方建議只能放啟動圖示,這裡只是為了測試才放置測試圖片。 對比一下上表對比資料,都一一對應,說明是ok的。

然後最終結論就是 1、圖片的直接記憶體佔用和圖片的畫素總數和系統的畫素格式相關,與磁碟儲存的圖片大小無關,其實與磁碟儲存的圖片位數也無關,圖片的直接記憶體佔用大小為:畫素總數 * 畫素的格式(畫素的格式其實就是確定了每個畫素佔用的位元組數) 2、圖片放置在res/drawable-[density]/ 資料夾中時,圖片佔用記憶體大小為:圖片最終記憶體 = 圖片原始記憶體 * (手機螢幕密度/資源圖片檔案密度) ^ 2

相關文章