Android Cache 解析

这里说的Cache 我们分为两种,LruCache 和DiskLruCache

翻译 android官方文档 Caching BitMaps

Loading a single bitmap into your user interface (UI) is straightforward, however things get more complicated if you need to load a larger set of images at once. In many cases (such as with components like ListView, GridView or ViewPager), the total number of images on-screen combined with images that might soon scroll onto the screen are essentially unlimited.

在UI界面上加载单个图片是很简单的,但是如果一次性加载大数量的图片就会很复杂,比如(ListView, GridView or ViewPager)中加载图片,在屏幕上显示的图片以及滚动后显示的图片的数量,往往是不受限制的

Memory usage is kept down with components like this by recycling the child views as they move off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don’t keep any long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI you want to avoid continually processing these images each time they come back on-screen. A memory and disk cache can often help here, allowing components to quickly reload processed images.

这些组件在它的子视图移除屏幕后进行视图回收,假如没有对bitmap保持长期有效的引用,GC会对该bitmap进行释放,这样做当然很好,但是事实上如果你要保持你的UI快速并且流畅,你必须避免在图片重新显示时对图片进行重新的处理,内存和磁盘缓存可以解决这个问题,缓存可以使组件快速的处理图片。

Use a Memory Cache
//使用内存缓存

A memory cache offers fast access to bitmaps at the cost of taking up valuable application memory. The LruCache class (also available in the Support Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size.

内存缓存提供了快速访问bitMap的方式,代价是占用了程序的宝贵的内存,LruCache类(在API4以后就可以使用了,在API12后用的是android.uti包,12以下用的是Support包)是非常好的缓存bitmap的方案,LruCache将最近使用对象保存在的强引用的LinkedHashMap中,当缓存超过一定值时,移除最后的对象。(这里的机制是,只要对象读取,就把对位放置到LinkedHashMap的第一位,当缓存不足时,移除最后的,也就是最不经常访问的对象)

Note: In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash.

在过去,使用软引用或者若引用缓存图片是一个很流行的内存缓存方案,但是现在已经不推荐这种方案了,在AndroidAndroid 2.3 (API Level 9) 后,GC更倾向于回收软/弱引用中的无效对象,另外,在 Android 3.0 (API Level 11)前,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

In order to choose a suitable size for a LruCache, a number of factors should be taken into consideration, for example:

为了选择合适的内存大小分配给LruCache,需要考虑一些因素,比如

How memory intensive is the rest of your activity and/or application?
How many images will be on-screen at once? How many need to be available ready to come on-screen?
What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy Nexus will need a larger cache to hold the same number of images in memory compared to a device like Nexus S (hdpi).
What dimensions and configuration are the bitmaps and therefore how much memory will each take up?
How frequently will the images be accessed? Will some be accessed more frequently than others? If so, perhaps you may want to keep certain items always in memory or even have multiple LruCache objects for different groups of bitmaps.
Can you balance quality against quantity? Sometimes it can be more useful to store a larger number of lower quality bitmaps, potentially loading a higher quality version in another background task.

There is no specific size or formula that suits all applications, it’s up to you to analyze your usage and come up with a suitable solution. A cache that is too small causes additional overhead with no benefit, a cache that is too large can once again cause java.lang.OutOfMemory exceptions and leave the rest of your app little memory to work with.
没有一个特定的匹配所有程序的公式去计算内存大小,只有通过具体的使用方法,来得到合适的解决方案,太小的缓存会增加更多附加的操作(图片重复的释放加载),这并没有什么好处,太大的缓存会造成OutOfMemory 的异常

//这里有个例子,使用 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);
}

Note: In this example, one eighth of the application memory is allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full screen GridView filled with images on a device with 800x480 resolution would use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in memory.
在这个例子中,八分之一的应用内存分配给了我们的缓存,在一个普通的或者hdpi设备上,这个值大约至少是4MB(32/8)。在一个800x480分辨率的设备上全屏显示一个填满图片的GridView大约要1.5MB(800*480*4 bytes),因此,这个LruCache至少能够缓存2.5页的图片。

When loading a bitmap into an ImageView, the LruCache is checked first. If an entry is found, it is used immediately to update the ImageView, otherwise a background thread is spawned to process the image:

我们把bitmap 加载到ImageView中,首先要检查LruCache,如果发现entry存在,立即使用并更新ImageView,否则要在子线程中去加载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);
    }
}

The BitmapWorkerTask also needs to be updated to add entries to the memory cache:
这个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;
    }

}

Use a Disk Cache
使用磁盘缓存

A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot rely on images being available in this cache. Components like GridView with larger datasets can easily fill up a memory cache. Your application could be interrupted by another task like a phone call, and while in the background it might be killed and the memory cache destroyed. Once the user resumes, your application has to process each image again.

内存缓存可以很好的提高对最近使用过得图片的访问速度(有点别扭,反正差不多就是提高加载速度),但是你不能太依赖内存中的图片,很多组件比如GridView有很多大的数据集,会很轻易的占满你的内存缓存,当有其他任务执行的时候(比如打电话)你的任务可能被打断,并且在后台的任务有可能被杀死,内存缓存被回收,当应用切换到前台时,你的应用得再一次的处理每张图片

A disk cache can be used in these cases to persist processed bitmaps and help decrease loading times where images are no longer available in a memory cache. Of course, fetching images from disk is slower than loading from memory and should be done in a background thread, as disk read times can be unpredictable.

在这些情况下,磁盘缓存可以用来存储bitmap,并在内存缓存被回收后加速图片的访问,当然,从硬盘加载图片比从内存中加载要满,所以从硬盘加载图片必须放在后台线程中执行,因为从磁盘读取的时间是不可预测的

Note: A ContentProvider might be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.

如果操作图片的次数很频繁,那么ContentProvider 会更加适合来缓存图片,比如相册

The sample code of this class uses a DiskLruCache implementation that is pulled from the Android source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:

下面的示例使用 DiskLruCache (JK大神作品)来实现,在这个示例代码中,除了已有的内存缓存,还添加了磁盘缓存

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

Note: Even initializing the disk cache requires disk operations and therefore should not take place on the main thread. However, this does mean there’s a chance the cache is accessed before initialization. To address this, in the above implementation, a lock object ensures that the app does not read from the disk cache until the cache has been initialized.

注意,即使是初始化磁盘缓存,也是会对磁盘进行操作的,因此不能再主线程中操作,然而,这并不意味着磁盘缓存在初始化前就能被访问,为了解决这个问题,在上面的实现中,用一个加锁的对象来保证不能访问磁盘,直到磁盘缓存被初始化后。

While the memory cache is checked in the UI thread, the disk cache is checked in the background thread. Disk operations should never take place on the UI thread. When image processing is complete, the final bitmap is added to both the memory and disk cache for future use.

在UI线程中检查内存缓存,在后台线程中去检查磁盘缓存,磁盘缓存永远不要在UI线程中,当图片处理完成后,最终的bitmap 需要存在内存和磁盘缓存中,以便后续使用。

Handle Configuration Changes
处理配置变化

Runtime configuration changes, such as a screen orientation change, cause Android to destroy and restart the running activity with the new configuration (For more information about this behavior, see Handling Runtime Changes). You want to avoid having to process all your images again so the user has a smooth and fast experience when a configuration change occurs.

运行时发生配置变化,比图屏幕方向的改变,造成activity的销毁并且以新的配置重新启动activity。为了让用户有着流畅和快速的体验,你需要避免配置改变时再次处理所有图片

Luckily, you have a nice memory cache of bitmaps that you built in the Use a Memory Cache section. This cache can be passed through to the new activity instance using a Fragment which is preserved by calling setRetainInstance(true)). After the activity has been recreated, this retained Fragment is reattached and you gain access to the existing cache object, allowing images to be quickly fetched and re-populated into the ImageView objects.

幸运的是,在Use a Memory Cache章节中你已经很好的处理了bitmap的内存缓存,这些内存缓存可以通过Fragment 传递到新的activity的实例,通过fragment调用setRetainInstance(true))将缓存保存,在activity被重新创建后,你可以在fragment 中访问已经保存下来得缓存,允许图片快速的取出并加载到ImageView上

Here’s an example of retaining a LruCache object across configuration changes using a Fragment:

下面有一个例子,在LruCache中保存对象,使用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);
    }
}

To test this out, try rotating a device both with and without retaining the Fragment. You should notice little to no lag as the images populate the activity almost instantly from memory when you retain the cache. Any images not found in the memory cache are hopefully available in the disk cache, if not, they are processed as usual.

为了验证这一点,尝试在Fragment 保存或者不保存缓存, 在你保存缓存时,图片很快的被加载了,几乎没有卡顿,任何图片在内存缓存中没有的话,就应该去磁盘缓存中寻找,如果都没有的话,就执行正常的图片加载方式

大致的总结下
  1. 在缓存图片时,最好使用LruCache 加DiskLruCache的组合 ,不要去用SoftReference或者 WeakReference
  2. 根据你的应用去计算内存缓存的大小
  3. 内存缓存可以在UI线程中执行,磁盘缓存则要放到子线程去执行
  4. LruCache 是线程安全的,但是多线程操作时,也要注意线程安全的处理
LinkedHashMap

在查看LruCache 和DiskLruCache的源码前,首先要理解下LinkedHashMap的原理,LruCache 和DiskLruCache底层都是用了LinkedHashMap来做存储,为什么使用LinkedHashMap,这里就要讲到LRU算法,LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。LinkedHashMap就是jdk的LRU实现,在上面的翻译文章也出现过大致的原理解释。

// LinkedHashMap继承自HashMap,从而实现了Map的接口
public class LinkedHashMap<K, V> extends HashMap<K, V> {

    /**
     * A dummy entry in the circular linked list of entries in the map.
     * The first real entry is header.nxt, and the last is header.prv.
     * If the map is empty, header.nxt == header && header.prv == header.
     */
    transient LinkedEntry<K, V> header;

    /**
     * True if access ordered, false if insertion ordered.
     */
    private final boolean accessOrder;

    /**
     * Constructs a new empty {@code LinkedHashMap} instance.
     */
    public LinkedHashMap() {
        init();
        accessOrder = false;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值