这篇文章其实也可以起另外一个标题:android 如何高效的展示图片

原文地址:http://developer.android.com/training/displaying-bitmaps/index.html

学习和使用常见的技术去处理和加载图片,能让你的用户界面快速响应,并且能避免动不动就超出了受限内存。可能你一不小心,就会导致内存消耗完毕从而crash

,抛出java.lang.OutofMemoryError: bitmap size exceeds VM budget,在应用程序中加载图片是个棘手的问题,主要包括以下几个原因:

1.移动设备通常有约束的系统资源android 设备中可能在一个应用里只有16M的可用内存。Android兼容定义文档(CDD),3.7节,指出了在

不同屏幕大小和密度所需的最小应用程序内存。应用程序应该优化过并且在这个最小的内存限制之下执行。当然,请记住许多设备配置更高的限制。 2.图片需要消耗大量的内存,尤其是丰富的图像比如照片。例如,摄像头拍照的Galaxy Nexus 2592 x1936像素(像素)。如果位图配置使用ARGB 8888(默认从Android 2.3起)然后加载这个图像到内存需要大约19 mb的内存(2592 * 1936 * 4字节),在一些设备会立即消耗完每个应用拥有的内存。


3.android App 的UI 可能需要频繁的将多个图片一次性加载 ,比如 ListView,GridView,ViewPager 一般是在屏幕上包含多张图片,同时在屏幕之外也可能即将需要展示(也许就在你滑动的一瞬间就显示了)。


内容分为五个部分,分别是:

1,高效的加载大图片:在不会超过单个应用限制内存这一条例下进行decode bitmap.

2, 异步加载图片:通过AsyncTask 异步处理图片,同时也提一下如何处理并发的情况。

3. 缓存图片:通过内存和磁盘缓存,来提高UI的响应速度和流程性。

4. 内存管理:如何管理位图内存来最大化你的应用程序的性能。

4. 在你的UI上展示图片:用例子说明如何在ViewPager和GridView中 使用后台线程来和图片缓存来加载图片。


一、高效的加载大图片
在decode一张Bitmap之前,先检查这张图片的尺寸(除非你明确的知道这张图片的尺寸,并且确定一定不会产生内存溢出),可以设置BitmapFactory.Options对象options的属性inJustDecodeBounds为true,因为他不会去开辟内存生成一张图片,却能够知道图片的宽度和高度

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;

在知道宽度和高度后,可根据需要生成采样率(说明一点:如果生成的采样率是2的幂,如2,4,8,16...那么解码一张图片将会更快更高效)
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) {        // Calculate ratios of height and width to requested height and width        final int heightRatio = Math.round((float) height / (float) reqHeight);        final int widthRatio = Math.round((float) width / (float) reqWidth);        // Choose the smallest ratio as inSampleSize value, this will guarantee        // a final image with both dimensions larger than or equal to the        // requested height and width.        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;    }    return inSampleSize;}


在知道采样率后,就可以生成图片了(这时要设置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);}

在获取图片后,就可以设置UI组件的图片相关属性(这里是举例,通常在UI中需要异步回调进行设置,而不是直接在UI线程中设置,如果需要在SD卡中读取,或者网络读取的话,会因为耗时导致阻塞UI线程,从而产出ANR):
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));



二、异步加载图片


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

注意:这里的ImageView 使用弱引用的目的 是为了确保AsyncTask不会阻止ImageView或者它引用的资源被系统回收

所以要异步加载一张图片很简单了,调用如下即可:
public void loadBitmap(int resId, ImageView imageView) {    BitmapWorkerTask task = new BitmapWorkerTask(imageView);    task.execute(resId);}

上面主要是针对普通的View ,但是ListView,GridView,ViewPage等这些滚动时有重用自己的child view又该怎么办呢?因为如果每一个child View 都触发一个AsyncTask的话,就无法
保证一种情况:AsyncTask已经完成,但与之相关连的child View却没有被回收,转而被重用了,你这样设置的话,显示的图片不是会错了么。。。就是顺序无法保证一致。

这种情况下的解决方案是:ImageView存储一个最近的AsyncTask的引用,并且在完成的时候再次判断一下,不就可以了吗!


仿照AsyncTask存储一个ImageView软应用的方法,我们可以自定义一个Drawable,也存储一个AsyncTask的软应用,使用了相同的思想


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

