Displaying Bitmaps Efficiently

Displaying Bitmaps Efficiently

摘自 android develop training。

Android 设备为每个应用提供 16M 的可用内存。
Bitmap 又会占用很大的内存。

参考 BitmapFun sample.

----------------------------------
Loading Large Bitmaps Efficiently
----------------------------------

1.Read Bitmap Dimensions and Type

BitmapFactory class 提供了一些 decode Bitmap 的方法 (decodeByteArray(), decodeFile(), decodeResource(), etc).
这些方法在在构造 Bitmap 的时候会分配内存,这很容易导致 OutOfMemory Exception。
但是每个类型的解码方法,都有额外的签名,通过 BitmapFactory.Options class 指定解码选项。
当解码时,设定 inJustDecodeBounds 属性为 true,可以避免分配内存,会返回 null bitmap 对象,但是会设置
outWidth, outHeight and outMimeType。
这技术,能让你在构造Bitmap之前,能够读到 bitmap 的尺寸大小(demesions) 和 image 数据的类型 (type)。

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;

为了避免 java.lang.OutOfMemory exceptions, 在 decode Bitmap 前,检查它的尺寸大小是很有必要的。

2.Load a Scaled Down (按比例缩小的) Version into Memory

现在image的大小你是知道了,那你就可以决定是否把 full image 或者 它的小样品版本 加载到内存。
这就要考虑一些因素:
* 加载 full image 到内存时,要估计内存的使用。
* Amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
* 加载image时,考虑目标 ImageView 或 UI component 的尺寸大小,
* 考虑当前设备的 屏幕大小 和 密度

例如,把一个 1024X768 pixel 的image 加载到内存中,而它最终却以一个 128X96 pixel 的略缩图在 ImageView 中显示,
这显然是不值得的。

要加载一个小的版本到内存,可以设置 inSampleSize = true in your BitmapFactory.Options Object。
For example, 一张 2048 X 1536 的image,设置 inSampleSize = 4, 将会产生大约 512 X 384 的 bitmap。
相应的,加载到内存中为 0.74MB 与 12MB (a bitmap configuration of ARGB_888)。
下面是 基于目标长和宽为基准的 计算 sample size 的方法。

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) {

        // Calculate ratios of height and width to requested height and width
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);

        // Choose the smallest ratio as inSampleSize value, this will guarantee
        // a final image with both dimensions larger than or equal to the
        // requested height and width.
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    return inSampleSize;
}

简单说就是比较原生的大小和目标imageview的大小,计算他们长宽的比例,选最小的作为 inSampleSize 的值。

Note: inSampleSize = 2 ,解码时是更快更高效的。

使用归纳:
首先设置 options.inJustDecodeBounds = true 获取Bitmap的大小,
计算inSampleSize,
设置options.inJustDecodeBounds = false,以inSampleSize加载Bitmap。

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

例子:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

------------------------------------
Processing Bitmaps Off the UI Thread

------------------------------------

上一节的那些decode方法,如果是从disk或network读取数据时,不应该在main UI Thread 上执行,分分钟会Blocks UI,导致ANR。
接下来,要shows you使用后台线程 AsyncTask 和 如何处理并行问题。

1.Use an AsyncTask

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

对 ImageView 使用 WeakReference 引用,确保 AsyncTask 并不能阻止 ImageView 和任何它的引用被垃圾回收掉(如:用户导航Activity时,或在任务完成前,configuration change 了)。
在任务完成前,并不能保证ImageView is still around, 所以你必须在 onPostExecute() 中检查它的引用,

使用:
public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

2.Handle Concurrency

一些 view 组件,如 ListView,GridView。在结合 AsyncTask 使用时,会出现一些问题。
这些组件,为了使内存高效,当用户scroll时往往会回收child views。
如果每个 child view 触发一个 AsyncTask,在任务完成时,并不能保证与任务相关联的 view 没有被回收来用于另外的 child view。
(简单讲就是,在任务完成时,相关联的 view 要回收用于别的 child view)
此外,也并不能保证,组件上item的顺序与多个异步任务先后执行完成的顺序一样。

一个解决方法是,存储在 AsyncTask 的 ImageView 的引用,当任务完成时,再进行延迟检查。
所以,对于上节的AsyncTask,可以采用类似的方法模式。

