Android性能優化:談話Bitmap內存管理和優化


最近除了那些忙着項目開發的事情,目前正在准備我的論文。短的時間沒有寫博客,今晚難得想總結。只要有一點時間。因此,為了湊合用,行。嘮叨羅嗦,直接進入正題。

從事Android自移動終端的發展,想必是常常要與內存問題打交道的,說到Android開發中遇到的內存問題,像Bitmap這樣的吃內存的大戶略微處理不當就非常easy造成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內存原理或者想自己做一個優秀的圖片載入框架。希望本文能夠為你提供一點點思路。假設讀者覺得文章有錯誤,歡迎在下方評論中批評指正。

版權聲明:本文博客原創文章。博客,未經同意,不得轉載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM