Android性能優化:談談Bitmap的內存管理與優化

最近除了忙着項目開發上的事情,還有就是準備我的畢業論文,有一小段時間沒寫博客了,今晚難得想總結一下,剛好又有一點時間,於是湊合着來一篇,好了,嘮叨話不多說,直接入正題。從事Android移動端的開發以來,想必是經常要與內存問題打交道的,說到Android開發中遇到的內存問題,像Bitmap這種吃內存的大戶稍微處理不當就很容易造成OOM,當然,目前已經有很多知名的開源圖片加載框架,例如:ImageLoader,Picasso等等,這些框架已經能夠很好的解決了Bitmap造成的OOM問題,雖然這些框架能夠節省很多開發者的寶貴時間,但是也會遇到一種情況,很多初學者只是會簡單的去調用這些框架的提供的接口,被問到框架內部的一些實現原理,基本上都是腦中一片空白。從我的觀點出發,我認爲如果能夠掌握一些框架原理,想必對我們進行應用調優的意義是非常重大的,今天,主要是是想談談,如果沒有了圖片加載框架,我們要怎麼去處理Bitmap的內存問題呢? 
談到Bitmap處理的問題,我們可能要先來了解一些基礎的知識,關於Bitmap在Android虛擬機中的內存分配,在Google的網站上給出了下面的一段話 
技術分享 
大致的意思也就是說,在Android3.0之前,Bitmap的內存分配分爲兩部分,一部分是分配在Dalvik的VM堆中,而像素數據的內存是分配在Native堆中,而到了Android3.0之後,Bitmap的內存則已經全部分配在VM堆上,這兩種分配方式的區別在於,Native堆的內存不受Dalvik虛擬機的管理,我們想要釋放Bitmap的內存,必須手動調用Recycle方法,而到了Android 3.0之後的平臺,我們就可以將Bitmap的內存完全放心的交給虛擬機管理了,我們只需要保證Bitmap對象遵守虛擬機的GC Root Tracing的回收規則即可。OK,基礎知識科普到此。接下來分幾個要點來談談如何優化Bitmap內存問題。

1.Bitmap的引用計數方式(針對Android3.0之前平臺的優化方案,先上Demo Code)

private int mCacheRefCount = 0;//緩存引用計數器
private int mDisplayRefCount = 0;//顯示引用計數器
...
// 當前Bitmap是否被顯示在UI界面上
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {
            mDisplayRefCount--;
        }
    }

    checkState();
}

//標記是否被緩存
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }

    checkState();
}

