1. 程式人生 > >Android 從具體例項分析Bitmap使用時候記憶體注意點

Android 從具體例項分析Bitmap使用時候記憶體注意點

根據Android官方提供給我們的Sample例子實實在在的分析Bitmap使用時候的注意點。

在分析Bitmap的使用之前先簡單的瞭解下BitmapFactory 類,BitmapFactory類可以根據各種不同的資料來源(檔案,流,位元組陣列等)來構建Bitmap點陣圖物件,BitmapFactory有一個內部類BitmapFactory.Options,大概看下BitmapFactory.Options裡面各個引數的作用(不一定全部對哦)
1). inBitmap: 如果設定,在載入Bitmap的時候會嘗試去重用這塊記憶體(記憶體複用),不能重用的時候會返回null,否則返回bitmap。
2). inDensity: 原始圖片的Bitmap的畫素密度
3). inDiter: 是否採用抖動解碼(舉個例子來理解抖動解碼,如果一張顏色很豐富的圖,用一個位數比較低的顏色模式來解碼的話,那麼一個直觀的感覺就是顏色不夠用,那麼這張圖解出來之後,在一些顏色漸變的區域上就會有一些很明顯的斷裂色帶,如果採用抖動解碼,那麼就會在這些色帶上採用隨機噪聲色來填充,目的是讓這張圖顯示效果更好,色帶不那麼明顯)
4). inInputShareable: 與inPurgeable一起使用,如果inPurgeable為false那該設定將被忽略,如果為true,那麼它可以決定點陣圖是否能夠共享一個指向資料來源的引用,或者是進行一份拷貝
5). inJustDecodeBounds: 當為true的時候bitmap返回null但是其他的一些option資訊還是會返回的。(比如有些情況下我們只是想要獲取圖片的寬度和高度就可以把這個引數設定為true)。
6). inMutable: 如果為true,解碼方法將始終返回一個可變的點陣圖
7). inPreferQualityOverSpeed: 如果設定為true,解碼器將嘗試重建影象以獲得更高質量的解碼,甚至犧牲解碼速度。現在只是對JPEG有用。
8). inPreferredConfig: 如果不為空,解碼器將嘗試解碼成這個內部配置,如果為空將嘗試挑選最好的匹配配置基於系統的螢幕。
9). inPremultiplied: 預設true,如果設定為true返回的bitmap有alpha的顏色通道。 一般不會去設定這個值直接用預設的。
10). inPurgeable: 如果設定為true,則由此產生的點陣圖將分配其畫素,以便系統需要回收記憶體時可以將它們清除。
11). inSampleSize: 如果設定的值大於1,解碼器將等比縮放影象以節約記憶體。
12). inScaled: 如果設定true,當inDensity和inTargetDensity不為0,載入時該點陣圖將被縮放,以匹配inTargetDensity,而不是依靠圖形系統縮放每次將它繪製到畫布上。
13). inScreenDensity: 當前正在使用的實際螢幕的畫素密度。
14). inTargetDensity: 這個點陣圖將被畫到的目標的畫素密度。
15). inTempStorage: 解碼的時候臨時儲存用的 建議設定16K。
16). mCancel: 用於指示已經呼叫了這個物件的取消方法的標誌。
17). outHeight: 影象的高度。
18). outMimeType: 如果知道,這個字串將被設定為解碼影象的MIME型別。
19). outWidth: 影象的寬度。

在分析Bitmap的使用之前還得知道,在Android3.0之前,Bitmap的記憶體分配分為兩部分,一部分是分配在Dalvik的VM堆中,而畫素資料的記憶體是分配在Native堆中,而到了Android3.0之後,Bitmap的記憶體則已經全部分配在VM堆上,這兩種分配方式的區別在於,Native堆的記憶體不受Dalvik虛擬機器的管理,必須手動呼叫Recycle方法釋放Bitmap的記憶體,而到了Android 3.0之後,我們就可以將Bitmap的記憶體完全放心的交給虛擬機器管理了,我們只需要保證Bitmap物件遵守虛擬機器的GC Root Tracing的回收規則即可。

Bitmap使用的時候主要從下面幾個方面優化

  1. 快取:記憶體快取+檔案快取 這個應該不能算是記憶體方面的優化,應該算效能上面的優化。
  2. 及時釋放Bitmap的記憶體:正如上面說到的Android 3.0之前有部分記憶體是分配在native上的,必須手動去釋放。
  3. 複用記憶體:BitmapFactory.Options 引數inBitmap的使用。inMutable設定為true,並且配合SoftReference軟引用使用(記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些軟引用物件的記憶體)。有一點要注意Android4.4以下的平臺,需要保證inBitmap和即將要得到decode的Bitmap的尺寸規格一致,Android4.4及其以上的平臺,只需要滿足inBitmap的尺寸大於要decode得到的Bitmap的尺寸規格即可。
  4. 降低取樣率,BitmapFactory.Options 引數inSampleSize的使用,從而減少記憶體的使用。

