现在的位置: 首页 > 移动开发 > 正文

android 优化那些事之图片缓存设计/如何高效的展示图片(三)

2019年09月13日 移动开发 ⁄ 共 15626字 ⁄ 字号 评论关闭

这篇文章其实也可以起另外一个标题: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;

@Override
protected 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; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected 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;

@Override
protected 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

抱歉!评论已关闭.