详细解读LruCache类

(转自:http://www.cnblogs.com/tianzhijiexian/p/4248677.html)

LruCache是android提供的一个缓存工具类,其算法是最近最少使用算法。它把最近使用的对象用“强引用”存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前就从内存中移除。其在API12被引进,低版本可以用support包中的类。

一、分析源码

这个源码是从网上找的,自己懒得去找源码了。

具体分析也是来自网络:http://www.open-open.com/lib/view/open1385474073171.html

  View Code

源码挺长的,内部注释也已经十分详尽了,所以有空可以看看。为了方便说明,下面说下我的分析:

  1. 其中用到的数据对象是LinkedHashMap,所以不要把这个类想的多么深不可测,还是数据结构 + 算法。既然用到了这个map,自然就要有添加修改和删除操作了,用到了最近最少使用算法,自然就要用到优先级了。
  2. 作为缓存,肯定有一个缓存的大小,这个大小是可以设定的(自定义sizeOf())。当你访问了一个item(需要缓存的对象),这个item应该被加入到内存中,然后移动到一个队列的顶部,如此循环后这个队列的顶部应该是最近访问的item了,而队尾部就是很久没有访问的item,这样我们就应该对队尾部的item优先进行回收操作。
  3. 因为用到了HashMap,那么就有这个数据存储对象的特点(KEY-VALUE),放入这个map的item应该会被强引用,要回收这个对象的时候是让这个key为空,这样就让有向图找不到对应的value,最终被GC。
  4. 缓存的最大特点是不做重复的劳动,如果你之前已经缓存过这个item了,当你再次想要缓存这个item时,应该会先判断是否已经缓存好了,如果已经缓存,那么就不执行添加的操作。
  5. 我们应该能通过某个方法来清空缓存,这个缓存在app被退出后就自动清理,不会常驻内存。
  6. sizeof()方法。这个方法默认返回的是你缓存的item数目,如果你想要自定义size的大小,直接重写这个方法,返回自定义的值即可。
复制代码
    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units. The default implementation returns 1 so that size is
     * the number of entries and max size is the maximum number of entries.
     * 返回用户定义的item的大小,默认返回1代表item的数量,最大size就是最大item值
     * <p>
     * An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }
复制代码
  • 7. 如果你cache的某个值需要明确释放,重写entryRemoved()方法。这个方法会在元素被put或remove时调用,源码默认是空实现的。
复制代码
    /**
     * Called for entries that have been evicted or removed. This method is
     * invoked when a value is evicted to make space, removed by a call to
     * {@link #remove}, or replaced by a call to {@link #put}. The default
     * implementation does nothing.
     * 当item被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用, 或者替换item值时put调用,默认实现什么都没做。
     * <p>
     * The method is called without synchronization: other threads may access
     * the cache while this method is executing.
     *
     * @param evicted
     *            true if the entry is being removed to make space, false if the
     *            removal was caused by a {@link #put} or {@link #remove}.
     *            true---为释放空间被删除;false---put或remove导致
     * @param newValue
     *            the new value for {@code key}, if it exists. If non-null, this
     *            removal was caused by a {@link #put}. Otherwise it was caused
     *            by an eviction or a {@link #remove}.
     */
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
    }
复制代码

通过判断这里面的evicted值就知道当前进行的是什么操作,你可以在这里进行你想要的操作。

 

二、初始化LruCache

2.1 定义cache大小

初始化这个cache前需要设定这个cache的大小,这里的大小官方推荐是用当前app可用内存的八分之一,当然你可以视情况而定。通过:

final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

就可以得到当前app可用的内存。

为了全局调用,我在application类中定义了计算cache大小的方法。

复制代码
package com.kale.lrucachetest;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;

public class KaleApplication extends Application{
    
    /**
     * @description 
     *
     * @param context
     * @return 得到需要分配的缓存大小,这里用八分之一的大小来做
     */
    public int getMemoryCacheSize() {
        // Get memory class of this device, exceeding this amount will throw an
        // OutOfMemory exception.
        final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

        // Use 1/8th of the available memory for this memory cache.
        return 1024 * 1024 * memClass / 8;
    }
}
复制代码

 

2.2 初始化类

  /* 内存缓存 */
    private LruCache<String, Bitmap> mMemoryCache;

设定缓存时要设定泛型,针对的是hashMap,你可以当作是key-value。我这里缓存的是bitmap,用到的key是string对象。

复制代码
final int memoryCache = ((KaleApplication) getApplication()).getMemoryCacheSize();
        Log.d(TAG, "cache size = " + memoryCache / 1024 / 1024 + "M");
        mMemoryCache = new LruCache<String, Bitmap>(memoryCache) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                return bitmap.getByteCount() / 1024;
            }
        }; // 初始化
复制代码

我通过缓存的值来初始化了cache对象,然后重写了sizeOf()方法。

 

三、添加/删除缓存

当我们初始化缓存后我们就应该能给这个缓存添加对象和移除对象。

复制代码
    /**
     * @description 将bitmap添加到内存中去
     *
     * @param key
     * @param bitmap
     */
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * @description 通过key来从内存缓存中获得bitmap对象
     *
     * @param key
     * @return
     */
    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }
复制代码

 