接下來就該進入正題了

對應上面四點Bitmap使用的優化點,下面結合Android官方提供給我們的Sample例子來簡單的看看具體是怎麼做的,對應的Sample例子在android-sdk目錄/sample/android版本/ui/DisplayingBitmaps。上面的每個優化點在這個例子裡面都有體現,在分析的時候會一一的指出來,給我們以後使用Bitmap的時候提供一個參照的作用。(例子下載地址)

在分析這個例項之前先對幾個類做大概的瞭解,方便後續程式碼分析。
例項簡單類圖如下

這裡寫圖片描述

ImageWorker類:載入圖片,從記憶體直接載入或非同步載入(從磁碟快取或processBitmap(這個要看ImageFetcher類中processBitmap()函式的的具體實現))。
1). ImageCache類: 快取的具體實現類。ImageCache裡面用到了兩種快取,記憶體快取LruCache,檔案快取DiskLruCache,在對圖片進行快取的時候記憶體和檔案都會快取的,但是記憶體快取的優先順序要高於檔案快取的優先順序,先讀記憶體快取然後在讀檔案快取。
2). BitmapWorkerTask類: 非同步載入(讀檔案快取或者呼叫processBitmap()函式, 因為這兩種讀取Bitmap資訊都是要耗費時間)的具體實現。裡面會先去檔案快取裡面讀Bitmap,如果沒讀到就呼叫ImageFetcher類的processBitmap()函式去載入Bitmap,然後又根據需要又把Bitmap加入到快取當中去。
3). ImageResizer類: 根據給定的大小對Image做調整,在降低取樣率的時候用到。會根據原圖的大小和要顯示的圖片的大小按照一定的演算法計算出inSampleSize的值(關於inSampleSize的計算我們以後是可以直接搬去用的)。
4). ImageFetcher類: 從網路下載圖片,processBitmap在ImageWorker中是一個抽象方法,並沒有實現體。為什麼這麼做呢,因為圖片的來源不確定可能是網路也可能是本地資料庫。所以ImageWorker中processBitmap直接用了抽象的方法,讓他的子類根據需求做不同的實現。在這裡做的是去網路上面讀取圖片。

只是分析大概的過程哦,方便以後使用Bitmap的時候更加容易入手點,還是按照具體的使用流程來直接跳到呼叫的地方。
ImageGridFragment類中內部類ImageAdapter的getView()函式中mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView)。兩個引數第一個引數url,第二個引數ImageView。 直接跟進去。
ImageWorker類的loadImage()函式

    public void loadImage(Object data, ImageView imageView) {
        if (data == null) {
            return;
        }

        BitmapDrawable value = null;

        if (mImageCache != null) {
            value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
        }

        if (value != null) {
            // Bitmap found in memory cache
            imageView.setImageDrawable(value);
        } else if (cancelPotentialWork(data, imageView)) {

            final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(mResources, mLoadingBitmap, task);
            imageView.setImageDrawable(asyncDrawable);

            // NOTE: This uses a custom version of AsyncTask that has been pulled from the
            // framework and slightly modified. Refer to the docs at the top of the class
            // for more info on what was changed.
            task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);

        }
    }

8-10行,從記憶體快取中去讀圖片資源。
12-15行,記憶體快取讀到了,直接設定給ImageView。
16-27行,記憶體快取裡面沒有圖片資源,先呼叫cancelPotentialWork()判斷當前ImageView對應的BitmapWorkerTask是否還在非同步請求當前url對應的影象資源,如果是則返回true不用再重新請求了,不是則返回false。返回false的時候我們就要新建一個BitmapWorkerTask去非同步請求url對應的影象資源了(可能是檔案快取也可能從網路上讀在接下來的分析中會體現出來)。然後通過AsyncDrawable把BitmapWorkerTask和ImageView關聯起來(注意AsyncDrawable是繼承BitmapDrawable的,所以是可以setImageDrawble的,正好在cancelPotentialWork()函式裡面會get出來去判斷)。
接下來就是BitmapWorkerTask類的具體實現了,重頭戲了(注意 通過上面記憶體快取已經讀過了哦,在記憶體快取裡面沒讀到才會進入BitmapWorkerTask非同步類哦)。