在执行AsyncTask之前,我们new 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);    }}

绑定之前,先清掉以前的Task即可:

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() 需要再次检查当前的task是不是ImageView相匹配的Task

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的getView()方法中执行loadBitmap方法即可了。


三、缓存图片


a.内存缓存。就是使用我们常说的LRU进行缓存了。

这里注意一点:在过去,很流行使用软应用和弱应用来缓存图片,但是现在已经不推荐使用了,因为从Android 2.3(API级别9)以后,垃圾收集器是更积极收集软/弱引用,这使得它们相当无效,

Android 3.0(API级别11) 之前,支持数据的位图存储在本地内存(而不是虚拟机内存),并且不是显示方式释放,可能导致应用程序超过其内存限制和崩溃。


如何为LruCahce选定一个合适的大小,需要考虑一系列的因素,如:

1.内存是如何加强你的activity/application.

2.有多少图片需要马上显示,有多少图片准备显示。

3.设备的尺寸和密度是什么,高密度设备在缓存相同数量的图片需要的更大的缓存

4.图片的尺寸和配置是什么,需要消耗多少的内存。

5.你是否访问频繁?如果频繁访问,你可能需要将某些图片内存常驻,或者使用多个LruCache(不同场合不同大小不同定义)

6.需要平衡数量与质量。

所以没有一个特定的大小或者适合所有的解决方案,得自己去分析APP,综合想出一个解决方案,缓存太小,导致额外的开销,缓存太大,容易再次使内存溢出或者留下很少的内存供给其它的用途。


这里举个例子:

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


所以之前的loadBitmap方法,使用LRUCache,就可以先check一下:

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也需要更新LRUCache:

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


b.DISK卡缓存

注意:如果访问特别的频繁,ContentProvider可能是一个合适的地方存储缓存的图片。

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

内存缓存检查在UI线程中,Disk卡缓存检查在非UI线程中,但是图片完成后,内存缓存和Disk缓存都需要添加。


举个例子,当Configuration 改变时候,如横竖屏切换。

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();        }        return fragment;    }    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setRetainInstance(true);    }}

四、内存管理


主要讲的是促进垃圾回收期回收,和使图片重用。
先讲下关于版本的一些变化知识:

1.在Android上Android 2.2(API级别8)和低版本时,当垃圾收集发生时,您的应用程序的线程被暂停,这导致滞后,降低性能。Android 2.3增加了并发垃圾收集,这意味着内存位图不再被引用不久后就会被回收。

2.在安卓2.3.3(API级别10)和低版本,位图像素数据存储在Native内存。它是独立于位图本身(存储在Dalvik堆)。像素数据在Native内存不是显式的方式释放,可能导致应用程序超过其内存限制和崩溃。在Android 3.0(API级别11),像素数据连同相关的位图存储在Dalvik堆。


2.3.3以及以前的优化:

通过引用计数的方式来判断图片是否可以回收了。

private int mCacheRefCount = 0;private int mDisplayRefCount = 0;...// Notify the drawable that the displayed state has changed.// Keep a count to determine when the drawable is no longer displayed.public void setIsDisplayed(boolean isDisplayed) {    synchronized (this) {        if (isDisplayed) {            mDisplayRefCount++;            mHasBeenDisplayed = true;        } else {            mDisplayRefCount--;        }    }    // Check to see if recycle() can be called.    checkState();}// Notify the drawable that the cache state has changed.// Keep a count to determine when the drawable is no longer being cached.public void setIsCached(boolean isCached) {    synchronized (this) {        if (isCached) {            mCacheRefCount++;        } else {            mCacheRefCount--;        }    }    // Check to see if recycle() can be called.    checkState();}private synchronized void checkState() {    // If the drawable cache and display ref counts = 0, and this drawable    // has been displayed, then recycle.    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed            && hasValidBitmap()) {        getBitmap().recycle();    }}private synchronized boolean hasValidBitmap() {    Bitmap bitmap = getBitmap();    return bitmap != null && !bitmap.isRecycled();}

3.0以及以后版本的优化:

3.0以后引进了BitmapFactory.Options.inBitmap 这个属性,如果option设置了这个属性的话,当load一张图片的时候,它将尝试去复用一张已经存在的图片:

