处理大图片(2)

来源:互联网 发布:2k17樱木花道脸部数据 编辑:程序博客网 时间:2024/03/29 21:27

Android Bitmap图像优化

分类: Android App 3242人阅读 评论(5)收藏 举报

试一试:点击下载

    

    在Android应用开发中不可避免的会用到图形图像,这样就会生成Bitmap对象。如果在开发过程中没有处理好Bitmap对象就很容易产生Out Of Memory(OOM)的异常。以下列举几点使用Bitmap对象需要注意的地方:

  •     一个Android应用程序最多只能使用16M的内存,在Android的 Android Compatibility Definition Document (CDD) 3.7节中描述了不同屏幕分辨率及密度的设备在VM中会分配的大小。                                                                       
Screen Size Screen Density Application Memorysmall / normal / large ldpi / mdpi 16MBsmall / normal / large tvdpi / hdpi 32MBsmall / normal / large xhdpi 64MBxlarge mdpi 32MBxlarge tvdpi / hdpi 64MBxlarge 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,需要做判断。
[java] view plaincopy
  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2. options.inJustDecodeBounds = true;  
  3. BitmapFactory.decodeResource(getResources(), R.id.myimage, options);  
  4. int imageHeight = options.outHeight;  
  5. int imageWidth = options.outWidth;  
  6. String imageType = options.outMimeType;  
          现在我们知道了图片的密度,在BitmapFactory.Options中设置inSampleSize值可以缩小图片。比如我们设置inSampleSize = 4,就会生成一个1/4长*1/4宽=1/16原始图的图片。当inSampleSize < 1的时候默认为1,系统提供了一个calculateInSampleSize()方法来帮我们算这个值:
[java] view plaincopy
  1. public static int calculateInSampleSize(  
  2.             BitmapFactory.Options options, int reqWidth, int reqHeight) {  
  3.     // Raw height and width of image  
  4.     final int height = options.outHeight;  
  5.     final int width = options.outWidth;  
  6.     int inSampleSize = 1;  
  7.   
  8.     if (height > reqHeight || width > reqWidth) {  
  9.         if (width > height) {  
  10.             inSampleSize = Math.round((float)height / (float)reqHeight);  
  11.         } else {  
  12.             inSampleSize = Math.round((float)width / (float)reqWidth);  
  13.         }  
  14.     }  
  15.     return inSampleSize;  
  16. }  
          创建一个完整的缩略图方法:
[java] view plaincopy
  1. public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,  
  2.         int reqWidth, int reqHeight) {  
  3.   
  4.     // First decode with inJustDecodeBounds=true to check dimensions  
  5.     final BitmapFactory.Options options = new BitmapFactory.Options();  
  6.     options.inJustDecodeBounds = true;  
  7.     BitmapFactory.decodeResource(res, resId, options);  
  8.   
  9.     // Calculate inSampleSize  
  10.     options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
  11.   
  12.     // Decode bitmap with inSampleSize set  
  13.     options.inJustDecodeBounds = false;  
  14.     return BitmapFactory.decodeResource(res, resId, options);  
  15. }  
        我们把它设进ImageView中:
[java] view plaincopy
  1. mImageView.setImageBitmap(  
  2.     decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100100));  
  • 不要在UI线程中处理Bitmap -- 图片下载/调整大小等不要放在UI线程中处理,可以使用AsyncTask处理并发的问题。

        刚刚我们提到过BitmapFactory.decode*的方法,值得注意的是这些方法都不能在UI线程中执行,因为他们的加载过程都是不可靠的,很可能引起应用程序的ANR。
        如何解决这个问题呢?我们需要用到AsyncTask来处理并发。AsyncTask提供了一种简单的方法在后台线程中执行一些操作并反馈结果给UI线程。下面我们来看一个例子:

