您的位置:首页 > 移动开发 > Android开发

Android高效加载位图避免OOM

2015-04-30 08:24 190 查看

概述

java.lang.OutofMemoryError: bitmap size exceeds VM budget.这个OOM异常应该是比较熟悉的。那么在小内存设备当中如何高效加载位图Bitmap是一个比较紧迫的问题。

Android中进行图片处理及加载操作一般不能在UI线程中进行。

有效地加载大位图文件

android.graphics.BitmapFactory

在不分配内存的前提下读取一个位图的尺寸和类型

BitmapFactory类提供了几个解码的方法(decodeByteArray(),decodeFile(),decodeResource(),等等)帮助你从多种资源创建位图。每种类型的解码方法都有额外的特征可以让你通过BitMapFactory.Options类指定解码选项。当解码时避免内存分配可以设置inJustDecodeBounds属性为true,位图对象返回null但是设置了outWidth,outHeight和outMimeType。这种技术允许你在创建位图(和分配内存)之前去读取图像的尺寸和类型。为了避免java.lang.OutOfMemory异常,在解码位图之前请检查它的尺寸,除非你十分确定资源提供给你的可预见的图像数据正好满足你的内存。

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;


加载一个缩小版本位图到内存中

需要考虑的前提:

估计加载完整图像所需要的内存;
你承诺加载这个图片所需空间带给你的程序的其他内存需求;
准备加载图像的目标ImageView或UI组件尺寸;
当前设备的屏幕尺寸和密度;

告诉解码器去重新采样这个图像,加载一个更小的版本到内存中,在你的BitmapFactory.Option对象中设置inSampleSize为true。例如,将一个分辨率为2048*1536的图像用 inSampleSize值为4去编码将产生一个大小为大约512*384的位图。加载这个到内存中仅使用0.75MB,而不是完整的12MB大小的图像

下面是一个示例用于计算SampleSize的值:
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) {
  if (width > height) {
  inSampleSize = Math.round((float)height / (float)reqHeight);
  } else {
  inSampleSize = Math.round((float)width / (float)reqWidth);
  }
  }
  return inSampleSize;
  }
要使用这种方法,首先解码,将inJustDecodeBounds设置为true,将选项传递进去,然后再次解码,在使用新的inSampleSize值并将inJustDecodeBounds设置为false:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,

  int reqWidth, int reqHeight) {

  // First decode with inJustDecodeBounds=true to check dimensions

  final BitmapFactory.Options options = new BitmapFactory.Options();

  options.inJustDecodeBounds = true;

  BitmapFactory.decodeResource(res, resId, options);

  // Calculate inSampleSize

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

  // Decode bitmap with inSampleSize set

  options.inJustDecodeBounds = false;

  return BitmapFactory.decodeResource(res, resId, options);
  }
这种方法可以很容易地加载任意大小的位图到一个ImageView显示一个100x100像素的缩略图,如下面的示例代码所示:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));


Processing Bitmaps Off the UI Thread

这节将通过在后台线程中使用AsyncTask处理位图,并处理并发问题.

使用一个AsyncTask

AsyncTask类提供了一种简单的方式来在一个后台线程中执行许多任务,并且把结果反馈给UI线程.使用的方法是,创建一个继承与它的子类并且实现提供的方法.这里是一个使用AsyncTask和decodeSampledBitmapFromResource()加载一个大图片到ImageView中的例子:

class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;

