在Android应用开发中不可避免的会用到图形图像,这样就会生成Bitmap对象。如果在开发过程中没有处理好Bitmap对象就很容易产生Out Of Memory(OOM)的异常。以下列举几点使用Bitmap对象需要注意的地方:
一个Android应用程序最多只能使用16M的内存,在Android的 Android Compatibility Definition Document (CDD) 3.7节中描述了不同屏幕分辨率及密度的设备在VM中会分配的大小。

Screen Size Screen Density Application Memory
small / normal / large ldpi / mdpi 16MB
small / normal / large tvdpi / hdpi 32MB
small / normal / large xhdpi 64MB
xlarge mdpi 32MB
xlarge tvdpi / hdpi 64MB
xlarge xhdpi 128MBBitmap

对象比较占用内存,特别像一些照片。比如使用Google Nexus照一张

分辨率为2592x1936的照片大概为5M,如果采用ARGB_8888的色彩格式(2.3之后默认使用该格式)加载这个图片就要占用19M内存(2592*1936*4 bytes),这样会导致某些设备直接挂掉。



Android中很多控件比如ListView/GridView/ViewPaper通常都会包含很多图片,特别是快速滑动的时候可能加载大量的图片,因此图片处理显得尤为重要。

下面会从四个方向讲述如何优化Bitmap的显示:
优化大图片 -- 注意Bitmap处理技巧,使其不会超过内存最大限值
通常情况下我们的UI并不需要很精致的图片。例如我们使用Gallery显示照相机拍摄的照片时,你的设备分辨率通常小于照片的分辨率。
BitmapFactory类提供了几个解码图片的方法(decodeByteArray(),decodeFile(),decodeResource()等),它们都可以通过BitmapFactory.Options指定解码选项。设置inJustDecodeBounds属性为true时解码并不会生成Bitmap对象,而是返回图片的解码信息(图片分辨率及类型:outWidth,outHeight,outMimeType)然后通过分辨率可以算出缩放值,再将inJustDecodeBounds设置为false,传入缩放值缩放图片,值得注意的是inJustDecodeBounds可能小于0,需要做判断。

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;
现在我们知道了图片的密度,在BitmapFactory.Options中设置inSampleSize值可以缩小图片。比如我们设置inSampleSize = 4,就会生成一个1/4长*1/4宽=1/16原始图的图片。当inSampleSize < 1的时候默认为1,系统提供了一个calculateInSampleSize()方法来帮我们算这个值:

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;
}
创建一个完整的缩略图方法:

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中:

mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
不要在UI线程中处理Bitmap -- 图片下载/调整大小等不要放在UI线程中处理,可以使用AsyncTask处理并发的问题。
刚刚我们提到过BitmapFactory.decode*的方法,值得注意的是这些方法都不能在UI线程中执行,因为他们的加载过程都是不可靠的,很可能引起应用程序的ANR。
如何解决这个问题呢?我们需要用到AsyncTask来处理并发。AsyncTask提供了一种简单的方法在后台线程中执行一些操作并反馈结果给UI线程。下面我们来看一个例子:

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

public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
当我们在ListView和GridView中使用AsyncTask的时候会引发一些问题,例如ListView快速滑动的时候其child view是循环未被回收的,我们也并不知道AsyncTask什么时候会完成,有可能AsyncTask还没执行完之前childView就已经被回收了,下面我们讲一种方法可以避免这种情况:
创建一个Drawable的子类来引用存储工作任务执行后返回的图片

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()方法

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);
}
}
}
}
缓存Bitmap -- 使用缓存可以改善图片加载速度提升用户体验
使用内存的Cache
从Android3.1开始,Google提供了一个缓存类叫LruCache,在此之前我们实现缓存通常都是用软引用或是弱引用,但是Google并不建议我们这样做,因为从Android2.3之后增加了GC回收的频率。
我们在使用LruCache的时候需要为它设置一个缓存大小,设置小了缓存没有作用,设置大了同样会导致OOM,因此设置缓存大小是一门技术活。

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);
}
这样当我们在ImageView中使用Bitmap的时候就可以先从缓存中获取,如果缓存没有就从网络中获取:

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 {
...
// 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.使用硬盘的Cache
我们会使用DiskLruCache来实现硬盘Cache

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);
}
值得一提的是Android API中并没有提供DiskLruCache接口,需要自己从4.x源码中移植至应用程序。源码地址:
libcore/luni/src/main/java/libcore/io/DiskLruCache.java

3.有时候在处理横竖屏切换的时候对象会全部重载,这样缓存就丢失了。为了避免这个问题,我们除了在Manifest中设置横竖屏不更新之外,就是使用Fragment做保存:

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

加入回收:

