[翻译]Bitmap的异步加载和缓存

内容概述

[翻译]开发文档:android Bitmap的高效使用

本文内容来自开发文档“Traning > Displaying Bitmaps Efficiently”,包括大尺寸Bitmap的高效加载,图片的异步加载和数据缓存。

Bitmap的处理和加载非常重要,这关系到app的流畅运行和内存占用,如果方法不当,很容易导致界面卡顿和OOM。其中的原因大致有:

  • android系统对进程的内存分配限制,移动设备的配置较低。
  • Bitmap会消耗很大内存。比如相机拍下的 2592x1936 像素的照片,以ARGB_8888 格式一次加载到内存,将占据19M(259219364 bytes)的内存!
  • 通常像ListView,GridView,ViewPager这样的UI元素会同时显示或预加载多个View,这导致内存中同时需要多个Bitmaps。

下面从几个方面来分析如何高效的使用图片。

高效地加载大图

原始图片和最终显示它的View对应,一般要比显示它的View的大小要大,一些拍摄的照片甚至要比手机的屏幕分辨率还要大得多。 原则上“显示多少加载多少”,没有必要加载一个分辨率比将要显示的分辨率还大的图片,除了浪费内存没有任何好处。 下面就来看如何加载一个图片的较小的二次采样后的版本。

读取Bitmap的尺寸和类型

BitmapFactory类提供了几个方法用来从不同的源来加载位图(decodeByteArray(), decodeFile(), decodeResource(), etc.) ,它们都接收一个BitmapFactory.Options类型的参数,为了获取目标图片的尺寸类型,可以将此参数的 inJustDecodeBounds设置为true来只加载图片属性信息,而不去实际加载其内容到内存中。

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;

上面的代码展示了如何不加载图片,而预先读取它的Width、Height、MiMeType。有了这些信息,就可以根据可用内存来“选择性”地加载图片,避免OOM。

加载缩小后的图片

知道目标图片的尺寸后,可以根据当前内存状态或者显示需求来决定是加载原始图片,或者是采样后的版本。下面是一些参考:

  • 估算加载完整图片需要的内存。
  • 加载这些图片允许的内存大小,要知道总得给程序其它操作留够内存。
  • 使用此图片资源的目标ImageView或其它UI组件的尺寸。
  • 当前设备的屏幕大小和分辨率。

比如,在一个作为缩略图的大小为128x96的ImageView中加载1024x768的图片是完全没有必要的。

为了让图片解码器(decoder)在加载图片时使用二次采样(subsample),可以设置参数BitmapFactory.Options 的inSampleSize属性。比如,一张2048x1536 分辨率的图片,使用inSampleSize为4的参数加载后,以ARGB_8888格式计算,最终是 512x384的图片,占0.75M的内存,而原始分辨率则占12M。

下面的方法用来计算采样率,它保证缩放的比例是2的次方,参见inSampleSize的说明: If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

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;

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

先设置 inJustDecodeBounds为true获得图片信息,计算出采样率,之后设置 inJustDecodeBounds为false,传递得到的inSampleSize来实际加载缩略图:

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

通过上述的方法,就可以把任意大小的图片加载为一个100x100的缩略图。类似这样:

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

其它几个BitmapFactory.decode**的方法都接收相同的参数,可以采用同样的方法来安全地加载大图。

在非UI线程中处理Bitmap

从网络和磁盘加载图片可能很耗时,这样如果在UI线程中执行加载就会很容易引起ANR,下面使用AsyncTask来在后台线程中异步加载图片,并演示一些同步技巧。

使用AsyncTask

AsyncTask提供了一个简单的方式异步执行操作,然后回到UI线程中处理结果。下面就实现一个AsyncTask子类来加载图片到ImageView。

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

上面方法decodeSampledBitmapFromResource()是前一节“加载大图”的代码示例。

WeakReference保证了其它对ImageView的强引用消失后,它可以被正常回收。也许异步加载操作执行结束后ImageView已经不存在了(界面销毁?横竖屏切换引起Activity重建?),这时就没必要去继续显示图片了。所以在onPostExecute()中需要额外的null检查。

有了上面的BitmapWorkerTask后,就可以异步加载图片:

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

并发处理