[java] view plaincopy
  1. class BitmapWorkerTask extends AsyncTask {  
  2.     private final WeakReference imageViewReference;  
  3.     private int data = 0;  
  4.   
  5.     public BitmapWorkerTask(ImageView imageView) {  
  6.         // Use a WeakReference to ensure the ImageView can be garbage collected  
  7.         imageViewReference = new WeakReference(imageView);  
  8.     }  
  9.   
  10.     // Decode image in background.  
  11.     @Override  
  12.     protected Bitmap doInBackground(Integer... params) {  
  13.         data = params[0];  
  14.         return decodeSampledBitmapFromResource(getResources(), data, 100100));  
  15.     }  
  16.   
  17.     // Once complete, see if ImageView is still around and set bitmap.  
  18.     @Override  
  19.     protected void onPostExecute(Bitmap bitmap) {  
  20.         if (imageViewReference != null && bitmap != null) {  
  21.             final ImageView imageView = imageViewReference.get();  
  22.             if (imageView != null) {  
  23.                 imageView.setImageBitmap(bitmap);  
  24.             }  
  25.         }  
  26.     }  
  27. }  
[java] view plaincopy
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
  3.     task.execute(resId);  
  4. }  
当我们在ListView和GridView中使用AsyncTask的时候会引发一些问题,例如ListView快速滑动的时候其child view是循环未被回收的,我们也并不知道AsyncTask什么时候会完成,有可能AsyncTask还没执行完之前childView就已经被回收了,下面我们讲一种方法可以避免这种情况:
创建一个Drawable的子类来引用存储工作任务执行后返回的图片
[java] view plaincopy
  1. static class AsyncDrawable extends BitmapDrawable {  
  2.     private final WeakReference bitmapWorkerTaskReference;  
  3.   
  4.     public AsyncDrawable(Resources res, Bitmap bitmap,  
  5.             BitmapWorkerTask bitmapWorkerTask) {  
  6.         super(res, bitmap);  
  7.         bitmapWorkerTaskReference =  
  8.             new WeakReference(bitmapWorkerTask);  
  9.     }  
  10.   
  11.     public BitmapWorkerTask getBitmapWorkerTask() {  
  12.         return bitmapWorkerTaskReference.get();  
  13.     }  
  14. }  
在执行BitmapWorkerTask之前,创建一个AsyncDrawable来绑定目标的ImageView:
[java] view plaincopy
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     if (cancelPotentialWork(resId, imageView)) {  
  3.         final BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
  4.         final AsyncDrawable asyncDrawable =  
  5.                 new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);  
  6.         imageView.setImageDrawable(asyncDrawable);  
  7.         task.execute(resId);  
  8.     }  
  9. }  
在给ImageView赋值之前会调用cancelPotentialWork方法,它会使用cancel()方法尝试取消已经过期的任务。
[java] view plaincopy
  1. public static boolean cancelPotentialWork(int data, ImageView imageView) {  
  2.     final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);  
  3.   
  4.     if (bitmapWorkerTask != null) {  
  5.         final int bitmapData = bitmapWorkerTask.data;  
  6.         if (bitmapData != data) {  
  7.             // Cancel previous task  
  8.             bitmapWorkerTask.cancel(true);  
  9.         } else {  
  10.             // The same work is already in progress  
  11.             return false;  
  12.         }  
  13.     }  
  14.     // No task associated with the ImageView, or an existing task was cancelled  
  15.     return true;  
  16. }  
[java] view plaincopy
  1. private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {  
  2.    if (imageView != null) {  
  3.        final Drawable drawable = imageView.getDrawable();  
  4.        if (drawable instanceof AsyncDrawable) {  
  5.            final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;  
  6.            return asyncDrawable.getBitmapWorkerTask();  
  7.        }  
  8.     }  
  9.     return null;  
  10. }  
最后一步,修改BitmapWorkerTask中的onPostExecute()方法
[java] view plaincopy
  1. class BitmapWorkerTask extends AsyncTask {  
  2.     ...  
  3.   
  4.     @Override  
  5.     protected void onPostExecute(Bitmap bitmap) {  
  6.         if (isCancelled()) {  
  7.             bitmap = null;  
  8.         }  
  9.   
  10.         if (imageViewReference != null && bitmap != null) {  
  11.             final ImageView imageView = imageViewReference.get();  
  12.             final BitmapWorkerTask bitmapWorkerTask =  
  13.                     getBitmapWorkerTask(imageView);  
  14.             if (this == bitmapWorkerTask && imageView != null) {  
  15.                 imageView.setImageBitmap(bitmap);  
  16.             }  
  17.         }  
  18.     }  
  19. }  
  • 缓存Bitmap -- 使用缓存可以改善图片加载速度提升用户体验
  1. 使用内存的Cache

       从Android3.1开始,Google提供了一个缓存类叫LruCache,在此之前我们实现缓存通常都是用软引用或是弱引用,但是Google并不建议我们这样做,因为从Android2.3之后增加了GC回收的频率。
       我们在使用LruCache的时候需要为它设置一个缓存大小,设置小了缓存没有作用,设置大了同样会导致OOM,因此设置缓存大小是一门技术活。

[java] view plaincopy
  1. private LruCache mMemoryCache;  
  2.   
  3. @Override  
  4. protected void onCreate(Bundle savedInstanceState) {  
  5.     ...  
  6.     // Get memory class of this device, exceeding this amount will throw an  
  7.     // OutOfMemory exception.  
  8.     final int memClass = ((ActivityManager) context.getSystemService(  
  9.             Context.ACTIVITY_SERVICE)).getMemoryClass();  
  10.   
  11.     // Use 1/8th of the available memory for this memory cache.  
  12.     final int cacheSize = 1024 * 1024 * memClass / 8;  
  13.   
  14.     mMemoryCache = new LruCache(cacheSize) {  
  15.         @Override  
  16.         protected int sizeOf(String key, Bitmap bitmap) {  
  17.             // The cache size will be measured in bytes rather than number of items.  
  18.             return bitmap.getByteCount();  
  19.         }  
  20.     };  
  21.     ...  
  22. }  
  23.   
  24. public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
  25.     if (getBitmapFromMemCache(key) == null) {  
  26.         mMemoryCache.put(key, bitmap);  
  27.     }  
  28. }  
  29.   
  30. public Bitmap getBitmapFromMemCache(String key) {  
  31.     return mMemoryCache.get(key);  
  32. }  
            这样当我们在ImageView中使用Bitmap的时候就可以先从缓存中获取,如果缓存没有就从网络中获取:
[java] view plaincopy
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     final String imageKey = String.valueOf(resId);  
  3.   
  4.     final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
  5.     if (bitmap != null) {  
  6.         mImageView.setImageBitmap(bitmap);  
  7.     } else {  
  8.         mImageView.setImageResource(R.drawable.image_placeholder);  
  9.         BitmapWorkerTask task = new BitmapWorkerTask(mImageView);  
  10.         task.execute(resId);  
  11.     }  
  12. }  
            我们需要更新一下刚刚写的BitmapWorkerTask
[java] view plaincopy
  1. class BitmapWorkerTask extends AsyncTask {  
  2.     ...  
  3.     // Decode image in background.  
  4.     @Override  
  5.     protected Bitmap doInBackground(Integer... params) {  
  6.         final Bitmap bitmap = decodeSampledBitmapFromResource(  
  7.                 getResources(), params[0], 100100));  
  8.         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);  
  9.         return bitmap;  
  10.     }  
  11.     ...  
  12. }  
        2.使用硬盘的Cache
           我们会使用DiskLruCache来实现硬盘Cache
