Android 圖片內存佔用過大?不存在的...

轉載請註明出處:http://blog.csdn.net/hjf_huangjinfu/article/details/79281829



概述

        Android 平臺的內存,一直都是比較珍貴的,防止內存佔用過多,再怎麼強調也不爲過的。目前絕大多數互聯網 App,都需要加載顯示很多圖片,如果方式不當,這些圖片會消耗掉大量內存,所以如何降低圖片的內存消耗,就比較重要了。


1、圖片數據

        想要研究圖片佔用內存,首先需要分析一下圖片數據。

        一張圖片是由衆多個像素點組成的。

        每一個像素點的數據,或者說每一個顏色都是由 三基色+透明度 組成的,每一個基色或者透明度,我們稱之爲一個通道

        每一個通道也有取值範圍,比如 8bit 的,就是 0-255,4bit 的,就是 0-16,它們的區別在於,取值範圍大的,色彩分辨率比較高,也就是顏色比較細膩。

        我們用 PictureData 表示圖片數據,用 PixelData 表示像素點數據的話,那麼結構是這樣的:

//圖片數據
public class PictureData {
    PixelData[] pixelData;
}
//每一個像素點的數據,每一個顏色都是由三基色+透明度組成的,每一個基色或者透明度,我們稱之爲一個通道
public class PixelData {
    byte[] alpha; //透明通道
    byte[] red;   //紅色通道
    byte[] green; //綠色通道
    byte[] blue;  //藍色通道
}

1.1、圖片文件格式

        上節分析了圖片數據的組成,這一節分析一下圖片文件的組織方式。每個圖片都是需要保存這些像素數據的,我們可以把一張圖片看成是一個二維矩陣,每一個像素點就是矩陣中的一個數據,我們可以簡單的按行從左到右,列從上到下的順序,依次存儲每個像素點數據,這樣就可以把一張圖片數據保存到文件,這就是 bmp 格式的圖片。其他常見的圖片格式還有 jpeg、png、webp 等等,其實它們都是文件格式。從像素點組成方式的維度來看,它們的區別就是像素點組成方式的不同,共同的目標都是壓縮數據,畢竟需要降低圖片傳輸成本,裏面涉及了一些壓縮算法,這裏不做討論。


1.2、像素數據以及格式

        上一節聚焦於衆多像素點如何組成圖片,這一節主要聚焦於每個像素點的內部表示。
        直觀來說就是,每個像素點是由多個通道組成的,那麼:

                第一,內部如何組織這些通道的數據。

                第二,每個通道的取值範圍。

        這就是像素格式,做 Android 開發的人可能都聽過 ARGB_8888 這個詞,這個詞就包含了上面提到的數據信息,ARGB 表示,透明度-紅色-綠色-藍色,這幾個通道的數據,按照這個順序排列,8888 表示,每個通道的數據用 8bit 來保存,也就是取值範圍爲 0-255。當然還有很多像素格式,Android 中常見的還有 RGB_565。其他格式的就不具體說明了。
參考鏈接:https://msdn.microsoft.com/en-us/library/windows/desktop/ee719797(v=vs.85).aspx


1.3、有損壓縮和無損壓縮

        上面提到了一個壓縮的概念,這一節簡單再介紹一下圖片壓縮的一些概念。第一個就是各種圖片格式,也就是衆多像素點數據如何組織的問題,比如第一種,把連續的顏色值一樣(各通道值都一樣)的像素點表達成(個數、顏色)的這種數據格式,就能壓縮不少空間。還有很多圖片壓縮方式,具體不做介紹。第二個就是大家常說的有損壓縮和無損壓縮,什麼概念呢,比如之前用 8bit 存儲紅色通道數據,所以有256種紅色,但是現在想降低大小,改成 4bit 存儲,所以就只有16種紅色,所以可表達的顏色數量就會變少,這種顏色通道數據丟失的壓縮方法,就叫做有損壓縮,顏色數據完整的壓縮方式叫做無損壓縮。這裏是兩個維度,文件格式維度和像素格式維度。圖片壓縮是不會改變像素點個數的。



2、Android圖片加載原理

2.1、圖片在內存中的表示以及空間計算

        不管圖片以什麼文件格式存儲,當它需要被使用的時候,它就要被加載到內存中,就需要解壓,把每一個像素點的每一個通道數據都存儲在內存中。至於如何存儲,就是由像素格式來確定的,比如 ARGB_8888,那麼就會使用4個字節,依次存儲 ARGB 這4個通道的數據,然後每個像素點依次存儲。
        所以,如何計算一張圖片在內存中需要佔用多少空間,先計算像素點,像素點個數=寬度*高度。然後計算每個像素點需要多少空間存儲,比如ARGB_8888就需要4個字節,那麼所需空間大小就是 空間(字節)=寬度*高度*4,比如RGB_565就需要2個字節,那麼所需空間大小就是 空間(字節)=寬度*高度*2。
        誤區一:圖片文件大小不是太大,只有幾十KB,不會佔用多少內存。錯,文件只有幾十KB,是因爲它內部色彩單純,壓縮率比較高,但是長寬都不變。我們App中有一個圖片鏈接,該圖只有110KB,看起來很無害的樣子,後臺就上傳了,然後它的尺寸是3240*1260,然後它又是 ARGB_8888 的圖片,所以計算一下,3240 * 1260 * 4 = 16329600 byte = 15.6M,哈哈,殺手級圖片。


