试一试:点击下载

在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 -- 使用缓存可以改善图片加载速度提升用户体验
  1. 使用内存的Cache

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

private LruCache mMemoryCache;@Overrideprotected 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; // 10MBprivate static final String DISK_CACHE_SUBDIR = "thumbnails";@Overrideprotected 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;@Overrideprotected 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

更多相关文章

  1. 箭头函数的基础使用
  2. NPM 和webpack 的基础使用
  3. Python list sort方法的具体使用
  4. 【阿里云镜像】使用阿里巴巴DNS镜像源——DNS配置教程
  5. android 使用html5作布局文件: webview跟javascript交互
  6. Android(安卓)matrix 控制图片的旋转、缩放、移动
  7. Android(安卓)Resource介绍和使用
  8. "Failed to fetch URL https://dl-ssl.google.com/android/repos
  9. 使用NetBeans搭建Android开发环境

随机推荐

  1. someone's android note
  2. 【Android(安卓)studio】No cached versi
  3. android 之json对象解析并展示(含json解
  4. Android(安卓)SystemServer学习之二
  5. Android系统之System Server大纲
  6. Android 记忆卡片游戏 记忆力 Android游
  7. 移动端开发三国时代
  8. android 快速滚动条设置(像新浪微博,滚动
  9. Android判断两个时间的间隔
  10. Android中开发版(debug)和发布版(release