1.创建一个图片缓存对象HashMap dataCache,integer对应Adapter中的位置position,我们只用缓存处在显示中的图片,对于之外的位置,如果dataCache中有对应的图片,我们需要进行回收内存。在这个例子中,Adapter对象的getView方法首先判断该位置是否有缓存的bitmap,如果没有,则解码图片(bitmapDecoder.getPhotoItem,BitmapDecoder类见后面)并返回bitmap对象,设置dataCache 在该位置上的bitmap缓存以便之后使用;若是该位置存在缓存,则直接取出来使用,避免了再一次调用底层的解码图像需要的内存开销。有时为了提高 Gallery的更新速度,我们还可以预存储一些位置上的bitmap,比如存储显示区域位置外向上3个向下3个位置的bitmap,这样上或下滚动 Gallery时可以加快getView的获取。

java代码:


  1. public View getView(int position, View convertView, ViewGroup parent) {

  2. if(convertView==null){
  3. LayoutInflater inflater = LayoutInflater.from(context);
  4. convertView = inflater.inflate(R.layout.photo_item, null);
  5. holder = new ViewHolder();
  6. holder.photo = (ImageView) convertView.findViewById(R.id.photo_item_image);
  7. holder.photoTitle = (TextView) convertView.findViewById(R.id.photo_item_title);
  8. holder.photoDate = (TextView) convertView.findViewById(R.id.photo_item_date);
  9. convertView.setTag(holder);
  10. }else {
  11. holder = (ViewHolder) convertView.getTag();
  12. }
  13. cursor.moveToPosition(position);
  14. Bitmap current = dateCache.get(position);
  15. if(current != null){//如果缓存中已解码该图片,则直接返回缓存中的图片
  16. holder.photo.setImageBitmap(current);
  17. }else {
  18. current = bitmapDecoder.getPhotoItem(cursor.getString(1), 2) ;
  19. holder.photo.setImageBitmap(current);
  20. dateCache.put(position, current);
  21. }

  22. holder.photoTitle.setText(cursor.getString(2));
  23. holder.photoDate.setText(cursor.getString(4));
  24. return convertView;
  25. }

  26. }
复制代码


java代码:


  1. package eoe.bestjoy;

  2. import java.io.FileNotFoundException;
  3. import java.io.FileOutputStream;
  4. import android.content.Context;
  5. import android.graphics.Bitmap;
  6. import android.graphics.BitmapFactory;
  7. import android.graphics.Matrix;

  8. public class BitmapDecoder {

  9. private static final String TAG = "BitmapDecoder";
  10. private Context context;
  11. public BitmapDecoder(Context context) {
  12. this.context = context;
  13. }

  14. public Bitmap getPhotoItem(String filepath,int size) {
  15. BitmapFactory.Options options = new BitmapFactory.Options();
  16. options.inSampleSize=size;
  17. Bitmap bitmap = BitmapFactory.decodeFile(filepath,options);
  18. bitmap=Bitmap.createScaledBitmap(bitmap, 180, 251, true);
  19. //预先缩放,避免实时缩放,可以提高更新率
  20. return bitmap;
  21. }

  22. }
复制代码


2.由于Gallery控件的特点,总有一个item处于当前选择状态,我们利用此时进行dataCache中额外不用的bitmap的清理,来释放内存。

java代码:


  1. @Override
  2. public void onItemSelected(AdapterView<?> parent, View view, int position,long id) {

  3. releaseBitmap();
  4. Log.v(TAG, "select id:"+ id);
  5. }

  6. private void releaseBitmap(){
  7. //在这,我们分别预存储了第一个和最后一个可见位置之外的3个位置的bitmap
  8. //即dataCache中始终只缓存了(M=6+Gallery当前可见view的个数)M个bitmap
  9. int start = mGallery.getFirstVisiblePosition()-3;
  10. int end = mGallery.getLastVisiblePosition()+3;


  11. Log.v(TAG, "start:"+ start);
  12. Log.v(TAG, "end:"+ end);

  13. //释放position<start之外的bitmap资源
  14. Bitmap delBitmap;
  15. for(int del=0;del<start;del++){
  16. delBitmap = dateCache.get(del);
  17. if(delBitmap != null){
  18. //如果非空则表示有缓存的bitmap,需要清理
  19. Log.v(TAG, "release position:"+ del);
  20. //从缓存中移除该del->bitmap的映射
  21. dateCache.remove(del);
  22. delBitmap.recycle();
  23. }
  24. }

  25. freeBitmapFromIndex(end);
  26. }

  27. /**
  28. * 从某一位置开始释放bitmap资源
  29. * @param index
  30. */

  31. private void freeBitmapFromIndex(int end) {
  32. //释放之外的bitmap资源
  33. Bitmap delBitmap;
  34. for(int del =end+1;del<dateCache.size();del++){
  35. delBitmap = dateCache.get(del);
  36. if(delBitmap != null){
  37. dateCache.remove(del);
  38. delBitmap.recycle();
  39. Log.v(TAG, "release position:"+ del);
  40. }

  41. }
  42. }