对于在像ListView和GridView中显示图片这样的场景,ImageView很可能会被“复用”,这样在快速滑动时,一个ImageView很可能在图片尚未加载显示时就被用来显示另一个图片,此时,上面的BitmapWorkerTask就无法保证onPostExecute中收到的Bitmap就是此ImageView当前需要显示的图片。简单地说就是图片在这些列表控件中发生错位了,本质来看,这是一个异步操作引发的并发问题。

下面采取“绑定/关联”的方式来处理上面的并发问题,这里创建一个Drawable的子类AsyncDrawable,它设置给ImageView,同时它持有对应BitmapWorkerTask 的引用,所以在对ImageView加载图片时,可以根据此AsyncDrawable来获取之前执行中的BitmapWorkerTask,之后取消它,或者在发现重复加载后放弃操作。

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

上面,AsyncDrawable的作用类似View.setTag和ViewHolder。 在执行BitmapWorkerTask前,创建一个AsyncDrawable,然后把它绑定到目标ImageView。

注意:列表异步加载图片的场景下,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);
    }
}

上面setImageDrawable()方法把ImageView和最新加载图片给它的异步任务关联起来了。 cancelPotentialWork方法()用来判断是否已经有一个任务正在加载图片到此ImageView中。如果没有,或者有但加载的是其它图片,则取消此“过期”的异步任务。如果有任务正在加载同样的图片到此ImageView那么就没必要重复开启任务了。

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

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || 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;
}

辅助方法getBitmapWorkerTask()用来获取ImageView关联的BitmapWorkerTask。

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

最后,修改BitmapWorkerTask的 onPostExecute()方法,只有在任务未被取消,而且目标ImageView关联的BitmapWorkerTask对象为当前BitmapWorkerTask时,才设置Bitmap给此ImageView:

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

通过上述过程,就可以使用BitmapWorkerTask 正确安全地异步加载图片了。

Bitmap的缓存

上面分别从节约内存和避免耗时加载卡顿界面两个方面讨论了有关图片处理的技巧。 在列表显示大量图片,或者其它任意的图片显示操作下,默认地系统会对内存中无强引用的图片数据进行回收,而很多时候,如列表来回滑动多次显示同样的图片,引起图片的内存释放和反复加载,图片加载是耗时操作,最终,使得图片展示交互体验无法流畅进行。

下面从“缓存”的方式讲起,介绍下如何使用内存缓存和磁盘缓存来提高图片显示的流畅度。

内存缓存

从Android 2.3 (API Level 9)开始,GC对Soft/WeakReference的回收更加频繁,所以基于这些引用的缓存策略效果大打折扣。而且在Android 3.0 (API Level 11)以前,Bitmap的数据是以native的方式存储的,对它们的“默认回收”的行为可能引发潜在的内存泄露。 所以,现在推荐的方式是使用强引用,结合LruCache类提供的算法(它在API 12引入,Support库也提供了相同的实现使得支持API 4以上版本)来实现缓存。LruCache算法内部使用 LinkedHashMap 保持缓存对象的强引用,它维持缓存在一个限制的范围内,在内存要超越限制时优先释放最近最少使用的key。

在选择LruCache要维护的缓存总大小时,下面时一些参考建议:

  • 其余Activity或进程对内存的大小要求?
  • 屏幕同时需要显示多少图片,多少会很快进入显示状态?
  • 设备的大小和分辨率?高分辨率设备在显示相同“大小”和数量图片时需要的内存更多。
  • 图片被访问的频率,如果一些图片的访问比其它一些更加频繁,那么最好使用多个LruCache来实现不同需求的缓存。
  • 数量和质量的平衡:有时可以先加载低质量的图片,然后异步加载高质量的版本。

缓存的大小没有标准的最佳数值,根据app的需求场景而定,如果太小则带来的速度收益不大,如果太大则容易引起OOM。

下面是一个使用LruCache来缓存Bitmap的简单示例:

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

上面的代码使用了进程分配内存的1/8来作为缓存的最大值。在一个标准/hdpi分辨率的设备上,最小值大约为4MB(32/8)。一个800x480的设备上,全屏的GridView填满图片后大约使用1.5MB(8004804 bytes)的内存,这样,缓存可以保证约2.5页的图片数据。

在使用ImageView加载图片时,先去内存缓存中查看,如果存在就直接使用内中的图片,否则就异步加载它:

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

异步加载图片的BitmapWorkerTask 在获取到图片后将数据添加到缓存:

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

磁盘缓存

