Android DisplayingBitmaps笔记
标签(空格分隔): Android开发
参考资料:
Android官方文档
Android异步任务处理从零开始
Android DiskLruCache完全解析,硬盘缓存的最佳方案
1. 概述
对Android中加载图片的优化,要追溯到进公司后接手的第一个项目。项目中有个模块就是通过GridView形式显示指定目录下的图片缩略图,点击缩略图后调用系统Gallery显示大图。
最初根据书上构建GridView适配器的方法,使用ViewHolder模式进行复用。出现了很多问题,比如:图片加载非常慢、ANR、GridView显示不美观等。后来学习了“Android异步任务处理从零开始”博客中的方法,有效地解决了这些问题,完成了显示图片模块的开发。最近,在学习Android官方文档的时候,发现官方文档中有个章节是专门来讲”Displaying Bitmaps Efficiently”,详细地讲解了Display Bitmaps时需要用到的技术,并给出了具体实例,让我很受启发。
本文便是在分析了源码的基础上,结合官方文档进行总结整理。官方文档给出的是加载网络图片的实例,我根据实际需求,修改为本地相册图片的加载。
2. Loading Large Bitmaps Efficiently
手机上的图像大小、形状各异。在很多情况下,图像的分辨率远超过手机设备的屏幕密度,这给手机的内存造成了很大负担,所以会出现卡顿、OOM甚至ANR现象。
考虑到手机的内存有限,而过高分辨率的图像并不会显示得更清楚细腻,且会浪费内存降低性能,所以理想的做法是根据用来显示图像的UI大小来对图像的分辨率进行合理匹配。
Read Bitmap Dimensions and Type
BitmapFactory是Android中提供的对图像的解析方法。BitmapFactory类提供了decodeResource、decodeFile、decodeFileDescriptor等方法根据不同资源来创建Bitmap。但是直接使用这些方法非常容易导致OOM。因此,Android提供了一个BitmapFactory.Options类来设置图像宽高、压缩比等属性。通过将inJustDecodeBounds属性设为true,可以在构建Bitmap对象之前获得图像的dimension、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;
Load a Scaled Down Version into Memory
现在,图像的dimensions信息已经知道了。接下来可以根据实际需要来决定加载原图像还是降采样图像。考虑因素如下:
- 估计加载原图像所需的内存
- 在考虑到你的应用其实内存需求的情况下,为加载这张图像分配多少内存。
- 加载图像的目标ImageView或UI组件的大小
- 屏幕尺寸以及设备的密度。
计算合适的压缩比inSampleSize,并将inJustDecodeBounds设为false,重新解析图像。例如:分辨率为2048x1536的一张图像,inSampleSize=4,可以构造出分辨率约为512x384的Bitmap。相比于加载原图像需要12MB内存,现在只需要为0.75MB。解析图像的代码如下:
/**
* Decode and sample down a bitmap from resources to the requested width and height.
*
* @param res The resources object containing the image data
* @param resId The resource id of the image data
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight, ImageCache cache) {
// BEGIN_INCLUDE (read_bitmap_dimensions)
// 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);
// END_INCLUDE (read_bitmap_dimensions)
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* Decode and sample down a bitmap from a file to the requested width and height.
*
* @param filename The full path of the file to decode
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromFile(String filename,
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.decodeFile(filename, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options);
}
/**
* Decode and sample down a bitmap from a file input stream to the requested width and height.
*
* @param fileDescriptor The file descriptor to read from
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
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);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
* the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap
* having a width and height equal to or larger than the requested width and height.
*
* @param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// BEGIN_INCLUDE (calculate_sample_size)
// 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;
// END_INCLUDE (calculate_sample_size)
}
然后,通过setImageBitmap方法就可以加载图像了。
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
这里也可以类似的方法解析其它资源图像,采用相应的BitmapFactory.decode*方法即可。
3. Processing Bitmaps Off the UI Thread
到目前为止,上一节讨论的BitmapFactory.decode*方法都是在主UI线程执行。无论是从硬盘加载还是网络下载的图像,加载时间都依赖于硬盘或网络的读取速度、图像大小、CPU性能等因素。因此容易造成主UI线程阻塞而使应用ANR。
这一节主要讨论如何通过AsyncTask在子线程处理Bitmaps,并处理并发事件。
Use an AsyncTask
AsyncTask类提供了一种方法在子线程来执行任务,并将结果返回至UI线程。它的使用方法很简单,只需要创建一个AsyncTask子类并复写方法。下面是一个使用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);
}
}
}
}
BitmapWorkerTask首先在构造函数中将要加载图像的imageView保存到WeakReference中,这是为了防止任务完成后imageView被garbage回收。例如,用户在任务完成前退出了当前activity或切换横竖屏。所以必须在onPostExecute中检查imageView还在不在。
通过创建一个BitmapWorkerTask并执行,就可以开始异步加载图像了。
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
Handle Concurrency
通常,我们会采用ListView和GridView组件并通过AsyncTask来显示图像。为了有效利用内存,这些组件会在用户滚动屏幕时循环利用child views。如果每个child view都触发一个AsyncTask,那么无法保证当任务完成后,绑定的view是否已被其它child view循环利用了。即该view与AsyncTask已经不是相对应的了,这时如果调用loadBitmap来加载图像就会出现张冠李戴的情况。况且,ListView和GridView的显示顺序也会出现错误。
既然问题的原因是触发的AsyncTask完成后与view不对应,那么解决思路也就有了。
首先,通过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;
}
AsyncDrawable是一个继承BitmapDrawable的子类,它保存了绑定BitmapWorkerTask的引用。AsyncDrawable的构造函数中有一个Bitmap类型的placeholder image,用来在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();
}
}
在执行BitmapWorkerTask之前,创建一个AsyncDrawable对象并将其绑定到指定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);
}
}
由于每个ImageView都会启动一个BitmapWorkerTask,而用户在来回滑动ListView和GridView时,当前页面中的ImageView已经启动了BitmapWorkerTask。那么,这时就没有必要重复启动了。因此在启动BitmapWorkerTask之前,需要先进行判断任务是否启动,启动的任务与传入的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;
}
最后在onPostExecute()更新ImageView。检查任务是否取消,当前任务是否与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);
}
}
}
}
4. Caching Bitmaps
加载一张图像的操作是非常简单直观的,然而当需要一次性加载大量图像时,事情就变得复杂了。
Android的内存管理机制是,当child views退出屏幕后,系统的garbage collector会重用这些child views并释放其加载的bitmaps。这样一来,我们为ImageView加载过bitmap后,重新浏览时还要重新加载一遍,这非常影响图像显示的流畅性。因此,为了快速重载加载过的图像,需要用到内存和硬盘缓存技术。
Use a Memory Cache
Google提供了一套内存缓存技术。内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。LruCache 是在support-v4中才引入的,在引入LruCache 之前,Google建议的是使用软引用或弱引用 (SoftReference or WeakReference)来进行内存缓存。但是从Android2.3开始,GC算法修改,软引用与弱引用同样会优先被GC回收
为bitmaps创建一个LruCache的例子:
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:在这个示例中,为缓存分配了1/8的应用内存(约4MB)。
在分辨率800*480的设备上,一个加载了图像的满屏GridView消耗的内存约为1.5MB,因此至少可以缓存2.5页。
在加载bitmap到ImageView之前,首先检查LruCache中是否保存有备份。如果有则直接使用该bitmap更新ImageView,否则启动BitmapWorkerTask。
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);
}
}
同时,需要在doInBackgroud中将解析的bitmap备份到LruCache。
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;
}
...
}
Use a Disk Cache
内存缓存对提高访问最近浏览过的图像速度非常有效,但它具有不稳定性。像GridView这种组件加载大量图像时就很有可能会出现内存缓存溢出。另外,应用可能会被像来电等其它任务中断,使得该应用和内存缓存被销毁。从而当用户resume时,应用又得重新加载图像。
在内存缓存中的图像不可用的情况下,可以使用硬盘缓存来持续处理bitmaps从而减少加载时间。由于硬盘读取时间的不可控性,从硬盘获取图像的时间要慢于内存缓存,所以应该放在子线程中。
Android的缓存技术也是使用了这样一个特性,总的来说,使用二级缓存的方案,就是先从一级缓存——内存中拿,没有的话,再去二级缓存——手机中拿,如果还没有,那就只能去下载了。
有了DiskLruCache,我们就可以很方便的将一部分内容缓存到手机存储中,做暂时的持久化保存,像我们经常用的一些新闻聚合类App、ZARKER等,基本都利用了DiskLruCache,浏览过的网页,即使在没有网络的情况下,也可以浏览。如果访问十分频繁的话,ContentProvider更适合用来保存缓存图像,例如图像gallery应用。
添加硬盘缓存的代码示例如下:
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确保了DiskCache初始化完成之前,app无法从硬盘缓存中读取数据。
Handle Configuration Changes
应用运行时,当configuration变化时,比如屏幕横竖屏切换,Android将会销毁并根据新configuration重启该Activity。这时为了获得一个快速流畅的应用体验,需要避免重新再加载图像。
通过调用setRetainInstance(true)方法创建一个保留的Fragment,可以将缓存传递给新的activity实例。当activity被重新创建时,这个保留的Fragment将被reattached,就可以访问存在的缓存对象了。
示例如下:
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();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
5. Managing Bitmap Memory
Manage Memory on Android 3.0 and Higher
Android 3.0 (API 11)引入了BitmapFactory.Option.inBitmap,设置option后,在加载内容时解析方法将会试图重用已存在的bitmap。这意味着bitmap所占的内存被重复利用了,不仅提升了性能,而且减少了内存分配。不过使用inBitmap也有限制,特别是Android 4.4 (API 19)之前,只支持同等大小的bitmaps。
Save a bitmap for later use
在Android 3.0或更高版本,当某个bitmap从LruCache中释放时,可以通过Set<SoftReference<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()));
}
}
}
....
}
Use an existing bitmap
在解析图像的时候,首先检查是否有可以重用的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);
}
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;
}
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;
}