2.2、圖片加載和顯示流程

        我們使用各種方式,把一個圖片從各種來源(res、assets、文件、url 等等),顯示到一個 ImageView 或者其他地方的時候,過程究竟是如何的呢?這一節來分析一下。
        加載並顯示圖片的流程大家都知道,BitmapFactory.decode....系列方法,拿到bitmap後,再顯示到控件上。
        圖片加載和顯示是2個不同的流程:
                加載流程,就是按照指定的參數,把圖片加載到內存中。
                顯示流程,就是把上一步加載進來的圖片數據,經過預處理,顯示到控件上面。
        BitmapFactory.Options 就是控制這兩個流程的參數,裏面類似於 inXxx 的屬性,就是控制加載流程的,比如 inPreferredConfig,類似於 outXxx 的屬性,就是控制顯示流程的,比如 outConfig。





3、如何降低內存佔用

        根據前面分析的結果,想要降低內存佔用,就需要控制 in 的過程,也就是控制圖片加載到內存中的解碼參數,當圖片數據加載到內存中後,不管你怎麼顯示,控件設置成10 * 10也好,只是繪製效果問題,原始圖片還是在內存中。如果以低尺寸加載,高尺寸繪製,就會造成圖片模糊的問題。

3.1、降低圖片尺寸

        前面我們知道,圖片在內存中的空間,是由尺寸和像素格式決定的,所以縮小尺寸可以降低圖片質量。因爲當圖片尺寸大於控件尺寸的時候,加載全尺寸圖片到內存中,沒有什麼好處。

        其實官方也是推薦我們這麼做的,https://developer.android.com/topic/performance/graphics/load-bitmap.html

        官方推薦的圖片降尺寸策略爲,找到一個最大的整數值 n,讓 inSampleSize = 2^n,保持 bitmapWidth / inSampleSize >= viewWidth; bitmapHeight / inSampleSize >= viewHeight;
        舉個例子,比如一個 ImageView 尺寸爲 400 * 200,一個 Bitmap 尺寸爲 900 * 500,那麼 inSize 的值爲 2。        

        現在的流行圖片加載庫,比如 Glide,都是支持這些功能的。

        注意點,看下面一段代碼,很常規的寫法。

<ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/icon_add_note_1"/>
靜態引用一個res目錄下的資源時,系統默認的加載參數中,inSampleSize=0,系統會把它當做1處理,下面是BitmapFactory.cpp中的doDecode的部分代碼:

sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// Correct a non-positive sampleSize.  sampleSize defaults to zero within the
// options object, which is strange.
if (sampleSize <= 0) {
    sampleSize = 1;
}
        所以,這種常規寫法,會把圖片全尺寸加載進來,如果你認爲apk就那麼大,圖片能有多大?那麼請看誤區一。

        圖片尺寸主要是 inSampleSize 來控制,具體使用方式,不做介紹。

        能使用代碼手動加載替代靜態資源引用的話,也會改善內存佔用。

        注意點:上圖有介紹說同一張圖片的數據是供多個繪製要求共用的,但是當你以不同的兩套尺寸加載同一張圖片到內存中時,會有2份以不同採樣率處理過的圖片數據,所以這裏要注意,假如你有十幾個尺寸各不相同的控件按照自己的尺寸加載同一張圖片時,哈哈,可能比全尺寸加載進來佔用的更多。


3.2、降低圖片質量

        現在來看影響圖片佔用內存的第二個因素,像素點格式。系統默認是使用ARGB_8888來接收像素數據。這部分有興趣可以查看Android源碼,主要集中在BitmapFactory.cpp中的doDecode函數,以及Chromium項目的Skia圖形庫。

        這個流程主要是通過inPreferredConfig來控制。

Bitmap.Config代碼如下:

public enum Config {
    /**
     * Each pixel is stored as a single translucency (alpha) channel.
     * This is very useful to efficiently store masks for instance.
     * No color information is stored.
     * With this configuration, each pixel requires 1 byte of memory.
     */
    ALPHA_8     (1),
    /**
     * Each pixel is stored on 2 bytes and only the RGB channels are
     * encoded: red is stored with 5 bits of precision (32 possible
     * values), green is stored with 6 bits of precision (64 possible
     * values) and blue is stored with 5 bits of precision.
     */
    RGB_565     (3),
    /**
     * @deprecated Because of the poor quality of this configuration,
     *             it is advised to use {@link #ARGB_8888} instead.
     */
    @Deprecated
    ARGB_4444   (4),
    /**
     * Each pixel is stored on 4 bytes. 
     */
    ARGB_8888   (5),
    /**
     * Each pixels is stored on 8 bytes. 
     */
    RGBA_F16    (6),
    HARDWARE    (7);
}

        縱觀Bitmap.Config所示,Android平臺就對外暴露了這幾種像素格式,想要降低畫質,就需要把ARGB_8888降低爲RGB_565。

        所以,你以爲事情就這麼簡單的結束了?

        誤區二:RGB_565不是你想用,想用就能用。加載策略不允許丟失透明度數據,比如jpeg,天然不含透明數據的格式,你用RGB_565完全沒問題。但是png、webp這種帶有透明度數據的格式,想要強制以RGB_565加載進來,是不行的。

        比如,這是Skia庫(chromium項目,Android在用)的webp解碼器部分源碼:

