Bitmaps加载之缓存

作者: keith666 | 来源:发表于2016-06-05 23:49 被阅读115次

前文介绍了Bitmaps的异步加载,将单个Bitmap加载到视图控件是很简单直接的,但是要同时批量加载的话就会变得复杂了.对于ListView或GridView或ViewPager来说,由于他们是可以滑动的,所以有多少图片将要被加载出来就变得不确定.

由于ListView等这种View带有回收机制,因此内存的使用会保持在较低的水平,但同时由于这个回收机制,之前已经加载的Bitmap会被GC释放掉,这种方式有个不好的地方是:你滑到一个位置a,然后滑到一个位置b,然后再滑到a,本来a位置的Bitmap已经加载完了,但是当你滑到b时a位置的Bitmap就被GC回收了,这时a又要重新加载Bitmap.可以想象像ListView这种需要频繁滑动并且带有很多childView的View如果每次都要重新加载的话是多么的耗时耗流量.
因此,需要引入内存和磁盘缓存的帮助,才能让ListView这类型的View快速的加载图片以支持快速滑动.

使用内存缓存

内存缓存是以占用app应用内存为代价来给Bitmap提供快速访问.LruCache这个非常适用于做内存缓存,比如缓存bitmap等,它的内部是用一个LinkedHashMap来保存缓存对象的强引用,并使用LRU(最近最少使用)算法来控制队列的移除以保证不超过设定的内存大小.

注意: 过去LinkedHashMap保存的是引用对象的弱引用或软引用,但是这现在不推荐,因为Android 2.3(API 9)以后GC变得更加的aggressive,弱引用或软引用会被回收,导致引用无效.除此之外,在3.0之前有个问题,就是Bitmap的backing date(也是就是真正的字节数据,Bitmap只是保存一些信息而已)占据的内存不会被正常释放,会潜在的导致应用因为内存溢出而crash.

LruCache使用的时候需要设置一个大小,如何设置一个合理的大小呢?可以参考以下几个因素:

  • 除了你的Activity和application之外,其他的地方使用内存情况如何?
  • 一次会有多少张图片显示在屏幕?有多少张图片需要被加载出来等待被显示?
  • 设备的屏幕大小和密度是多少?高密度屏幕的设备需要更大的cache空间.
  • Bitmaps的尺寸和配置参数是什么?它们每个要占多少内存?
  • 这些Images放被访问的频率如何?有一些访问比其他频繁的吗?可以根据这个来决定是否将这些对象设置成常驻内存或是使用多个LruCache.
  • 你能平衡好数量和质量吗?有时只存储一大推的小图,然后把大图放在使用的时候再加载,这种方式在某些情况很有用.

上述提到了这么多的因素,但是实际上并没有什么具体的最合理的数值或公式,你需要自己去分析然后确定对于你的app最适合的缓存大小.太小的话不仅无用还带来而外的计算,太大又会导致app的可用内存的减少,容易OOM.

下面看个LruCache的设置例子:

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

注意:

  • 上面代码中给cache分配的大小为app可用总内存的1/8,假设app总内存为32M,那么cache的大小为4M,如屏幕分辨率为800x480,满屏的话需要图片大小为1.5M左右(8004804 bytes),对于GridView来说,这个cache可以缓存2.5个页面左右(4/1.5).
  • 注意计算时的单位要统一,即maxMemory的单位和sizeOf()返回的单位要统一,上述的代码用的是Kbytes.

有了cache之后加载图片的逻辑变成这样:
当收到图片加载请求,先去cache中查找是否有缓存,有则直接返回,没有则返回空接着去请求图片.

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    // 1. 去cache中查找
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    
    if (bitmap != null) {
        // 2. cache中有缓存
        mImageView.setImageBitmap(bitmap);
    } else {
        // 3. cache中内缓存
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

之前自定义的Task也要更新一下,需要把加载完毕的Bitmap放到cache中

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));
        // 将加载完毕的bitmap放到cache中
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

使用磁盘缓存

内存缓存对于最近使用的Bitmap的快速访问非常有用,但是这个还是不够可靠.

  • 像GridView这种带大量childView的控件很容易就占满了内存缓存.
  • 当你的应用被其他任务打断时,比如来了个电话,你的app就会跑到后台运行,这样你的app的进程可能会被系统给destroy掉,这样内存缓存也就没了,而当用户返回时app又要重新加载这些资源.