创建一个dedicated(专用的)Drawable,用来存储worker task 的引用。
这样一来,任务完成时,这个 BitmapDrawable 充当一个占位符显示在 ImageView 上。

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTAsk bitmapWorkerTask) {
	super(res, bitmap);
	bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public bitmapWorkerTask getBitmapWorkerTask() {
	return bitmapWorkerTaskReference.get();
    }
}

在执行 BitmapWorkerTask 之前,先创建一个 AsyncDrawable,并把它绑定到目标 ImageView 上。

public void loadBitmap(int resId, ImageView imageView) {
    if (canclePotentialWork(resId, imageView)) {
	final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
	final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
	imageView.setImageDrawable(asyncDrawable);
	task.execute(resId);
    }
}

canclePotentialWork 方法是用来检查是否有正在运行的任务关联到 ImageView。
如果是,将会尝试用 cancle() 取消之前的任务。

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

最后就是更新 onPostExecute()。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {

    @Override
    protected void onPostExecute(Bitmap bitmap) {
	if (isCancelled()) {
   	bitmap = null;
}

if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

只实现适用于任何会回收child view 的组件。如:GridView 在getView()中调用loadBitmap()。

------------------------------------
Caching Bitmaps
------------------------------------

一些组件(ListView, GridView, ViewPage),会回收child view 当他们off-screen时。
垃圾收集器(gc)也会释放你的加载了的 bitmaps,前提是 bitmap 的引用不是长生命周期的。
这是好的,但为了UI的流畅性,速度,所以,当图片on-screen时,你要避免不断的每次都要处理那些图片。
内存和硬盘缓存能很好的帮到你,能够很快的重新加载处理过的图片。

1.Use a Memory Cache

这里使用 LruCache class, 能很好的缓存图片。
保存最近的对象引用到一个强引用的 LinkedHashMap 中,并且当缓存超过指定的大小时,会清除最近比较少用的成员。

Note: 以前,一种流行的内存缓存实现时使用 SoftReference or WeakReference bitmap cache,
但是现在不建议用了,自从Android2.3(API Level 9),垃圾收集器对 sofe/weak 引用显得很有侵略性,使得它们相当的不高效。
另外,在Android3.0(API Level 11)之前,bitmap的数据时存储在native memory中的,释放的方式不好预测,并且可能的使应用
会超过内存限制界限,导致应用crash掉。

对于LrcCache的大小,没有特定的大小或公式适合所有的应用,这取决于你对使用的分析和解决方案。
太小的cache导致额外的开销没有好处,太大的cache能一次性导致OutOfMemory exception,或使你的应用留下较少的内存使用。

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

Note:In this example, 1/8的应用内存分配了给我们的cache。在个 normal/hdpi 设备上,这大概有4MB(32/8)。
一个全屏的GridView在一个800X480的设备上填满images,需要1.5MB(800*480*4),所以,这可以缓存大约 2.5 pages的images in memory。

修改下 loadBitmap:

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);


    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

2.Use a Disk Cache

内存缓存是能提高访问最近的bitmaps的速度的,但对于images这种缓存就不太可靠了。
像GridView这种组件,会很容易填满内存缓存。
你的应用往往会被干扰,如一个phone call。在后台时,引用会被kill掉,导致cache也被销毁。
重新resumes时,你的应用又会再次处理每个image。

磁盘缓存能持久保留处理过的 bitmaps ,在内存缓存无效时,减少加载的次数。
当然,从磁盘获取images会比从内存中加载慢的多,所以应该在后台线程进行操作。

Note:如果访问次数较频繁,ContentProvider 是个较适合的地方存储缓存images,如Gallery app。

这里使用的是 DiskLruCache class 来实现磁盘缓存。(来自 Android source)

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);


        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);


        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

Note:初始化disk cache 是要操作到磁盘的,所以不应该在main UI Thread 上操作。
也就是说,在它初始化之前,就有可能被访问到。To address this(为了解决这个问题),
a lock object ensures that the app does not read from the disk cache until the cache has been initialized.
使用lock object 直到初始化后才能访问。

While the memory cache is checked in the UI thread, the disk cache is checked in the background thread.

3.Handle Configuration Changes

当应用的 configuration 改变时(如 screen orientation change),activity 会重新的加载。
应该避免重新处理images。