那就該去看BitmapWorkerTask類的doInBackground()函數了。(非同步,負責讀檔案快取,從網路上讀圖片資訊)。

        @Override
        protected BitmapDrawable doInBackground(Void... params) {

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - starting work");
            }

            final String dataString = String.valueOf(mData);
            Bitmap bitmap = null;
            BitmapDrawable drawable = null;

            // Wait here if work is paused and the task is not cancelled
            synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {}
                }
            }

            // If the image cache is available and this task has not been cancelled by another
            // thread and the ImageView that was originally bound to this task is still bound back
            // to this task and our "exit early" flag is not set then try and fetch the bitmap from
            // the cache
            if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
                bitmap = mImageCache.getBitmapFromDiskCache(dataString);
            }

            // If the bitmap was not found in the cache and this task has not been cancelled by
            // another thread and the ImageView that was originally bound to this task is still
            // bound back to this task and our "exit early" flag is not set, then call the main
            // process method (as implemented by a subclass)
            if (bitmap == null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
                bitmap = processBitmap(mData);
            }

            // If the bitmap was processed and the image cache is available, then add the processed
            // bitmap to the cache for future use. Note we don't check if the task was cancelled
            // here, if it was, and the thread is still running, we may as well add the processed
            // bitmap to our cache as it might be used again in the future
            if (bitmap != null) {
                if (Utils.hasHoneycomb()) {
                    // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
                    drawable = new BitmapDrawable(mResources, bitmap);
                } else {
                    // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
                    // which will recycle automagically
                    drawable = new RecyclingBitmapDrawable(mResources, bitmap);
                }

                if (mImageCache != null) {
                    mImageCache.addBitmapToCache(dataString, drawable);
                }
            }

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - finished work");
            }

            return drawable;

        }

25-28行,從檔案快取去讀圖片資源
34-37行,檔案快取沒有讀到,呼叫processBitmap()函式從網路上面讀圖片資源,processBitmap()函式的具體實現在ImageFetcher類中。這個我們等下再看。
43-56行,拿到了圖片資源,44-47行,Android3.0之後的裝置上面說過3.0之後的是itmap的記憶體則已經全部分配在VM堆上,不需要我們手動去釋放,這部分還是按照我們正常的使用流程。47-51行,Android3.0之前的裝置Bitmap有一部分記憶體是分配在Native堆中需要手動去釋放,在這裡對應Android 3.0之前的裝置用了RecyclingBitmapDrawable,RecyclingBitmapDrawable繼承BitmapDrawable並且裡面用了兩個計數器mDisplayRefCount,mCacheRefCount。mDisplayRefCount當ImageView顯示的時候加一如果顯示別的時候先把之前的減一,mCacheRefCount快取的時候加一從快取裡面移除的時候減一。這樣當這個RecyclingBitmapDrawable的mDisplayRefCount和mCacheRefCount都是0的時候說明這個資源不需要使用了呼叫getBitmap().recycle();了。

接著該看下上面說到的ImageFetcher類processBitmap()函式。

    private Bitmap processBitmap(String data) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "processBitmap - " + data);
        }

        final String key = ImageCache.hashKeyForDisk(data);
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        DiskLruCache.Snapshot snapshot;
        synchronized (mHttpDiskCacheLock) {
            // Wait for disk cache to initialize
            while (mHttpDiskCacheStarting) {
                try {
                    mHttpDiskCacheLock.wait();
                } catch (InterruptedException e) {}
            }

            if (mHttpDiskCache != null) {
                try {
                    snapshot = mHttpDiskCache.get(key);
                    if (snapshot == null) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "processBitmap, not found in http cache, downloading...");
                        }
                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
                        if (editor != null) {
                            if (downloadUrlToStream(data,
                                    editor.newOutputStream(DISK_CACHE_INDEX))) {
                                editor.commit();
                            } else {
                                editor.abort();
                            }
                        }
                        snapshot = mHttpDiskCache.get(key);
                    }
                    if (snapshot != null) {
                        fileInputStream =
                                (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                        fileDescriptor = fileInputStream.getFD();
                    }
                } catch (IOException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } catch (IllegalStateException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } finally {
                    if (fileDescriptor == null && fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {}
                    }
                }
            }
        }

        Bitmap bitmap = null;
        if (fileDescriptor != null) {
            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
                    mImageHeight, getImageCache());
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
        return bitmap;
    }