BitmapFactory.decodeStreamBitmapFactory.decodeResource

后来在程序中去测试对比了一下,发现还是有比较大的差别的。同样是加载十张图片,我们先看看使用BitmapFactory.decodeResource后的内存占用情况:

?
代码片段,双击复制
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 //加载图片前的空余内存空间 long freeStart = Runtime.getRuntime().freeMemory(); bubble2 = BitmapFactory.decodeResource(resources, R.drawable.bubble2); bubble5 = BitmapFactory.decodeResource(resources, R.drawable.bubble5); bubble_2 = BitmapFactory.decodeResource(resources, R.drawable.bubble_2); speeding = BitmapFactory.decodeResource(resources, R.drawable.speeding); slowing = BitmapFactory.decodeResource(resources, R.drawable.slowing); resee = BitmapFactory.decodeResource(resources, R.drawable.resee); network = BitmapFactory.decodeResource(resources, R.drawable.network5); audio = BitmapFactory.decodeResource(resources, R.drawable.audio); eye_back = BitmapFactory.decodeResource(resources, R.drawable.eye_back); eye = BitmapFactory.decodeResource(resources, R.drawable.eye); //加载图片后的空余内存空间 long freeEnd = Runtime.getRuntime().freeMemory(); System.out.println( "freeStart:" +freeStart+ "\nfreeEnd:" +freeEnd+ "\n 相差:" +(freeStart-freeEnd));

2012-11-28 10:49 上传 下载附件 (5.29 KB)



再来看看使用BitmapFactory.decodeStream的情况:

?
代码片段,双击复制
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public Bitmap readBitmap(Context context, int id){ BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inPreferredConfig=Bitmap.Config.RGB_565; //表示16位位图 565代表对应三原色占的位数 opt.inInputShareable= true ; opt.inPurgeable= true ; //设置图片可以被回收 InputStream is = context.getResources().openRawResource(id); return BitmapFactory.decodeStream(is, null , opt); } //加载图片前的空余内存空间 long freeStart = Runtime.getRuntime().freeMemory(); bubble2 = utils.readBitmap(context, R.drawable.bubble2); bubble5 = utils.readBitmap(context, R.drawable.bubble5); bubble_2 = utils.readBitmap(context, R.drawable.bubble_2); speeding = utils.readBitmap(context, R.drawable.speeding); slowing = utils.readBitmap(context, R.drawable.slowing); resee = utils.readBitmap(context, R.drawable.resee); network = utils.readBitmap(context, R.drawable.network5); audio = utils.readBitmap(context, R.drawable.audio); eye_back = utils.readBitmap(context, R.drawable.eye_back); eye = utils.readBitmap(context, R.drawable.eye); //加载图片后的空余内存空间 long freeEnd = Runtime.getRuntime().freeMemory(); System.out.println( "freeStart:" +freeStart+ "\nfreeEnd:" +freeEnd+ "\n 相差:" +(freeStart-freeEnd));

2012-11-28 10:50 上传 下载附件 (4.88 KB)



从两个的运行结果中可以看出,使用 BitmapFactory.decodeResource 来设置图片资源要消耗更多的内存,如果程序中的图片资源很多的话,那这个内存就很客观啦。主要因为是 BitmapFactory.decodeResource 是通过Java层来createBitmap来完成图片的加载,增加了java层的内存消耗。而 BitmapFactory.decodeStream 则是直接调用了JNI,避免了java层的消耗。同时,在加载图片时,图片Config参数也可以有效减少内存的消耗。比如图片存储的位数及options.inSampleSize 图片的尺寸等。

更多相关文章

  1. Android异步下载网络图片(其三)
  2. Android面试系列文章2018之内存管理篇
  3. 【Android】更改程序图标
  4. DICOM入门(四)——Android读取DCM文件图片
  5. android 异常问题 Scrollview中嵌套webview出现大面积空白(第二次
  6. Android(安卓)非UI线程使用View.post()方法一处潜在的内存泄漏
  7. Android(安卓)读取Bitmap的几种方式
  8. Android实现QQ图片说说照片选择效果
  9. Android(安卓)Studio 编译缓存(Build Cache)

随机推荐

  1. Android菜谱应用客户端
  2. android上传图片并附带上传数据,文件流
  3. Android(安卓)R新特性:非SDK接口限制的更
  4. Android(安卓)4.4 Kitkat 音频实现及简要
  5. [置顶] Android即时通信软件设计(一)(网络通
  6. Android(安卓)内存泄露与优化,以及MAT工具
  7. Android/Linux 系统添加对多点触摸屏的支
  8. 给 Android(安卓)初学者的 Gradle 知识普
  9. Android实践——密码本SecretBook
  10. 团队Android小作品Chinchilla Guitar(琴琪