if (info) {
    // FIXME: Is N32 the right type?
    // Is unpremul the right type? Clients of SkCodec may assume it's the
    // best type, when Skia currently cannot draw unpremul (and raster is faster
    // with premul).
    *info = SkImageInfo::Make(features.width, features.height, kN32_SkColorType,
                              SkToBool(features.has_alpha) ? kUnpremul_SkAlphaType
                                                          : kOpaque_SkAlphaType);
}

    switch (dst.colorType()) {
        // Both byte orders are supported.
        case kBGRA_8888_SkColorType:
        case kRGBA_8888_SkColorType:
            return true;
        case kRGB_565_SkColorType:
            return src.alphaType() == kOpaque_SkAlphaType;
        default:
            return false;
    }

        大概意思就是如果該圖片有透明度數據,那麼就不可以使用RGB_565格式。
        但是,我就想用RGB_565來降低圖片質量怎麼辦,好辦,對於那些視覺上沒有透明度的圖片,使用 cwebp -noalpha 壓縮,丟掉透明度通道,不影響視覺。

        webp詳細信息:https://developers.google.com/speed/webp/

        png也可以通過處理,去掉alpha通道。



4、React Native 頁面圖片佔用內存過大問題

        我們的 App,有些用 React Native 編寫的頁面,佔用內存比較高,經過研究後發現,好多類似於上面那個殺手級的圖片存在於內存中,所以,要想辦法幹掉他們。

        怎麼幹,由於使用 React Native 的原生寫法,使用 <Image> 標籤,所以我們不好弄 BitmapFactory 來自定義加載。反正就是想辦法降尺寸或者降質量。

        在 ReactNative 0.46.4 版本後,React Native 提供了 resizeMethod 的屬性,來提供一個圖片裁剪接口。

        把 resizeMethod 設置爲 resize 後,把圖片降尺寸後再加載到內存中,就會減少很多內存開銷。

        網絡圖片,需要把自帶的 FrescoModule 的 配置項,也就是 ImagePipelineConfig 對象,把它的 downSampleEnable 設置爲true。


        在RN中使用 ListView 或者 FlatList 加載 Image 時,這個 ListView 並不是像 Android 中的 ListView 一樣,有回收機制,RN 的 ListView,運行時實現爲 ReactScrollView 嵌套 ReactViewGroup,所以,圖片不會被回收。目前版本0.53.0,在實現上面有缺陷,RN 內部會引用所有 View,即使它不可見,也確實它不在 View 樹中,但是 RN 內部有引用,沒法釋放。ReactViewGroup 中有一個 mAllChildren ,會引用所有 View。可以考慮使用自己包裹的 Android 原生 ListView 或者 RecyclerView 。



5、res 目錄下面的各種分辨率的 drawable 目錄陷阱

        圖片放到不同分辨率目錄中,從內存層面的影響有多大呢?很大。

        比如,你把一個圖片放在 mdpi 目錄中,然後靜態引用(在 xml 中以src引用)了該資源,比如你的機器 dpi 是 420 的,那麼,恭喜你,你中槍了。

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;
    }
}

// Scale is necessary due to density differences.
if (scale != 1.0f) {
    willScale = true;
    scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
    scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
        看看,因爲 mdpi = 160, 所以 420 / 160 = 2.6(近似)。所以,你的圖片尺寸加載到內存中,長寬都會被擴大到 2.6 倍,所以,內存佔用是 2.6 * 2.6 = 6.76 倍。

        因爲系統需要做分辨率適配,所以,這是顯而易見的。

        該是什麼分辨率的圖片,就要放到指定的分辨率目錄下。

        最佳實踐就是準備一套高分辨率圖片,放到高分辨率目錄下,然後向做降尺寸操作。


6、Glide緩存問題

        一張大圖,比如啓動頁的歡迎圖片,一般尺寸都是跟手機分辨率差不多,比如是 1920 * 1080,假如是 ARGB_8888 的,那麼它在內存中大小大約爲 7.92M,但是業務要求如此,不能降尺寸,也不能去掉透明度。但是使用Glide加載後,你會把它放到 Glide 的緩存裏面,但是這個緩存是強引用的,那麼,哈哈,如果一直觸碰不到它的緩存臨界區,也就是說,Glide 不會清理它的緩存,然後這種過眼即逝的高清大圖,就長期霸佔着你的內存,但是你卻基本上不會再看到它了,多麼痛的領悟。


結論

        總之,遇到圖片佔用內存過大的問題,都可以想辦法解決了,本質方案就是降尺寸或者降質量。對症下藥。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章