Android Bitmap 高效加載以及內(nèi)存管理

Handling bitmaps

https://youtu.be/HY9aaXHx8yA

在 Android 應用中加載位圖很棘手主要有以下幾個原因:

  • 位圖很容易耗盡應用程序的內(nèi)存。例如,Pixel 手機上的相機拍攝的照片最高可達 4048 x 3036 像素(1200 萬像素)。如果使用的位圖配置是ARGB_8888,則默認為 Android 2.3(API 級別 9)及更高版本,加載單張照片進入內(nèi)存需要大約 48 MB 的內(nèi)存(4048 * 3036 * 4 字節(jié))。如此大的內(nèi)存需求可以立即耗盡應用程序可用的所有內(nèi)存。

  • 在 UI 線程上加載位圖會降低應用程序的性能,導致響應速度慢甚至 ANR。因此,在使用位圖時適當?shù)毓芾砭€程非常重要。

  • 如果你的應用程序正在將多個位圖加載到內(nèi)存中,則需要有技巧地管理內(nèi)存和磁盤緩存。否則,應用程序 UI 的響應性和流暢性可能會受到影響。

多數(shù)情況下建議使用 Glide 庫來獲取,解碼和顯示應用中的位圖。其他受歡迎的圖像加載庫還有 Square 的 Picasso 和 Facebook 的 Fresco。這些庫簡化了與 Android 上的位圖和其他類型圖像相關的大多數(shù)復雜任務。你還可以選擇直接使用 Android 框架中內(nèi)置的低級 API。有關執(zhí)行此操作的詳細信息,請參閱 Loading Large Bitmaps EfficientlyCaching Bitmaps,和 Managing Bitmap Memory

一、高效加載大位圖 — Loading Large Bitmaps Efficiently

下面介紹如何通過在內(nèi)存中加載較小的子采樣版本來解碼大型位圖,而不會超出每個應用程序的內(nèi)存限制。

1.1 讀取位圖尺寸和類型 — Read Bitmap Dimensions and Type

BitmapFactory 類提供了幾種解碼方法(decodeByteArray()decodeFile()decodeResource() 等),用于從各種源創(chuàng)建位圖。根據(jù)圖像數(shù)據(jù)源選擇最合適的解碼方法。這些方法嘗試為構(gòu)造的位圖分配內(nèi)存,因此很容易導致 OutOfMemory 異常。每種類型的解碼方法都有其他簽名,可讓你通過 BitmapFactory.Options 類指定解碼選項。解碼時將 inJustDecodeBounds 屬性設置為 true 可避免內(nèi)存分配,為位圖對象返回 null 但設置 outWidth,outHeight 和 outMimeType。此技術允許你在構(gòu)造(和內(nèi)存分配)位圖之前讀取圖像數(shù)據(jù)的尺寸和類型。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

要避免 java.lang.OutOfMemory 異常,請在解碼之前檢查位圖的尺寸,除非你完全信任該源為你提供可預測大小的圖像數(shù)據(jù),這些數(shù)據(jù)可以輕松地放入可用內(nèi)存中。

1.2 將縮小版本加載到內(nèi)存中 — Load a Scaled Down Version into Memory

既然圖像尺寸已知,它們可用于決定是否應將完整圖像加載到內(nèi)存中,或者是否應加載子采樣版本。以下是需要考慮的一些因素:

  • 估計在內(nèi)存中加載完整圖像的內(nèi)存使用情況。

  • 根據(jù)應用程序的其他內(nèi)存要求,你愿意加載此圖片的內(nèi)存量。

  • 要加載圖像的目標 ImageView 或 UI 組件的尺寸。

  • 當前設備的屏幕尺寸和密度。

例如,如果最終將在 ImageView 中以 128 x 96 像素的縮略圖顯示,則不值得將 1024 x 768 像素圖像加載到內(nèi)存中。

要告訴解碼器對圖像進行子采樣,將較小的版本加載到內(nèi)存中,請在 BitmapFactory.Options 對象中將 inSampleSize 設置為 true。例如,使用 inSampleSize 為 4 解碼的分辨率為 2048 x 1536 的圖像會產(chǎn)生大約 512 x 384 的位圖。將其加載到內(nèi)存中對于完整圖像使用 0.75 MB 而不是 12 MB(假設 ARGB_8888 的位圖配置)。以下是一種根據(jù)目標寬度和高度計算樣本大小值的方法,該值為 2 的冪:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

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

        final int halfHeight = height / 2;
        final int halfWidth = 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;
}

注意:計算 2 的冪是因為解碼器使用最終值,通過向下舍入到最接近的 2 的冪,根據(jù) inSampleSize 文檔。

要使用此方法,首先將 inJustDecodeBounds 設置為 true 進行解碼,傳遞選項然后使用新的 inSampleSize 值再次解碼,并將 inJustDecodeBounds 設置為 false

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

此方法可以輕松地將任意大尺寸的位圖加載到顯示 100 x 100 像素縮略圖的 ImageView 中,如以下示例代碼所示:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