//用於檢測Bitmap是否已經被回收
private synchronized void checkState() {
    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

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

上面的實例代碼,它使用了引用計數的方法(mDisplayRefCount 與 mCacheRefCount)來追蹤一個bitmap目前是否有被顯示或者是在緩存中. 當下麪條件滿足時回收bitmap: 
mDisplayRefCount 與 mCacheRefCount 的引用計數均爲 0. 
bitmap不爲null, 並且它還沒有被回收.

2.使用緩存,LruCache和DiskLruCache的結合 
關於LruCache和DiskLruCache,大家一定不會陌生(有疑問的朋友可以去API官網搜一下LruCache,而DiskLrucCache可以參考一下這篇不錯的文章:DiskLruCache使用介紹),出於對性能和app的考慮,我們肯定是想着第一次從網絡中加載到圖片之後,能夠將圖片緩存在內存和sd卡中,這樣,我們就不用頻繁的去網絡中加載圖片,爲了很好的控制內存問題,則會考慮使用LruCache作爲Bitmap在內存中的存放容器,在sd卡則使用DiskLruCache來統一管理磁盤上的圖片緩存。

3.SoftReference和inBitmap參數的結合 
在第二點中提及到,可以採用LruCache作爲存放Bitmap的容器,而在LruCache中有一個方法值得留意,那就是entryRemoved,按照文檔給出的說法,在LruCache容器滿了需要淘汰存放其中的對象騰出空間的時候會調用此方法(注意,這裏只是對象被淘汰出LruCache容器,但並不意味着對象的內存會立即被Dalvik虛擬機回收掉),此時可以在此方法中將Bitmap使用SoftReference包裹起來,並用事先準備好的一個HashSet容器來存放這些即將被回收的Bitmap,有人會問,這樣存放有什麼意義?之所以會這樣存放,還需要再提及到inBitmap參數(在Android3.0纔開始有的,詳情查閱API中的BitmapFactory.Options參數信息),這個參數主要是提供給我們進行復用內存中的Bitmap,如果設置了此參數,且滿足以下條件的時候:

  • Bitmap一定要是可變的,即inmutable設置一定爲ture;
  • Android4.4以下的平臺,需要保證inBitmap和即將要得到decode的Bitmap的尺寸規格一致;
  • Android4.4及其以上的平臺,只需要滿足inBitmap的尺寸大於要decode得到的Bitmap的尺寸規格即可;

在滿足以上條件的時候,系統對圖片進行decoder的時候會檢查內存中是否有可複用的Bitmap,避免我們頻繁的去SD卡上加載圖片而造成系統性能的下降,畢竟從直接從內存中複用要比在SD卡上進行IO操作的效率要提高几十倍。寫了太多文字,下面接着給出幾段Demo Code

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

// 用來盛放被LruCache淘汰出列的Bitmap
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

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

    // 當LruCache淘汰對象的時候被調用,用於在內存中重用Bitmap,提高加載圖片的性能
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {

        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {

            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {

            if (Utils.hasHoneycomb()) {

                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
        //將inMutable設置true,inBitmap生效的條件之一
    options.inMutable = true;

    if (cache != null) {
        // 嘗試尋找可以內存中課複用的的Bitmap
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {

            options.inBitmap = inBitmap;
        }
    }
}

// 獲取當前可以滿足複用條件的Bitmap,存在則返回該Bitmap,不存在則返回null
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()) {

                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;
                        iterator.remove();
                        break;
                    }
                } else {

                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

//判斷是否滿足使用inBitmap的條件
static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // Android4.4開始,被複用的Bitmap尺寸規格大於等於需要的解碼規格即可滿足複用條件
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // Android4.4之前,必須滿足被複用的Bitmap和請求的Bitmap尺寸規格一致才能被複用
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

4.降低採樣率,inSampleSize的計算 
相信大家對inSampleSize是一定不會陌生的,所以此處不再做過多的介紹,關於降低採樣率對inSampleSize的計算方法,我看到網上的算法有很多,下面的這段算法應該是最好的算法了,其中還考慮了那種寬高相差很懸殊的圖片(例如:全景圖)的處理。

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;

            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }

            long totalPixels = width / inSampleSize * height / inSampleSize ;

            final long totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels > totalReqPixelsCap) {
                inSampleSize *= 2;
                totalPixels /= 2;
            }
        }
        return inSampleSize;

5.採用decodeFileDescriptor來編碼圖片(暫時不知道原理,歡迎高手指點迷津) 
關於採用decodeFileDescriptor去處理圖片可以節省內存這方面,我在寫代碼的時候進行過嘗試,確實想比其他的decode方法要節省內存,查詢了網上的解釋,不是很清楚,自己看了一些源代碼也弄不出個名堂,爲什麼使用這種方式就能夠節省內存一些呢,如果有明白其中原理的高手,歡迎解答我的疑惑

到此,關於Bitmap處理的幾個優化點已經分析完畢,就目前來說,可能大家在開發的過程習慣了使用框架來加載圖片,所以不大在意圖片內存處理的相關問題,如果你想知道一些優化Bitmap內存原理或者想自己做一個優秀的圖片加載框架,希望本文能夠爲你提供一點點思路。如果讀者覺得文章有錯誤,歡迎在下方評論中批評指正。

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