public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(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和任何它的引用被垃圾回收期回收.不能保证ImageView在任务完成后仍然存在,所以你必须在onPostExecute()方法中检查它的引用.ImageView可能不再存在,如果例如,如果在任务完成之前用户退出了活动或者配置发生了变化.
为了异步地加载位图,简单地创建一个新的任务并且执行它:

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


处理并发

常见的视图组件例如ListView和GridView如在上一节中当和AsyncTask结合使用时引出了另外一个问题.为了优化内存,当用户滚动时这些组件回收了子视图.如果每个子视图触发一个AsyncTask,当它完成时没法保证,相关的视图还没有被回收时已经用在了别的子视图当中.此外,还有异步任务开始的顺序是不能保证他们完成的顺序.

创建一个专用的Drawable的子类来存储一个引用备份工作任务.在这种情况下,一个BitmapDrawable被使用以便任务完成后一个占位符图像可以显示在ImageView中:

static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;

public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(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关联,cancelPotentialWork引用在上面的代码示例检查中.如果这样,它试图通过调用cancel()取消先前的任务

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;
}
这最后一步是在BitmapWorkerTask更新onPostExecute()方法,以便任务取消时并且当前任务和这个ImageView关联时进行检查:

class BitmapWorkerTask extends AsyncTask {
...

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


缓存位图

在许多情况下(例如有些组件像ListView,GridView以及ViewPager等),出现在屏幕上的图片总量,其中包括可能马上要滚动显示在屏幕上的那些图片,实际上是无限的.

当加载多个位图时使用一个内存和磁盘的位图缓存来提高响应速度以及提升整个UI界面的流畅性.

使用内存缓存LruCache

其原理是最近被引用的对象保存在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存.

注意:在过去,一个常用的内存缓存实现是一个SoftReference或WeakReference的位图缓存,然而现在不推荐使用.从android2.3(API 级别9)开始,垃圾回收器更加注重于回收软/弱引用,这使得使用以上引用很大程度上无效.

为了选择一个合适的的LruCache大小,许多因素应当被予以考虑,例如:

大量的图片是如何立刻出现在屏幕上的?需要多少即将在屏幕上显示的?
设备的屏幕的大小和分辨率是多少?
位图是什么尺寸和配置以及每张图片要占用多少内存?
图片访问频繁嘛?比起别的将会被频繁地访问吗?也许你可能总是想要在内存中保存一定项,甚至对于不同的位图组来说有多个LRUCache对象.
你能在数量和质量之间取得平衡嘛?有时对于存储更多的低质量的位图是更有用的,潜在地在另外的后台任务中加载一个更高质量版本.

缓存太小会导致额外的没有益处的开销,缓存过大会再次导致java.lang.OutOfMemory异常,并且只保留下你的应用程序其余相当少的内存来运行你的应用程序. 这个例子是针对位图来设置一个LruCache:(注意:一般需要覆盖sizeOf())

private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
注意:在这个例子中,应用程序中八分之一的内存被用作缓存.在一个普通的/hdpi设备中最低有4MB.在一个800*480分辨率的设备上全屏显示一个充满图片的GridView控件视图将使用1.5MB左右的缓存,所以这将在内存中缓冲至少四分之一的图片.

当将一张位图加载进一个ImageView中,LruCache首先被检查.如果有输入,缓存立刻被使用来更新这个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);
}
}
class BitmapWorkerTask extends AsyncTask {
...
// 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;
}
...
}

使用磁盘缓存DiskLruCache

一个内存缓冲对于加快访问最近浏览过的位图是很有用的,然而你不能局限图片在缓存中可用.像GridView这种具有更大的数据集的组件很容易地会占用所有内存缓存.你的应用程序会被别的任务像打电话等打断,并且当运行在后台时被进程杀死以及内存缓存被回收.一旦用户重新打开,你的应用程序不得不重新处理每一张图片.

在这种情况下使用磁盘缓存来持续处理位图,并且有助于在图片在内存缓存中不再可用时缩短加载时间.当然,从磁盘获取图片比从内存加载更慢并且应当在后台线程中处理,因为磁盘读取的时间是不可预知的. 注意:如果它们被更频繁地访问,那么一个ContentProvider可能是一个更合适的地方来存储缓存中的图像,例如在一个图片库应用程序里.

private DiskLruCache mDiskCache;
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
...
File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
...
}
class BitmapWorkerTask extends AsyncTask {
...
// 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(String.valueOf(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
if (!mDiskCache.containsKey(key)) {
mDiskCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromDiskCache(String key) {
return mDiskCache.get(key);
}
// 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 getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED
|| !Environment.isExternalStorageRemovable() ?
context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
当内存缓存在UI线程中被检查时,磁盘缓存在后台线程中被检查.磁盘操作应当永远不会发生在UI线程中.当图片处理完成时,最终的位图都将被添加到内存和磁盘缓存中

处理配置更改

程序运行时配置改变,例如屏幕的方向改变,导致系统销毁活动并且采用新的配置重新运行活动.你想要避免不得不再次处理所有的图片以使用户在配置发生改变时有一个平稳和快速的体验.
缓存能通过新的活动实例来使用一个Fragment,这个Fragment是通过调用setRetainInstance(true)方法被保留的.活动被重新构造后,保留的片段重新连接,并且你获得现有的高速缓存对象的访问权限,使得图片能快速的加载并重新填充到ImageView对象中.

接下来是一个使用Fragment将一个LruCache对象保留在配置更改中的范例:

private LruCache mMemoryCache;

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

class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache 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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: