Android加载图片你必须知道的技巧

来源:互联网 发布:箪食壶浆以迎将军者乎 编辑:程序博客网 时间:2024/06/05 15:47

学习如何处理和加载Bitmap,显示在UI上非常的重要。如果你不重视这块,Bitmap讲很快耗尽你的内存资源,最终导致oom内存溢出。

  • 移动设备的内存资源很稀缺,很多时候每个应用只能分配到16MB的内存空间。部分机型可能分配的会更多,但是我们必须保证不超过最大内存的限制。
  • Bitmaps本身就非常占用资源。比如一个Galaxy Nexus拍一张照片2592x1936分辨率。如果使用ARGB_8888(2.3版本以后默认值)加载bitmap的话,加载这张图将耗费将近19MB(2592*1936*4 bytes)的内存,直接就超过了很多机器的最大内存。
  • 有时候针对ListView, GridView 和 ViewPager 这种控件,我们会有显示很多图片的需求,也要为即将可能显示在屏幕上的图片做处理,让图片为显示做好准备。

加载大图片

图片什么大小和形状都有。经常图片比你的UI控件大得多。例如摄像头拍出来的照片比你手机屏幕的分辨率高得多。

获取Bitmap大小和类型

BitmapFactory提供了很多通过各种各样渠道解码的方法(decodeByteArray(), decodeFile(), decodeResource(), 等等.) 。这些方法都很容易引起oom内存溢出。每个方法都可以通过BitmapFactory.Options这个类去指定解码选项。把inJustDecodeBounds设定成true,然后进行解码,这个时候就不会真的去分配内存,而是返回空的bitmap同时也会返回outWidth, outHeight 和 outMimeType.有了这三个值,我们就可以按照需要对图片进行压缩。

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;

除非你对图片的来源有绝对的信心,不然建议每次解码都需要检查图片的大小和类型。

按比例压缩后加载

你需要注意这些问题
- 预估加载整个图片需要多少内存
- 愿意从整个应用中,分配多少内存给这张图
- 目标UI控件比如ImageView 的大小
- 当前设备的屏幕分辨率和尺寸