[java] view plaincopy
  1. private DiskLruCache mDiskCache;  
  2. private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10// 10MB  
  3. private static final String DISK_CACHE_SUBDIR = "thumbnails";  
  4.   
  5. @Override  
  6. protected void onCreate(Bundle savedInstanceState) {  
  7.     ...  
  8.     // Initialize memory cache  
  9.     ...  
  10.     File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);  
  11.     mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);  
  12.     ...  
  13. }  
  14.   
  15. class BitmapWorkerTask extends AsyncTask {  
  16.     ...  
  17.     // Decode image in background.  
  18.     @Override  
  19.     protected Bitmap doInBackground(Integer... params) {  
  20.         final String imageKey = String.valueOf(params[0]);  
  21.   
  22.         // Check disk cache in background thread  
  23.         Bitmap bitmap = getBitmapFromDiskCache(imageKey);  
  24.   
  25.         if (bitmap == null) { // Not found in disk cache  
  26.             // Process as normal  
  27.             final Bitmap bitmap = decodeSampledBitmapFromResource(  
  28.                     getResources(), params[0], 100100));  
  29.         }  
  30.   
  31.         // Add final bitmap to caches  
  32.         addBitmapToCache(String.valueOf(imageKey, bitmap);  
  33.   
  34.         return bitmap;  
  35.     }  
  36.     ...  
  37. }  
  38.   
  39. public void addBitmapToCache(String key, Bitmap bitmap) {  
  40.     // Add to memory cache as before  
  41.     if (getBitmapFromMemCache(key) == null) {  
  42.         mMemoryCache.put(key, bitmap);  
  43.     }  
  44.   
  45.     // Also add to disk cache  
  46.     if (!mDiskCache.containsKey(key)) {  
  47.         mDiskCache.put(key, bitmap);  
  48.     }  
  49. }  
  50.   
  51. public Bitmap getBitmapFromDiskCache(String key) {  
  52.     return mDiskCache.get(key);  
  53. }  
  54.   
  55. // Creates a unique subdirectory of the designated app cache directory. Tries to use external  
  56. // but if not mounted, falls back on internal storage.  
  57. public static File getCacheDir(Context context, String uniqueName) {  
  58.     // Check if media is mounted or storage is built-in, if so, try and use external cache dir  
  59.     // otherwise use internal cache dir  
  60.     final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED  
  61.             || !Environment.isExternalStorageRemovable() ?  
  62.                     context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();  
  63.   
  64.     return new File(cachePath + File.separator + uniqueName);  
  65. }  
值得一提的是Android API中并没有提供DiskLruCache接口,需要自己从4.x源码中移植至应用程序。源码地址:
libcore/luni/src/main/java/libcore/io/DiskLruCache.java

3.有时候在处理横竖屏切换的时候对象会全部重载,这样缓存就丢失了。为了避免这个问题,我们除了在Manifest中设置横竖屏不更新之外,就是使用Fragment做保存:
[java] view plaincopy
  1. private LruCache mMemoryCache;  
  2.   
  3. @Override  
  4. protected void onCreate(Bundle savedInstanceState) {  
  5.     ...  
  6.     RetainFragment mRetainFragment =  
  7.             RetainFragment.findOrCreateRetainFragment(getFragmentManager());  
  8.     mMemoryCache = RetainFragment.mRetainedCache;  
  9.     if (mMemoryCache == null) {  
  10.         mMemoryCache = new LruCache(cacheSize) {  
  11.             ... // Initialize cache here as usual  
  12.         }  
  13.         mRetainFragment.mRetainedCache = mMemoryCache;  
  14.     }  
  15.     ...  
  16. }  
  17.   
  18. class RetainFragment extends Fragment {  
  19.     private static final String TAG = "RetainFragment";  
  20.     public LruCache mRetainedCache;  
  21.   
  22.     public RetainFragment() {}  
  23.   
  24.     public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {  
  25.         RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);  
  26.         if (fragment == null) {  
  27.             fragment = new RetainFragment();  
  28.         }  
  29.         return fragment;  
  30.     }  
  31.   
  32.     @Override  
  33.     public void onCreate(Bundle savedInstanceState) {  
  34.         super.onCreate(savedInstanceState);  
  35.         setRetainInstance(true);  
  36.     }  
  37. }  
原文:http://developer.android.com/training/displaying-bitmaps/index.html
0 0