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

最近除了那些忙着項目開發的事情,目前正在準備個人論文。短的時間沒有寫博客,今晚可貴想總結。只要有一點時間。所以,爲了湊合用,行。嘮叨羅嗦,直接進入正題。java

從事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, 並且它尚未被回收.markdown

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,假設設置了此參數,且知足如下條件的時候:app

  • 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的計算方法。我看到網上的算法有很是多。如下的這段算法應該是最好的算法了,當中還考慮了那種寬高相差很是懸殊的圖片(好比:全景圖)的處理。ide

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內存原理或者想本身作一個優秀的圖片載入框架。但願本文可以爲你提供一點點思路。假設讀者以爲文章有錯誤,歡迎在下方評論中批評指正。學習

相關文章
相關標籤/搜索