10-53行,通過mHttpDiskCache把從網路上面(downloadUrlToStream)讀取到的圖片資源快取到檔案裡面去,同時拿到了快取檔案的描述符fileDescriptor。(這個時候還不會有記憶體問題,因為網路上面拿到的圖片資源直接放到檔案裡面去了)
56-59行,bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,mImageHeight, getImageCache()); 準備從檔案裡面去讀圖片資訊了,這個時候就要讀到記憶體裡面來了,在這個函式裡面就會對記憶體的優化做處理了。
decodeSampledBitmapFromDescriptor()函式四個引數,檔案描述符,要顯示的寬度,要顯示的高度,快取類物件(肯定是在圖片顯示完之後要做快取)。

    public static Bitmap decodeSampledBitmapFromDescriptor(
            FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

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

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;

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

        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

6行,options.inJustDecodeBounds 設定只是先只是去解析圖片的寬度和高度資訊,不去載入具體的圖片資源。
10行,根據calculateInSampleSize()函式計算出合適的inSampleSize值。具體是怎麼計算的可以直接看下calculateInSampleSize()函式的具體實現,反正這個函式如果以後我們要用是可以直接搬來用的。通過這個函式就降低了圖片的取樣率等下讀圖片資源的時候大大的減少了記憶體的消耗了。
16-18行,android 3.0以上的版本。做記憶體複用處理。那就的看下addInBitmapOptions()函數了

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    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 and find a bitmap to use for inBitmap
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

            if (inBitmap != null) {
                options.inBitmap = inBitmap;
            }
        }

    }

6行 options.inMutable設定為true。
10行 去cache裡面找是否有適合的Bitmap給複用。如果有直接賦值給options.inBitmap。 這下就得去看下怎麼去找複用的Bitmap了。

    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;

    }

遍歷mReusableBitmaps(Bitmap的軟引用Set),通過canUseForInBitmap()函式去判斷是否可以複用 判斷的規則是如果Android 小於4.4的要保證寬度和高度都要相同並且inSampleSize為1,高於4.4的版本只要要顯示的圖片佔的記憶體比複用的那個Bitmap的記憶體小就可以。
到這裡我們清楚了複用條件的判斷,但是我們不知道mReusableBitmaps這個set是什麼時候在哪裡把可以複用的Bitmap加入進去的,直接看ImageCache的init()函式。mMemoryCache初始化部分

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

看到了吧,在這裡加入進來的,看到是當一個Bitmap從記憶體快取中拿掉的時候,這個Bitmap會加入到mReusableBitmaps中去,這裡我們知道某個Bitmap從記憶體快取中拿掉的時候這個Bitmap對應的記憶體不一定會這麼快釋放掉的,我們還是可以用的所以這裡用了軟應用。

到此整個流程非常簡單的分析完了,但是感覺還是很亂。接下來做一個總結,總結下上面提到的Bitmap的優化處理的幾點載例項中是怎麼做的。
1. 快取
例項中採用的方法,ImageCache使用了記憶體快取,檔案快取雙快取機制。
2. 及時釋放Bitmap的記憶體(針對Android 3.0之前的裝置)
例項中藉助RecyclingBitmapDrawable類和RecyclingImageView類的配合使用實現Bitmap記憶體的及時釋放

RecyclingBitmapDrawable繼承BitmapDrawable同時裡面有兩個計數器mDisplayRefCount,mCacheRefCount。mDisplayRefCount當RecyclingBitmapDrawable要顯示的時候加一換別的顯示的時候減一,mCacheRefCount快取的時候加一從快取裡面移除的時候減一,這樣當這個RecyclingBitmapDrawable的mDisplayRefCount和mCacheRefCount都是0的時候說明這個資源不需要使用了呼叫getBitmap().recycle()釋放掉。

RecyclingImageView繼承ImageView, 當RecyclingImageView setImageDrawable的時候先通知之前的Drawble不顯示了同時通知當前的Drawable我要顯示了這裡就要看你Drawable對應的是不是RecyclingBitmapDrawable了。
3. 複用記憶體
BitmapFactory.Options 引數inBitmap的使用。inMutable設定為true,並且配合SoftReference軟引用使用(記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體)。當一個Bitmap從記憶體快取中移除掉的時候,把這個Bitmap加入到複用的Set集合裡面去。判斷是否有Bitmap可以複用的時候先去這個集合裡面拿到Bitmap,然後按照複用圖片的規則(Android4.4以下的平臺,需要保證inBitmap和即將要得到decode的Bitmap的尺寸規格一致,Android4.4及其以上的平臺,只需要滿足inBitmap的尺寸大於要decode得到的Bitmap的尺寸規格即可)判斷是否可以複用。
4. 降低取樣率
BitmapFactory.Options 引數inSampleSize的使用,先把options.inJustDecodeBounds設為true,只是去讀取圖片的大小,在拿到圖片的大小之後和要顯示的大小做比較通過calculateInSampleSize()函式計算出inSampleSize的具體值,得到值之後。options.inJustDecodeBounds設為false讀圖片資源。