四、模拟从网络下载图片并加入缓存

4.1 判断是否已经缓存过

一般我们都是将图片显示到listview或者是gridView中,适配器读取view前应该判断在不在缓存中,如果在就直接显示,如果不在就从网络下载,其中可能还要用到viewholder类和滑动监听器来提高流畅性。

复制代码
/**
     * @description 将bitmap加载到imageview中去
     *
     * @param resId
     * @param imageView
     */
    private void loadBitmapToImageView(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);

        final Bitmap bitmap = getBitmapFromMemCache(imageKey); // 先看这个资源在不在内存中,如果在直接读取为bitmap,否则返回null
        if (bitmap != null) {
            Log.d(TAG, "in memory");
            imageView.setImageBitmap(bitmap);
        } else {
            Log.d(TAG, "not in memory");
            imageView.setImageResource(R.drawable.ic_launcher); // 如果没有在内存中,先显示默认的图片,然后启动线程去下载图片
            BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            task.execute(resId); // 启动线程,模拟从网络下载图片,下载后加入缓存
        }
    }
复制代码

这个方法是典型的加载模式,看缓存,如果没有就去启动asyncTask下载图片,由于内部类会保留外部类的强引用,所以asyncTask不应该作为内部类,而且一般用在10s内中的io操作,这里为了说明方便和尽可能符合官方实例,就还是用了asyncTask来说明。

4.2 异步任务中的操作

复制代码
package com.kale.lrucachetest;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

public class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap>{

    private MainActivity mActivity;
    private ImageView mImageView;
    
    public BitmapWorkerTask(ImageView imageView) {
        // TODO 自动生成的构造函数存根
        mImageView = imageView;
        mActivity = (MainActivity) imageView.getContext(); // 初始化activity
    }
    
    @Override
    protected Bitmap doInBackground(Integer... params) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Bitmap  bitmap = BitmapFactory.decodeResource(mActivity.getResources(), R.drawable.kale);
        mActivity.addBitmapToMemoryCache(String.valueOf(R.drawable.kale), bitmap);
        return bitmap;
    }
    
    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (result != null) {
            mImageView.setImageBitmap(result); // 将bitmap设置到imageView中去
        }
    }

}
复制代码

线程暂停1秒(模拟从网络下载),然后得到图片(网络访问正常的情况下),得到后将bitmap放入缓存中,最后在imageview中展示。

 

五、通过Fragment来保存缓存

屏幕方向改变会导致Android摧毁正在运行的Activity,然后使用新的配置从新启动该Activity (详情,参考这里 Handling Runtime Changes)。

需要注意避免在配置改变的时候导致重新处理所有的图片,从而提高用户体验。

幸运的是,您在 使用内存缓存 部分已经有一个很好的图片缓存了。该缓存可以通过Fragment (Fragment会通过setRetainInstance(true)函数保存起来)来传递给新的Activity。

当Activity重新启动 后,Fragment 被重新附加到Activity中,您可以通过该Fragment来获取缓存对象。下面是一个在 Fragment中保存缓存的示例:

RetainFragment

复制代码
package com.kale.bitmaptest;

import android.app.Fragment;
import android.app.FragmentManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.LruCache;

public class RetainFragment extends Fragment {

    private static final String TAG = "RetainFragment";
    public static LruCache<String, Bitmap> mRetainedCache;

    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);
    }
}
复制代码

Fragment中保存了一个lruCache对象,这个fragment有一个findOrCreateRetainFragment()方法。这个方法等同构造函数,它先判断fragmentManager中有没有这个fragment,如果有就调出,如果没有就new一个。在onCreat中调用了setRetainInstance(true)来保存缓存。

 

Activity

复制代码
package com.kale.bitmaptest;


import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.LruCache;

public class TestActivit extends Activity {
    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...

        RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager());
        mMemoryCache = RetainFragment.mRetainedCache;
        if (mMemoryCache == null) {
            mMemoryCache = new LruCache<String, Bitmap>(10);
            RetainFragment.mRetainedCache = mMemoryCache;
        }
        // ...
    }

}
复制代码

类似的代码在bitmapfun中就可以看到:

  View Code

在Activity中建立一个retainFragment对象,首先看看这个对象中有没有缓存,如果是第一次启动的话肯定是没有的,所以就初始化缓存,同时让fragment去引用这个缓存对象(在fragment中备份)。如果这个activity是由于屏幕方向改变而再次产生的,那么就可以从fragment中获得之前的缓存对象,无须重新初始化缓存了,这样可以保证之前的缓存不被丢弃。

 

六、总结

这样我们就完成了对lruCache的使用,现在我们发现这个缓存类也没那么复杂,用法也十分简单,正是因为简单,所以我们可以很方便的对其进行扩展。当然了,这仅仅是做了内存缓存,熟悉缓存机制的朋友一定会知道磁盘缓存和内存缓存二者的关系,有关磁盘缓存的问题我将在以后的文章中进行讲述。

 

源码下载:http://download.csdn.net/detail/shark0017/8395797

 

参考自:

http://stormzhang.com/android/2013/11/20/android-display-bitmaps-efficiently/

http://blog.csdn.net/androidzhaoxiaogang/article/details/7910364

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值