你可以按照類似的過程解碼來自其他來源的位圖,方法是根據(jù)需要替換相應的 BitmapFactory.decode* 方法。

二、管理位圖內(nèi)存 — Managing Bitmap Memory

除了緩存位圖中描述的步驟之外,你還可以執(zhí)行一些特定操作來促進垃圾回收和位圖重用。推薦的策略取決于你的目標 Android 版本。以下內(nèi)容將向你展示如何設計應用程序以在不同版本的 Android 中高效工作。

為了之后更好的說明,我們先看看 Android 對位圖內(nèi)存管理的演變過程:

  • 在 Android 2.2(API 級別 8)及更低版本中,當垃圾回收發(fā)生時,你的應用程序的線程會停止。這會導致延遲,從而降低性能。Android 2.3 添加了并發(fā)垃圾回收,這意味著當不再引用位圖時很快就會回收內(nèi)存。

  • 在 Android 2.3.3(API 級別 10)及更低版本中,位圖的像素數(shù)據(jù)存儲在 native 內(nèi)存中。它與位圖本身是分開的,位圖本身存儲在 Dalvik 堆中。native 內(nèi)存中的像素數(shù)據(jù)不會以可預測的方式釋放,這可能導致應用程序短暫超出其內(nèi)存限制并崩潰。從 Android 3.0(API 級別 11)到 Android 7.1(API 級別 25),像素數(shù)據(jù)與相關位圖一起存儲在 Dalvik 堆上。在 Android 8.0(API 級別 26)及更高版本中,位圖像素數(shù)據(jù)存儲在 native 堆中。

以下部分介紹如何針對不同的 Android 版本優(yōu)化位圖內(nèi)存管理。

2.1 在 Android 2.3.3 和更低版本上管理內(nèi)存 — Manage Memory on Android 2.3.3 and Lower

在 Android 2.3.3(API 級別 10)及更低版本上,建議使用 recycle()。如果你在應用中顯示大量位圖數(shù)據(jù),則可能會遇到 OutOfMemoryError 錯誤。recycle() 方法允許應用程序盡快回收內(nèi)存。

警告:只有在確定不再使用位圖時才應使用 recycle()。如果你調(diào)用 recycle() 并稍后嘗試繪制該位圖,將收到錯誤:"Canvas: trying to use a recycled bitmap"

以下代碼片段給出了調(diào)用 recycle() 的示例。它使用引用計數(shù)(在變量 mDisplayRefCount 和 mCacheRefCount 中)來跟蹤當前位圖是正在顯示還是在緩存中。當滿足以下條件時,代碼會回收位圖:

  • mDisplayRefCount 和 mCacheRefCount 的引用計數(shù)均為 0。

  • 位圖不為 null 且尚未回收。

private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {
            mDisplayRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

private synchronized void checkState() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

2.2 在 Android 3.0 及更高版本上管理內(nèi)存 — Manage Memory on Android 3.0 and Higher

Android 3.0(API 級別 11)引入了 BitmapFactory.Options.inBitmap 字段。如果設置了此選項,則使用 Options 對象的解碼方法將在加載內(nèi)容時嘗試重用現(xiàn)有位圖。這意味著重用了位圖的內(nèi)存,從而提高了性能,并省去了內(nèi)存分配和回收的步驟。但是,使用 inBitmap 有一些限制。比如說,在 Android 4.4(API 級別 19)之前,僅支持相同大小的位圖。

2.2.1 保存位圖供以后使用 — Save a bitmap for later use

以下代碼段演示了如何存儲現(xiàn)有位圖以便以后使用。當應用程序在 Android 3.0 或更高版本上運行并且位圖從 LruCache 中移出時,可以把位圖的軟引用放置在 HashSet 中,以便稍后可以在 inBitmap 中重用:

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}
2.2.2 使用現(xiàn)有位圖 — Use an existing bitmap

在正在運行的應用程序中,decoder 方法檢查是否存在可以使用的現(xiàn)有位圖。例如:

public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    ...
    BitmapFactory.decodeFile(filename, options);
    ...

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

以下代碼段顯示了上述代碼段中調(diào)用的 addInBitmapOptions() 方法。它查找現(xiàn)有位圖以設置為 inBitmap 的值。請注意,如果找到合適的匹配項,此方法僅為 inBitmap 設置一個值(你永遠不應該假設一定能找到匹配項):

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found, set it as the value of
            // inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = mReusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();

                if (null != item && item.isMutable()) {
                    // Check to see it the item can be used for inBitmap.
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;

                        // Remove from reusable set so it can't be used again.
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

最后,使用以下方法確定候選位圖是否滿足用于 inBitmap 的大小標準:

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
static int getBytesPerPixel(Config config) {
    if (config == Config.ARGB_8888) {
        return 4;
    } else if (config == Config.RGB_565) {
        return 2;
    } else if (config == Config.ARGB_4444) {
        return 2;
    } else if (config == Config.ALPHA_8) {
        return 1;
    }
    return 1;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內(nèi)容