use a memory cache 是个很好的解决方法。
当使用Fragment时,设置 setRetainInstance(true), 就能把 memory cache 传给这个对象。
所以,当Activiity recreated 时,这个retained Fragment 就会重新 reattached,
那你就能获取到那个已经存在的缓存对象。

Here’s an example of retaining a LruCache object across configuration changes using a Fragment:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

------------------------------------
Managing Bitmap Memory
------------------------------------

处理上节所说的Caching Bitmaps 的知识外,你还可以做些别的事来促进 垃圾的收集 和 bitmap reuse。
这取决于你用Android的那个版本了。

先了解一下,Android 版本对bitmap 内存的处理方式:

* Android 2.2(API Level 8) and lower,当垃圾回收时,你的app 线程会停止,这会有延时的感觉。
Android 2.3 增加了并行垃圾回收,也就是说,the memory is reclaimed soon after a bitmap is no longer referenced.

*On Android 2.3.3 (API level 10) and lower,一个bitmap的backing pixel data是存储在native memory中的,
它是与bitmap(存储在Dalvik heap)本身分离的。backing pixel data在native memory是不容易预料到释放的方式,
这会导致app容易超出内存限制,使app crash掉。
 As of Android 3.0 (API Level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

1.Manage Memory on Android 2.3.3 and Lower

On Android 2.3.3 (API level 10) and lower, using recycle() is recommended. 
The recycle() method allows an app to reclaim memory as soon as possible.

Caution: You should use recycle() only when you are sure that the bitmap is no longer being used. 
If you call recycle() and later attempt to draw the bitmap,you will get the error: "Canvas: trying to use a recycled bitmap".

下面是使用recycle()的例子,使用了计数引用来跟踪bitmap是当前显示还是缓存着。
当下面的条件到达时,bitmap就会被回收掉。

* 引用计数为0。(mDisplayRefCount and mCacheRefCount 都为0)
* The bitmap is not null, and it hasn't been recycled yet.

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.
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

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

2.Manage Memory on Android 3.0 and Higher

Android3.0(API Level 11) 使用的是 BitmapFactory.Options.inBitmap 这个属性。
如果设置了这个属性,decode method 在加载内容是,就会尝试重新使用存在的 bitmap。
也就是说 bitmap 的内存得到重新使用,提高了性能,免除了内存的分配,释放。
在使用 inBitmap 时,有些要说明的事项:

* 再用的 bitmap 必须和源内容的大小一致(确保相同的数量的内存在使用),并且格式也要一样,JPEG 或 PNG。
* reused bitmap 的 configuration 会 overrides inPreferredConfig 属性,如果它被设置了的话。
* 你应该总是使用能返回 bitmap 的decode method,因为你不能假设 reusing the bitmap worked (如:大小不一致)。

Save a bitmap for later use

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

// If you're running on Honeycomb or newer, create
// a HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    mReusableBitmaps = 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()));
            }
        }
    }
....
}

Use an existing 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);
}

寻找个合适的bitmap来设置inBitmap。

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

private static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {
    int width = targetOptions.outWidth / targetOptions.inSampleSize;
    int height = targetOptions.outHeight / targetOptions.inSampleSize;

    // Returns true if "candidate" can be used for inBitmap re-use with
    // "targetOptions".
    return candidate.getWidth() == width && candidate.getHeight() == height;
}

------------------------------------
Displaying Bitmaps in Your UI
------------------------------------

现在把之前的知识结合起来,想你展示在处理并行情况和configuration改变时,Viewpager 和 GridView 是如何在
使用后台线程和 bitmap cache 加载多个 bitmaps 的。

1.Load Bitmaps into a ViewPager Implementation

ViewPager 比较适合的 adapter 是 FragmentStatePageAdapter 的子类,
当它们off-screen时,能自动的销毁和保存 Fragments 的状态,降低内存的消耗。

Note:如果只有小数量的images,或你能肯定它们占用的内存在app 内存限制之内,
用正常的 PageAdapter or FragmentPageAdapter 可能比较适合。

public class ImageDetailActivity extends FragmentActivity {
    public static final String EXTRA_IMAGE = "extra_image";

    private ImagePagerAdapter mAdapter;
    private ViewPager mPager;

    // A static dataset to back the ViewPager adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image_detail_pager); // Contains just a ViewPager

        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter);
    }

    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
        private final int mSize;

        public ImagePagerAdapter(FragmentManager fm, int size) {
            super(fm);
            mSize = size;
        }

        @Override
        public int getCount() {
            return mSize;
        }

        @Override
        public Fragment getItem(int position) {
            return ImageDetailFragment.newInstance(position);
        }
    }
}