内存缓存的确是最快速的,但是一方面内存容易受限,另一方面进程重建后缓存就失效了。可以增加一个磁盘缓存的策略,这样可以缓存更多的内容,而且依然提供比网络获取数据更好的速度。如果图片被访问非常频繁,也可以考虑使用ContentProvider实现图片数据的缓存。

下面的代码使用DiskLruCache(它从android源码中可获得,在sdk提供的sample中也有)来实现磁盘缓存:

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

因为磁盘读取依然属于耗时操作,需要在后台线程中从磁盘加载图片。另一方面,磁盘缓存需要一个初始化过程,也是异步完成,所以上面提供一个mDiskCacheLock 来保证DiskLruCache的访问同步。显然,磁盘缓存结合内存缓存是最佳的选择,上面数据从网络和从磁盘读取后都会同步到内存缓存中。

Bitmap内存管理

上面介绍了对Bitmap的缓存的实现,更进一步,下面来看看如何高效地释放Bitmap的内存,以及促进对它的复用。 首先,Bitmap的内存管理在不同的android版本中默认策略不同:

  • 在android 2.2(API 8)及更低的版本中,GC回收内存时主线程等待,而之后3.0 (API level 11)引入了并发的垃圾回收线程,这样,如果Bitmap不再被引用时,它对应的内存很快就会被回收。
  • 在2.3.3 (API level 10)版本及以前,Bitmap对应图片的像素数据是native内存中存储的,和Bitmap对象(在Dalvik堆内存中)是分开的。当Bitmap对象回收后对应的内存的回收行为不可预期,这样就会导致程序很容易达到内存边界。3.0版本就将像素数据和Bitmap对象存储在一起(Dalvik heap中 ),对象回收后对应像素数据也被释放。

android 2.3.3及更低版本的Bitmap内存管理

在2.3.3及以前版本中,android.graphics.Bitmap#recycle方法被推荐使用,调用后对应图片数据回尽快被回收掉。但确保对应图片的确不再使用了,因为方法执行后就不能再对对应的Bitmap做任何使用了,否则收到“"Canvas: trying to use a recycled bitmap"”这样的错误。

下面的代码演示了使用“引用计数”的方式来管理recycle()方法的执行,当一个Bitmap对象不再被显示或缓存时,就调用其recycle()方法主动释放其像素数据。

/**
 * A BitmapDrawable that keeps track of whether it is being displayed or cached.
 * When the drawable is no longer being displayed or cached,
 * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
 */
public class RecyclingBitmapDrawable extends BitmapDrawable {

    static final String TAG = "CountingBitmapDrawable";

    private int mCacheRefCount = 0;
    private int mDisplayRefCount = 0;

    private boolean mHasBeenDisplayed;

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    /**
     * Notify the drawable that the displayed state has changed. Internally a
     * count is kept so that the drawable knows when it is no longer being
     * displayed.
     *
     * @param isDisplayed - Whether the drawable is being displayed or not
     */
    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. Internally a count
     * is kept so that the drawable knows when it is no longer being cached.
     *
     * @param isCached - Whether the drawable is being cached or not
     */
    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()) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "No longer being used or cached so recycling. "
                        + toString());
            }

            getBitmap().recycle();
        }

    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}

android 3.0 及以上版本bitmap内存的管理

在3.0(API 11)版本后 增加了BitmapFactory.Options.inBitmap 字段,使用为此字段设置了Bitmap对象的参数的decode方法会尝试复用现有的bitmap内存,这样避免了内存的分配和回收。 不过实际的实现很受限,比如在4.4(API 19)版本以前,只有大小相同的bitmap可以被复用。

下面的代码中,使用一个HashSet来维护一个WeakReference的集合,它们引用了使用LruCache缓存被丢弃的那些Bitmap,这样后续的decode就可以复用它们。

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

复用已有的Bitmap

decode方法可以从前面的集合中先查看是否有可用的对象。

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的设置:

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

要知道并不一定可以找到“合适”的Bitmap来复用。方法canUseForInBitmap()通过对大小的检查来判定是否可以被复用:

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;
    } else if (config == Config.RGB_565) {
        return 2;
    } else if (config == Config.ARGB_4444) {
        return 2;
    } else if (config == Config.ALPHA_8) {
        return 1;
    }
    return 1;
}

在界面显示图片

下面分别示范ViewPager和GridView的形式来展示图片,综合了上面的异步加载,缓存等知识。