就是复用之前那种图片的内存,而不用频繁的去开辟/回收内存,从而提高了效率。

当然是有条件的:复用图片的大小必须和新生成的图片大小一致(确保所占用的内存一致)

HashSet<SoftReference<Bitmap>> mReusableBitmaps;private LruCache<String, BitmapDrawable> mMemoryCache;// If you're running on Honeycomb or newer, create// a HashSet of references to reusable bitmaps.if (Utils.hasHoneycomb()) {    mReusableBitmaps = new HashSet<SoftReference<Bitmap>>();}mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {    // Notify the removed entry that is no longer being cached.    @Override    protected void entryRemoved(boolean evicted, String key,            BitmapDrawable oldValue, BitmapDrawable newValue) {        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {            // The removed entry is a recycling drawable, so notify it            // that it has been removed from the memory cache.            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);        } else {            // The removed entry is a standard BitmapDrawable.            if (Utils.hasHoneycomb()) {                // We're running on Honeycomb or later, so add the bitmap                // to a SoftReference set for possible use with inBitmap later.                mReusableBitmaps.add                        (new SoftReference<Bitmap>(oldValue.getBitmap()));            }        }    }....}

public static Bitmap decodeSampledBitmapFromFile(String filename,        int reqWidth, int reqHeight, ImageCache cache) {    final BitmapFactory.Options options = new BitmapFactory.Options();    ...    BitmapFactory.decodeFile(filename, options);    ...    // If we're running on Honeycomb or newer, try to use inBitmap.    if (Utils.hasHoneycomb()) {        addInBitmapOptions(options, cache);    }    ...    return BitmapFactory.decodeFile(filename, options);}

private static void addInBitmapOptions(BitmapFactory.Options options,        ImageCache cache) {    // inBitmap only works with mutable bitmaps, so force the decoder to    // return mutable bitmaps.    options.inMutable = true;    if (cache != null) {        // Try to find a bitmap to use for inBitmap.        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);        if (inBitmap != null) {            // If a suitable bitmap has been found, set it as the value of            // inBitmap.            options.inBitmap = inBitmap;        }    }}// This method iterates through the reusable bitmaps, looking for one // to use for inBitmap:protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {        Bitmap bitmap = null;    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {        final Iterator<SoftReference<Bitmap>> iterator                = mReusableBitmaps.iterator();        Bitmap item;        while (iterator.hasNext()) {            item = iterator.next().get();            if (null != item && item.isMutable()) {                // Check to see it the item can be used for inBitmap.                if (canUseForInBitmap(item, options)) {                    bitmap = item;                    // Remove from reusable set so it can't be used again.                    iterator.remove();                    break;                }            } else {                // Remove from the set if the reference has been cleared.                iterator.remove();            }        }    }    return bitmap;}

private static boolean canUseForInBitmap(        Bitmap candidate, BitmapFactory.Options targetOptions) {    int width = targetOptions.outWidth / targetOptions.inSampleSize;    int height = targetOptions.outHeight / targetOptions.inSampleSize;    // Returns true if "candidate" can be used for inBitmap re-use with    // "targetOptions".    return candidate.getWidth() == width && candidate.getHeight() == height;}



邮箱:zz7zz7zz@163.com

微博:http://weibo.com/u/3209971935






更多相关文章

  1. 怎样在android中添加背景图片?
  2. Android--Adapter深入理解及ListView优化
  3. Android(安卓)Bitmap内存限制问题
  4. android开发图片小技巧
  5. Android_ViewPager实现滚动广告
  6. Android(安卓)获取并显示远程图片 Picasso框架的使用(一)
  7. Android(安卓)DiskLruCache完全解析,硬盘缓存的最佳方案
  8. Android(安卓)内存泄漏调试(转载)
  9. Android(安卓)内存溢出解决方案(OOM) 整理总结

随机推荐

  1. Android版本和API Level对应关系
  2. android camera照片旋转90度
  3. android 键盘 搜索键
  4. android中自定义进度条风格
  5. Android的所有权限说明
  6. AgentWeb WebView 与 Android交互 JS调用
  7. Android单元测试和日志输出
  8. android设置view透明度的效果
  9. No 122 · android获取本机号码和本机唯
  10. android之实现ProgressBar进度条组件