public class ImageDetailFragment extends Fragment {
    private static final String IMAGE_DATA_EXTRA = "resId";
    private int mImageNum;
    private ImageView mImageView;

    static ImageDetailFragment newInstance(int imageNum) {
        final ImageDetailFragment f = new ImageDetailFragment();
        final Bundle args = new Bundle();
        args.putInt(IMAGE_DATA_EXTRA, imageNum);
        f.setArguments(args);
        return f;
    }

    // Empty constructor, required as per Fragment docs
    public ImageDetailFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // image_detail_fragment.xml contains just an ImageView
        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
        mImageView = (ImageView) v.findViewById(R.id.imageView);
        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final int resId = ImageDetailActivity.imageResIds[mImageNum];
        mImageView.setImageResource(resId); // Load image into ImageView
    }
}

issue: images 是从UI Thread 上读取资源的,可能导致block app啦,用AsyncTask,
顺便加上缓存。

public class ImageDetailActivity extends FragmentActivity {
    ...

    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    public void onCreate(Bundle SavedInstanceState) {
...
// initialize LruCache as per Use a Memory Cache section
    }

    public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);

final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
   mImageView.setImageBitmap(bitmap);
} else {
   mImageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
            task.execute(resId);
}
    }

    ... // include BitmapWorkerTask class
}

public class ImageDetailFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (ImageDetailActivity.class.isInstance(getActivity())) {
            final int resId = ImageDetailActivity.imageResIds[mImageNum];
            // Call out to ImageDetailActivity to load the bitmap in a background thread
            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
        }
    }
}

2.Load Bitmaps into a GridView Implementation

先看下标准处理方法,看下有什么问题。

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    private ImageAdapter mAdapter;

    // A static dataset to back the GridView adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    // Empty constructor as per Fragment docs
    public ImageGridFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
        return v;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
    }

    private class ImageAdapter extends BaseAdapter {
        private final Context mContext;

        public ImageAdapter(Context context) {
            super();
            mContext = context;
        }

        @Override
        public int getCount() {
            return imageResIds.length;
        }

        @Override
        public Object getItem(int position) {
            return imageResIds[position];
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some attributes
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            } else {
                imageView = (ImageView) convertView;
            }
            imageView.setImageResource(imageResIds[position]); // Load image into ImageView
            return imageView;
        }
    }
}

问题与上面是一样的,在UI thread上加载image。更新如下:

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...

    private class ImageAdapter extends BaseAdapter {
        ...

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return imageView;
        }
    }

    public void loadBitmap(int resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (bitmapData != data) {
                // Cancel previous task
                bitmapWorkerTask.cancel(true);
            } else {
                // The same work is already in progress
                return false;
            }
        }
        // No task associated with the ImageView, or an existing task was cancelled
        return true;
    }

    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
       if (imageView != null) {
           final Drawable drawable = imageView.getDrawable();
           if (drawable instanceof AsyncDrawable) {
               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
               return asyncDrawable.getBitmapWorkerTask();
           }
        }
        return null;
    }

    ... // include updated BitmapWorkerTask class

    Note: The same code can easily be adapted to work with ListView as well.

这种实现很好的提供了处理图片的灵活性,弹性,并且加载时不会影响UI的流畅性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 当应用程序加载过多的位图时,可能会导致内存不足的问题。为了避免这种情况,可以采取一些常见的技术来处理和加载位图对象。首先,可以使用LRU缓存来管理位图的内存使用。通过LRU缓存,可以将最常访问的位图保留在内存中,而将不常访问的位图从内存中移除,以保持内存的可用性。此外,还可以使用一些优化技巧,如使用合适的尺寸和配置来加载位图,避免加载过大的位图,以及在加载位图时保持用户界面的响应性。如果不小心处理位图,可能会导致内存超限的异常,如java.lang.OutofMemoryError: bitmap size exceeds VM budget。因此,需要注意位图的处理方式,以避免内存不足的问题。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [Caching Bitmaps](https://blog.csdn.net/jwzhangjie/article/details/28447207)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Displaying Bitmaps Efficiently](https://blog.csdn.net/huanghailang/article/details/51836886)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值