使用ViewPager

可以用ViewPager实现“swipe view pattern”,比如在图片浏览功能中左右滑动来查看不同的图片(上一个,下一个)。 既然使用ViewPager,就需要为它提供PagerAdapter子类。假设图片的预计内存使用不用太过担心,那么PagerAdapter或者FragmentPagerAdapter就够用了,更复杂的内存管理需求下,可以采用FragmentStatePagerAdapter,它在ViewPager显示的不同Fragment离开屏幕后自动销毁它们并保持其状态。

下面代码中,ImageDetailActivity中定义了显示用的ViewPager和它对应的ImagePagerAdapter:

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

类ImageDetailFragment作为ViewPager的item,用来展示一个图片的详细内容:

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

上面的图片加载是在UI线程中执行的,利用之前的AsyncTask实现的异步加载功能,将操作放在后台线程中去:

public class ImageDetailActivity extends FragmentActivity {
    ...

    public void loadBitmap(int resId, ImageView imageView) {
        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);
        }
    }
}

正如上面展示的那样,可以将需要的耗时处理放在BitmapWorkerTask中去执行。下面为整个代码加入缓存功能:

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 updated BitmapWorkerTask from Use a Memory Cache section
}

上面的代码实现使得图片的加载显示灰常流畅,如果还需要对图片施加额外的处理,都可以继续去扩展异步任务来实现。

使用GridView展示图片

网格视图的显示风格非常适合每个Item都是缩略图这样的情形。这时,同时在屏幕上会展示大量图片,随着滑动ImageView也会被回收利用。相比ViewPager每次展示一个图片的较大的情况,此时除了可以使用上面提到的缓存,异步加载技术外,一个需要处理的问题就是“并发”——异步加载时保证ImageView显示图片不会错乱。同样的问题在ListView中也是存在的,因为它们的re-use原则。

下面的ImageGridFragment 用来显示整个GridView,它里面同时定义了用到的BaseAdapter:

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

上面的代码暴露的问题就是异步加载和ImageView复用会产生错乱,下面使用之前异步加载图片中讨论过的“关联”技术来解决它:

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

以上的代码保证了对GridView展示的图片的异步加载不会导致错乱,必须牢记耗时操作不要阻塞UI,保证交互流畅。对应ListView上面的代码依然适用。

资料

  • sdk开发文档 Training > Displaying Bitmaps Efficiently, 目录:/docs/training/displaying-bitmaps/index.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏双十二技术哥

Android性能优化(五)之细说Bitmap

在上一篇《Android性能优化(四)之内存优化实战》中谈到那个内存中的大胖子Bitmap,Bitmap对内存的影响极大。 例如:使用Pixel手机拍摄404...

25350
来自专栏向治洪

listview异步加载图片并防止错位

android listview 异步加载图片并防止错位 ? 网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertV...

20770
来自专栏潇涧技术专栏

Android Text View with Custom Font

本文以自定义TextView为例简单实践下如何自定义View,它能够根据设置的xml属性采用不同的字体显示文字

8910
来自专栏郭霖

Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功...

2K110
来自专栏刘望舒

RxBinding使用和源码解析

作者 | juexingzhe 地址 | https://www.jianshu.com/u/ea71bb3770b4 声明 | 本文是 juexingzhe...

466100
来自专栏Vamei实验室

安卓第五夜 维纳斯的诞生

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

20530
来自专栏james大数据架构

实例演示Android异步加载图片

本文给大家演示异步加载图片的分析过程。让大家了解异步加载图片的好处,以及如何更新UI。 首先给出main.xml布局文件: 简单来说就是 LinearLayou...

22150
来自专栏知识分享

8-51单片机ESP8266学习-AT指令(测试TCP服务器--51单片机程序配置8266,做自己的手机TCP客户端发信息给单片机控制小灯的亮灭)

http://www.cnblogs.com/yangfengwu/p/8776712.html 先把源码和资料链接放到这里 链接:https://pan.ba...

63720
来自专栏iOSDevLog

Android 闪屏 Splash

29760
来自专栏james大数据架构

实例演示Android异步加载图片

本文给大家演示异步加载图片的分析过程。让大家了解异步加载图片的好处,以及如何更新UI。 首先给出main.xml布局文件: 简单来说就是 LinearLayou...

30180

扫码关注云+社区

领取腾讯云代金券