例如把一个1024x768的图片全部加载显示在一个128x96像素的ImageView.上是没有价值的。
下面是根据目标控件传入的宽和高,计算出合适的inSampleSize值的方法。

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) {        final int halfHeight = height / 2;        final int halfWidth = width / 2;        // Calculate the largest inSampleSize value that is a power of 2 and keeps both        // height and width larger than the requested height and width.        while ((halfHeight / inSampleSize) > reqHeight                && (halfWidth / inSampleSize) > reqWidth) {            inSampleSize *= 2;        }    }    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上。

mImageView.setImageBitmap(    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

避免在UI线程上处理图片

如果图片来源在磁盘或者网络上(或者其他内存之外的媒介),上面讨论的BitmapFactory.decode*方法不应该在主UI线程上执行。加载图片需要的时间是不可预知的,依赖于很多因素(磁盘读取速度,网速,图片大小,CPU速度等)。任何一个工作都可以阻塞UI线程,造成无响应。下面说说用子线程加载bitmaps的问题。

AsyncTask

AsyncTask应该是大家都很熟悉的子线程通知UI线程修改的方法。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {    private final WeakReference<ImageView> imageViewReference;    private int data = 0;    public BitmapWorkerTask(ImageView imageView) {        // Use a WeakReference to ensure the ImageView can be garbage collected        imageViewReference = new WeakReference<ImageView>(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);            }        }    }}

这段代码很简单,但是可以看到好的代码习惯,比如使用软引用避免AsyncTask的引用导致内存泄漏。判断是否为空,避免空指针异常。使用代码如下:

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

并发的使用

如果我们使用ListView 和 GridView这种重复利用子控件的UI控件时,如果我们使用上面的AsyncTask,当任务完成的时候 无法保证该子控件是否已经被重用了。此外,任务开始的顺序和完成的顺序也无法保证。
下面是解决方案,创建一个专门的 Drawable 子类来储存载入图片的任务引用。这样使用BitmapDrawable,当任务完成的时候placeholder 中的图片就能在ImageView显示了。

static class AsyncDrawable extends BitmapDrawable {    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;    public AsyncDrawable(Resources res, Bitmap bitmap,            BitmapWorkerTask bitmapWorkerTask) {        super(res, bitmap);        bitmapWorkerTaskReference =            new WeakReference<BitmapWorkerTask>(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);    }}

cancelPotentialWork方法用来检查是否已经有任务绑定到,如果已经有一个任务了就尝试取消这个任务(调用 cancel()方法)。
在少数情况下,如果新的任务和已经存在任务的数据一样,则不需要特殊额外的处理。下面是 cancelPotentialWork 方法的一种实现:

public static boolean cancelPotentialWork(int data, ImageView imageView) {    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);    if (bitmapWorkerTask != null) {        final int bitmapData = bitmapWorkerTask.data;        // If bitmapData is not yet set or it differs from the new data        if (bitmapData == 0 || 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;}

方法 getBitmapWorkerTask(),获取和 ImageView关联的任务:

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<Integer, Void, Bitmap> {    ...    @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 ,也适用于其他有子控件回收机制的控件。简单的在设置ImageView图片的地方调用loadBitmap 方法就可以。比如在GridView的getView方法中调用loadBitmap。


缓存Bitmaps

加载一张图片很简单,加载一大堆图片就麻烦了,比如 ListView, GridView 或者 ViewPager。LruCache 是官方推荐的缓存图片的类,低版本可以使用v4支持包。

  • 你的设备可以为每个应用程序分配多大的内存?
  • 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
  • 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
  • 图片的尺寸和大小,还有每张图片会占据多少内存空间。
  • 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
  • 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。

下面是一个例子:

private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected void onCreate(Bundle savedInstanceState) {    ...    // Get max available VM memory, exceeding this amount will throw an    // OutOfMemory exception. Stored in kilobytes as LruCache takes an    // int in its constructor.    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);    // Use 1/8th of the available memory for this memory cache. 使用最大可以内存的八分之一作为LruCache的缓存大小    final int cacheSize = maxMemory / 8;    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {        @Override        protected int sizeOf(String key, Bitmap bitmap) {            // The cache size will be measured in kilobytes rather than 这个是必须实现的,返回的是每个bitmap的大小,每次添加bitmap时都会调用            // number of items.            return bitmap.getByteCount() / 1024;        }    };    ...}public void addBitmapToMemoryCache(String key, Bitmap bitmap) {    if (getBitmapFromMemCache(key) == null) {        mMemoryCache.put(key, bitmap);    }}public Bitmap getBitmapFromMemCache(String key) {    return mMemoryCache.get(key);}

当加载bitmap到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);    }}

BitmapWorkerTask 也需要把新加载的图片的键值对放到缓存中。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {    ...    // 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;    }    ...}

使用磁盘缓存

如果使用GridView LruCache很容易超出内存限制, DiskLruCache是官方推荐做持久化的缓存类,把图片缓存到磁盘内避免反复网络下载图片也能提高加载速度。

private DiskLruCache mDiskLruCache;private final Object mDiskCacheLock = new Object();private boolean mDiskCacheStarting = true;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    ...    // Initialize disk cache on background thread    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);    new InitDiskCacheTask().execute(cacheDir);    ...}class InitDiskCacheTask extends AsyncTask<File, Void, Void> {    @Override    protected Void doInBackground(File... params) {        synchronized (mDiskCacheLock) {            File cacheDir = params[0];            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);            mDiskCacheStarting = false; // Finished initialization            mDiskCacheLock.notifyAll(); // Wake any waiting threads        }        return null;    }}class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {    ...    // 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(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    synchronized (mDiskCacheLock) {        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {            mDiskLruCache.put(key, bitmap);        }    }}public Bitmap getBitmapFromDiskCache(String key) {    synchronized (mDiskCacheLock) {        // Wait while disk cache is started from background thread        while (mDiskCacheStarting) {            try {                mDiskCacheLock.wait();            } catch (InterruptedException e) {}        }        if (mDiskLruCache != null) {            return mDiskLruCache.get(key);        }    }    return null;}// 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 getDiskCacheDir(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.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :                            context.getCacheDir().getPath();    return new File(cachePath + File.separator + uniqueName);}

当图片加载完毕,图片应该同时缓存到memory和磁盘中,以便以后加载。

处理配置变化

当配置发生变化,例如横竖屏切换的时候,安卓会销毁使用新的配置重建activity。为了避免图片重新处理,我们需要对代码做一些修改。

这是一个Fragment中保留LruCache不因为配置变化而重新加载的例子。

private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected void onCreate(Bundle savedInstanceState) {    ...    RetainFragment retainFragment =            RetainFragment.findOrCreateRetainFragment(getFragmentManager());    mMemoryCache = retainFragment.mRetainedCache;    if (mMemoryCache == null) {        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {            ... // Initialize cache here as usual        }        retainFragment.mRetainedCache = mMemoryCache;    }    ...}class RetainFragment extends Fragment {    private static final String TAG = "RetainFragment";    public LruCache<String, Bitmap> mRetainedCache;    public RetainFragment() {}    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);        if (fragment == null) {            fragment = new RetainFragment();            fm.beginTransaction().add(fragment, TAG).commit();        }        return fragment;    }    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setRetainInstance(true);    }}

使用第三方图片加载库

处理图片是每个开发者都无法避免的大问题,幸运的是已经有很多非常成熟的第三方图片加载库。笔者用过市面上主流的四种加载库。ImageLoader,Picasso,Glide,Fresco,甚至Volley的图片加载也尝试过。每个加载库各有千秋,可以根据需求做选择,大大提升开发速度,减少oom异常的概率。笔者更推荐来自facebook的Fresco,在使用过程中,渐进式显示,三级内存在低端机上体验更好。送上github上的链接: https://github.com/facebook/fresco 。中文文档非常详尽,地址: http://fresco-cn.org/ 。开发者可以根据自己的需求,选择适合自己项目的库。

1 0
原创粉丝点击