Android Bit地图图像优化
Android Bitmap图像优化
创建一个Drawable的子类来引用存储工作任务执行后返回的图片
我们会使用DiskLruCache来实现硬盘Cache
3.有时候在处理横竖屏切换的时候对象会全部重载,这样缓存就丢失了。为了避免这个问题,我们除了在Manifest中设置横竖屏不更新之外,就是使用Fragment做保存:
在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 | 128MB |
- Bitmap对象比较占用内存,特别像一些照片。比如使用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); } }原文:http://developer.android.com/training/displaying-bitmaps/index.html