Android Managing Bitmap Memory And Loading Large Bitmaps Efficiently

  除了图片缓存外,还有一些方法可以优化 gc 和图片的重用。具体的方法和不同的 Android 版本有关。你可以通过后面提供的 demo 来学习如何在不同版本的 Android 版本上高效地处理图片。
  在 Android 2.2(API 8)及更低的版本中,当 gc 触发的时候,app 的主线程会被阻塞,这样会降低应用性能和用户体验。从 Android 2.3 开始,gc 变成了并发的操作,这样当 bitmap 不再被引用时,它就会很快地被回收掉。在 Android 2.3.3(API 10)及之前的版本,bitmap 的像素数据是被存储在 native memory 里面的,而 bitmap 对象本身却被存储 Dalvik heap 中。在不同的环境中,native memory 中的像素数据被释放的方式、时机是不同的,这可能会造成内存溢出。从 Android 3.0(API 11)开始,bitmap 对象和其像素数据都被存储在 Dalvik heap 中,从而解决了前面的问题。
  下面讨论在不同的 Android 版本中,如何管理优化 bitmap 的内存。

一. Android 2.3.3 及更低版本

  在 Android 2.3.3(API 10) 及之前的版本中,必须主动调用 recycle() 方法回收 bitmap 内存。如果要加载大量的图片,那么就很容易出现 OutOfMemoryError,而 recycle() 方法会使 app 能够尽快地回收内存。
  注意:在调用 recycle() 前,你得确保对应的 bitmap 不再使用了,因为在调用 recycle() 后,你再访问该 bitmap 时,就会出现错误:"Canvas: trying to use a recycled bitmap" 。
  下面的这个例子使用引用计数法来跟踪 bitmap 的状态(正在显示还是被 cache 了),当同时满足以下条件时,bitmap 对象会被回收:
(1)mDisplayRefCount 和 mCacheRefCount 的值都为 0;
(2)bitmap 不为 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.
publicvoid setIsCached(boolean isCached){
    synchronized(this){
        if(isCached){
            mCacheRefCount++;
        }else{
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

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

二. Manage Memory on Android 3.0 and Higher

  从 Android 3.0(API 11)开始,引入了 BitmapFactory.Options.inBitmap 变量,如果设置了这个变量,那么有 Options 的 object 在加载内容的时候,就会尝试使用已存在的 bitmap 对象。这也就意味着 bitmap 是可重用的,这样可以提高性能,因为可以减少分配回收内存这部分开销。但是,inBitmap 变量的使用是有明确的限制的,特别是在 Android 4.4(API 19)以前(只有当 bitmap 的大小相等时,才可以重用)。

1. 存储 bitmap ,以便后面重用:下面 这段代码演示了如何存储一个 bitmap 对象,以备后面重用。
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. 使用已存在的 bitmap:app 在运行的过程中,decoder 方法会检查是不是有可用的 bitmap,如下

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

  addInBitmapOptions() 方法会查找已存在的 bitmap ,并把它赋值给 inBitmap 对象,如果没有找到 bitmap,那么它自然没法给 inBitmap 赋值了。

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

Finally, this method determines whether a candidate bitmap satisfies the size criteria to be used for  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;
    }elseif(config == Config.RGB_565){
        return 2;
    }elseif(config == Config.ARGB_4444){
        return 2;
    }elseif(config == Config.ALPHA_8){
        return 1;
    }
    return1;
}

三. Loading Large Bitmaps Efficiently

  Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI). For example, the system Gallery application displays photos taken using your Android devices's camera which are typically much higher resolution than the screen density of your device.

Given that you are working with limited memory, ideally you only want to load a lower resolution version in memory. The lower resolution version should match the size of the UI component that displays it. An image with a higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling.

This lesson walks you through decoding large bitmaps without exceeding the per application memory limit by loading a smaller subsampled version in memory.

Read Bitmap Dimensions and Type

The BitmapFactory class provides several decoding methods (decodeByteArray()decodeFile(),decodeResource(), etc.) for creating a Bitmap from various sources. Choose the most appropriate decode method based on your image data source. These methods attempt to allocate memory for the constructed bitmap and therefore can easily result in an OutOfMemory exception. Each type of decode method has additional signatures that let you specify decoding options via the BitmapFactory.Options class. Setting the inJustDecodeBounds property to true while decoding avoids memory allocation, returning null for the bitmap object but setting outWidthoutHeight and outMimeType. This technique allows you to read the dimensions and type of the image data prior to construction (and memory allocation) of the bitmap.

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;

To avoid java.lang.OutOfMemory exceptions, check the dimensions of a bitmap before decoding it, unless you absolutely trust the source to provide you with predictably sized image data that comfortably fits within the available memory.

Load a Scaled Down Version into Memory

Now that the image dimensions are known, they can be used to decide if the full image should be loaded into memory or if a subsampled version should be loaded instead. Here are some factors to consider:

  • Estimated memory usage of loading the full image in memory.
  • Amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
  • Dimensions of the target ImageView or UI component that the image is to be loaded into.
  • Screen size and density of the current device.

For example, it’s not worth loading a 1024x768 pixel image into memory if it will eventually be displayed in a 128x96 pixel thumbnail in an ImageView.

To tell the decoder to subsample the image, loading a smaller version into memory, set inSampleSize to true in your BitmapFactory.Options object. For example, an image with resolution 2048x1536 that is decoded with aninSampleSize of 4 produces a bitmap of approximately 512x384. Loading this into memory uses 0.75MB rather than 12MB for the full image (assuming a bitmap configuration of ARGB_8888). Here’s a method to calculate a sample size value that is a power of two based on a target width and height:

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

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

        finalint halfHeight = height /2;
        finalint 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;
}

Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the inSampleSize documentation.

To use this method, first decode with inJustDecodeBounds set to true, pass the options through and then decode again using the new inSampleSize value and inJustDecodeBounds set to 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);
}

This method makes it easy to load a bitmap of arbitrarily large size into an  ImageView  that displays a 100x100 pixel thumbnail, as shown in the following example code:
mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage,100,100));

You can follow a similar process to decode bitmaps from other sources, by substituting the appropriate BitmapFactory.decode* method as needed.


处理 Bitmap 的参考 Demo 下载链接


你可能感兴趣的:(android,cache,bitmap,memory)