这个时候磁盘缓存就可以帮忙了,磁盘缓存可以缓存Bitmaps,这样就可以减少非内存缓存图片的加载速度,当然磁盘的访问速度要比内存的来的慢并且加载的时间也不可预知,因此应该使用异步加载.

注意: 如果要缓存经常访问的图片,可以使用ContentProvider,比如相册类.

下面的代码使用了一个DiskLruCache类的引用,来自Android source,具体如下:

// DiskLruCache的引用,算法一样为LRU
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
// 磁盘缓存初始化标志值,true表示还没初始化
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];
            // 获取一个DiskLruCache对象
            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) {
            // 注意这个方法,与源码的存入方式不一样,这里把这个方法当作是将bitmap存入磁盘缓存的操作即可
            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);
}

注意:

  • 初始化磁盘缓存要进行磁盘操作,因此需要异步.
  • 由于磁盘缓存初始化的异步,从而导致了一种可能的的出现: 用户调用getBitmapFromDiskCache()来访问磁盘缓存,然后磁盘缓存还没创建完毕,因此使用同步锁来防止这种情况的出现.
  • 检查内存缓存是否有请求资源是在UI线程中操作的,而检查磁盘缓存是在异步操作的.
  • 图片从网络加载完毕之后要放到内存缓存和磁盘缓存,以便后续使用.
  • 上述代码中有一句: mDiskLruCache.put(key, bitmap);注意这个方法,源码中没有这个方法,源码的存入方式不一样,这里把这个方法当作是将bitmap存入磁盘缓存的操作即可,具体可以参考官方Demo.

处理运行时配置的变更

Handling Runtime Changes,配置的变更如屏幕旋转等,会导致Activity被destroy掉然后restart,所以如果你想要在配置变更的时候还保存内存缓存的话,有一个方法可以帮助:
将内存缓存引用存到一个设置了setRetainInstance(true)(该方法会改变Fragment的生命周期)的Fragment里面,在Activity重新创建完毕之后重该fragment里面获取内存缓存的引用,具体看下面代码:

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();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

要测试的话,可以调用setRetainInstance()方法,分别传入true和false,然后旋转屏幕.

Reference:

  1. Caching Bitmaps
  2. 官方DisplayingBitmaps Demo
  3. What's difference between “backing pixel data” and bitmap object?](http://stackoverflow.com/questions/33297471/whats-difference-between-backing-pixel-data-and-bitmap-object)

相关文章

  • Bitmaps加载之缓存

    前文介绍了Bitmaps的异步加载,将单个Bitmap加载到视图控件是很简单直接的,但是要同时批量加载的话就会变得...

  • Bitmaps加载之异步加载

    介绍 前文介绍了Bitmap大图的高效加载,如果需要加载的图片来源于网络或本次磁盘或其他非内存地方,这个加载的任务...

  • Bitmaps加载之介绍

    介绍 Android开发应用过程中加载bitmap是很tricky的,如果你稍微不注意,bitmaps就会快速消耗...

  • Android 性能优化之Loading Big Bitmaps

    高效加载Large Bitmaps 加载大Bitmaps到内存中,总是会有各种各样的问题,我们在开发过程中,经常会...

  • Bitmaps加载之综合应用

    前面分别从Bitmap的介绍,高效加载大图,异步加载,缓存和内存管理这些方面来学习Bitmap的加载,本文将综合以...

  • JAVA类加载机制

    jvm之java类加载机制和类加载器(ClassLoader)的详解java类加载机制:全盘负责、双亲委派、缓存机...

  • bitmaps.md

    Dirty Bitmaps and Incremental Backup Dirty Bitmaps are ob...

  • 自定义图片框架实现图片三级缓存

    什么是三级缓存 网络缓存, 不优先加载, 速度慢,浪费流量本地缓存, 次优先加载, 速度快内存缓存, 优先加载, ...

  • Bitmaps加载之内存管理

    除了前面说的Bitmap缓存之外,还有一些事情我们可以做来使用好GC和Bitmap的重用. 对于不同的Androi...

  • Bitmaps加载之高效加载大图

    前文 很多时候我们要加载的图片的尺寸要比放置的控件的来的大,而该控件只要求图片的像素和控件本身的尺寸一致即可达到最...

网友评论

    本文标题:Bitmaps加载之缓存

    本文链接:https://www.haomeiwen.com/subject/hpmrdttx.html