开发杂谈:说说Android应用开发中的缓存那些事儿

本文出自门心叼龙的博客,属于原创类容,转载请注明出处。

做过WEB开发的朋友应该都知道,WEB程序员在开发项目的过程中几乎就不用考虑数据缓存的问题,因为浏览器已经把这些事情都帮我们处理好了,在之PC端压根儿就不用考虑流量的问题,宽带包年拉了网就随便使用不限量。而在Android移动应用开发的时候,从网络上获取大量的图片在列表ListView中展示,用户在屏幕上频繁的上下滑动就会出现卡顿,甚至会出现内存溢出而导致应用直接崩溃掉,更要命的是流量的消耗是用户所受不了的,因为在手机上用户所使用的每1MB流量都要收费的,移动网络由于由于成本问题不可能建立大量的基站向用户无限量使用。

这类问题的解决主要是要做好这两方面的工作:一是图片压缩,二是图片缓存。对于图片缓存在早期的Android开发中一般采用的手段就是用过软引用(SoftReference)或者弱引用(WeakReference)对加载的图片数据进行缓存,每次访问的时候先从软引用或者弱引用里面去取,如果没有再从网络中去拉取,这样增强了图片的重复使用,避免了每次访问图片都要从网络上获取的问题,从Android2.3开始垃圾回收的运行频率会变得更加的频繁,这就导致释放软引用,弱引用的频率也随之增高,这样引用的使用效率就会大大降低,直到Android3.1的时候Google官方推出了内存缓存类LruCache,以及后来在2014年由JakeWharton大神推出的磁盘缓存类DiskLruCache,图片缓存问题都变得迎刃而解。再到后来的出现很多非常优秀的图片加载框架Glide,Picasso等等,他们底层大部分都使用了LruCache,DiskLruCache作为缓存策略,我们也很有必要了解一下他们的底层实现原理,因此我们今天主要介绍LruCache,DiskLruCache基本使用方法和工作原理,最后会介绍一个通用的二级缓存图片加载工具类ImageLoader的使用。

LruCache的基本使用

LruCache的创建

LruCache是Android3.1所提供的一个缓存类,Lru是英文Least Recently Used的缩写,即最近最少使用算法,它的核心思想是,当缓存快满时,会淘汰掉最近最少使用的缓存数据。下面我们看它的使用方法,首先我们实例化一个LruCache,代码如下:

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache mMemoryCache= new LruCache(cacheSize) {
       protected int sizeOf(String key, Bitmap value) {
           return value.getByteCount();
       }
}

在上面的代码中首先使用cacheSize指定了LruCache的缓存大小并复写了sizeOf方法,sizeOf方法的作用是计算缓存对象的大小,当然了指定的cacheSize为4mb也只是为了举个例子,一般通常的做法是取当前进程可用内存的1/8即可,要注意了cacheSize和sizeOf方法返回值的单位要保持一致。这样一个LruCache就创建好了,接下来我们向LruCache添加一个对象:

LruCache缓存的添加
 mMemoryCache.put(key,bitmap);

添加完毕就可以从内存中获取了,代码实现如下:

LruCache缓存的获取
 mMemoryCache.get(key);

这就是要给LruCache的基本使用,是不很简单,下面我们看看他的内部工作原理。

LruCache的工作原理

LruCache的创建

LruCache它的内部是一个LinkedHashMap,提供了get和put方法来完成缓存对象的获取和添加操作,当缓存满时LruCache会移除较早添加的图片。下面我们看他的构造实现,代码如下:

public class LruCache {
    ...
    private final LinkedHashMap map;
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(0, 0.75f, true);
    }
    ...

很显然,LruCache是一个泛型类,定义了两个泛型参数,在构造方法的内部创建了一个LinkedHashMap,毫不含糊LruCache底层使用了LinkedHashMap来实现弹性缓存的。

LruCache缓存添加

接下我们看缓存对象的添加,主要代码如下:

 public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            //注释1
            size += safeSizeOf(key, value);
            //注释2
            previous = map.put(key, value);
            if (previous != null) {
                //注释3
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //注释4
        trimToSize(maxSize);
        return previous;
    }

在put方法的注释1处会计算当前的缓存的总容量,在注释2处调用LinkedHashMap的put方法加入缓存,这里要注意一下如果当前对象第一次添加则previous值为null,如果是再次添加了那么previous就有值了,那么就会在注释3处要减去上面已经添加过的size。最后如果缓存容量超过限制了,那么会在注释4处调用tirmToSize方法删除不常用的数据,该方法的实现如下所示:

private void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //注释1
                if (size <= maxSize) {
                    break;
                }

                // BEGIN LAYOUTLIB CHANGE
                // get the last item in the linked list.
                // This is not efficient, the goal here is to minimize the changes
                // compared to the platform version.
                Map.Entry toEvict = null;
                //注释2
                for (Map.Entry entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                //注释2
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

trimToSize方法的注释1处如果当前使用的容量小于等于总容量则就直接退出循环了,否则将在注释2处获取当前缓存队列中的最后一个元素,并且在注释2处将其直接remove掉。注意了这个方法是一个死循环,它会不断的循环去删除队列中的数据,直到size小于maxSize。

LruCache缓存获取
  public final V get(K key) {
        ···
        V mapValue;
        synchronized (this) {
            //注释1
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
        ···

在LruCache的get方法的注释1处会调用LinkedHashMap的get方法,该方法的具体实现如下:

 public V get(Object key) {
        Node e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //注释1
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

LinkedHashMap的get方法也很简单,直接会把查询到的节点赋值给了e,这个方法我们要注意的是注释1处的这个判断,如果accessOrder为true,则要把该节点放到队列的最后面,让它成为最新的数据项。而这个accessOrder是在LurCache的构造方法中实例化LinkedHashMap的时候已经给他赋值为true了。所以注释1处的afterNodeAccess方法是必然要执行的,这个方法的主要作用就是把最新使用的数据调整到队列的尾部。
好了,这就是LurCache的主要工作原理,还是比较简单的,接下来我们看DiskLruCache的基本使用和工作原理。

DiskLruCache的基本用法

DiskLruCache是在2014年由JakeWharton大神推出的磁盘缓存类,它将对象写入文件系统从而实现缓存的效果,它得到了Android的官方推荐,它的源码地址如下:https://github.com/JakeWharton/DiskLruCache

DiskLruCache的创建

DiskLruCache并不能通过构造方法来创建,只能通过的静态方法open方法来创建,具体实现如下所示:

DiskLruCache  mDiskLruCache = DiskLruCache.open(Environment.getDownloadCacheDirectory(), 100, 1, 50 * 1024 * 1024);

open方法共有四个参数,参数directory表示缓存文件的存储目录,第二个参数appVersion表示应用的版本号,一般为1即可,当版本号发生变化时DiskLruCache所缓存的数据将被清空,第三个参数valueCount表示单个节点上对应的数据个数,一般为1即可,第四个参数表示缓存的总容量的大小。

DiskLruCache缓存的添加

DiskLruCache的缓存添加的操作是由它的内部类Editor对象完成的,Editor表示一个缓存对象的编辑对象,这个是不是和我们的SharedPreferences有点像,SharedPreferences也是通过它的内部编辑对象Editor完成对数据的增删改查的。

private void putCache(String key) {
        File forder = Environment.getDownloadCacheDirectory();
        try {
            mDiskLruCache = DiskLruCache.open(forder, 100, 1, 50 * 1024 * 1024);
            //注释1
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                 //注释2
                OutputStream outputStream = editor.newOutputStream(0);
                 //注释3
                if (downloadBitMapByUrl(key, outputStream)) {
                    //注释4
                    editor.commit();
                } else {
                    editor.abort();
                }
                mDiskLruCache.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在注释1处会调用DiskLruCache对象的edit方法获取一个缓存编辑对象editor,注意了参数key,它里面不能有特殊字符,一般都是图片url地址通过md5加密生成的。在DiskLruCache类的说明文档里对key的定义有详细的解释:each key must match the regex [a-z0-9_-]{1,120}。在注释2处调用的editor的newOutputStream方法得到了一个输出流,然后在注释3处就是一个文件的下载操作,文件下载完毕在注释4处才是真正的图片写入文件系统的操作,如果在下载的过程中出现了异常,则可以调用editor的abort方法进行回退操作。

DiskLruCache缓存的获取

缓存的获取首先也需要把key进行转换,然后通过DiskLruCache的get方法得到一个Snapshot对象,然后通过Snapshot对象可以得到缓存对象的文件输入流,有了文件输入流就可以得到bitmap对象了。

public void getCache(String key){
        Bitmap bitmap = null;
        DiskLruCache.Snapshot snapShot = null;
        try {
            snapShot = mDiskLruCache.get(key);
            if (snapShot != null) {
                FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(0);
                //注释1
                FileDescriptor fileDescriptor = fileInputStream.getFD();
                bitmap = new ImageResizer().decodeSampledBitmapFromFileDescriptor(fileDescriptor,100,100);
               
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

在getCache方法的注释1需要注意一下,不能直接使用FileInputStream,可以获取FileInputStream的对应的文件描述符FileDescriptor,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片。

DiskLruCache的工作原理

接下来我们看DiskLruCache的工作原理

DiskLruCache的创建
 private final LinkedHashMap lruEntries =
            new LinkedHashMap(0, 0.75f, true);
 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // If a bkp file exists, use it instead.
        //注释1
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        if (backupFile.exists()) {
            File journalFile = new File(directory, JOURNAL_FILE);
            // If journal file also exists just delete backup file.
            if (journalFile.exists()) {
                backupFile.delete();
            } else {
                renameTo(backupFile, journalFile, false);
            }
        }

        // Prefer to pick up where we left off.
        //注释2
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                return cache;
            } catch (IOException journalIsCorrupt) {
                System.out
                        .println("DiskLruCache "
                                + directory
                                + " is corrupt: "
                                + journalIsCorrupt.getMessage()
                                + ", removing");
                cache.delete();
            }
        }

        // Create a new empty cache.
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }

DiskLruCache的open方法的第一行创建了一个LinkedHashMap,LinkedHashMap是文件系统的底实现,它和LruCache一样,底层的数据结构都是一个LinkedHashMap,在注释1处创建了一个日志文件,该文件是整个文件缓存系统的核心文件,相当是整个缓存系统的数据库信息,该文件主要保存缓存文件的状态信息,如缓存文件的名称,文件大小等信息,在注释2处创建了DiskLruCache对象。对象创建完毕,接着就是文件的的缓存了,首先会调用DiskLruCache的edit方法获取一个Editor对象,该方法实现如下:

DiskLruCache缓存的添加
public Editor edit(String key) throws IOException {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }

    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        //注释1
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // Snapshot is stale.
        }
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null; // Another edit is in progress.
        }
        //注释2
        Editor editor = new Editor(entry);
        entry.currentEditor = editor;

        // Flush the journal before creating files to prevent file leaks.
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }

首先会在注释1处获取缓存文件的具体信息Entry,然后在注释2处根据entry创建了一个Editor对象,然后就可以获取这个缓存对象所对应的输入流了,具体的代码实现如下所示:

public OutputStream newOutputStream(int index) throws IOException {
            if (index < 0 || index >= valueCount) {
                throw new IllegalArgumentException("Expected index " + index + " to "
                        + "be greater than 0 and less than the maximum value count "
                        + "of " + valueCount);
            }
            synchronized (DiskLruCache.this) {
                if (entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                if (!entry.readable) {
                    written[index] = true;
                }
                //注释1
                File dirtyFile = entry.getDirtyFile(index);
                FileOutputStream outputStream;
                try {
                    //注释2
                    outputStream = new FileOutputStream(dirtyFile);
                } catch (FileNotFoundException e) {
                    // Attempt to recreate the cache directory.
                    directory.mkdirs();
                    try {
                        outputStream = new FileOutputStream(dirtyFile);
                    } catch (FileNotFoundException e2) {
                        // We are unable to recover. Silently eat the writes.
                        return NULL_OUTPUT_STREAM;
                    }
                }
                return new FaultHidingOutputStream(outputStream);
            }
        }

以上就是Editor的newOutputStream方法实现,它的参数index一般传0即可,在注释1处创建了一个File,在注释2处根据这个file构建了一个文件输入流FileOutputStream,有了这个输入流,就可以像文件写数据了,最后文件下载完毕,还要进行向整个文件系统的入库操作,即就是要调用Editor对象的commit方法,该方法的具体实现如下所示:

 public void commit() throws IOException {
            if (hasErrors) {
                completeEdit(this, false);
                remove(entry.key); // The previous entry is stale.
            } else {
                //注释1
                completeEdit(this, true);
            }
            committed = true;
        }

在commit方法中,如果没有错误则会直接执行注释1处的completeEdit方法,该方法的具体实现如下:

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        // If this edit is creating the entry for the first time, every index must have a value.
        if (success && !entry.readable) {
            for (int i = 0; i < valueCount; i++) {
                if (!editor.written[i]) {
                    editor.abort();
                    throw new IllegalStateException("Newly created entry didn't create value for index " + i);
                }
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    return;
                }
            }
        }

        for (int i = 0; i < valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                if (dirty.exists()) {
                    File clean = entry.getCleanFile(i);
                    dirty.renameTo(clean);
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    size = size - oldLength + newLength;
                }
            } else {
                deleteIfExists(dirty);
            }
        }

        redundantOpCount++;
        entry.currentEditor = null;
        //注释1
        if (entry.readable | success) {
            entry.readable = true;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        journalWriter.flush();

        if (size > maxSize || journalRebuildRequired()) {
            //注释2
            executorService.submit(cleanupCallable);
        }
    }

在注释1处缓存文件的相关信息会存入journal文件当中,如果说超过了缓存的大小,则直接会执行注释2进行缓存文件的删除操作,以上就是整个缓存的添加过程,接下来我们看缓存的获取。

DiskLruCache缓存的获取

获取缓存的操作直接调用DiskLruCache对象的get方法即可,该方法的具体实现如下所示:

public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }

        // Open all streams eagerly to guarantee that we see a single published
        // snapshot. If we opened streams lazily then the streams could come
        // from different edits.
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } catch (FileNotFoundException e) {
            // A file must have been deleted manually!
            for (int i = 0; i < valueCount; i++) {
                if (ins[i] != null) {
                    Util.closeQuietly(ins[i]);
                } else {
                    break;
                }
            }
            return null;
        }

        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
    }

DiskLruCache对象的get方法会返回一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的 文件输入流,有了文件输出流,自然就可以得到Bitmap对象了。

ImageLoader的工作原理

截止目前LruCache和DiskLruCache就彻底讲完了,下面看一个通用的图片加载工具类ImageLoader的实现。一般来说,一个优秀的ImageLoader应该具备如下功能:图片的同步加载;图片的异步加载;图片压缩;内存缓存;磁盘缓存;网络拉取。图片的同步加载是指能够以同步的方式向调用者提供所加载的图片,这个图片可能是从内存缓存中读取的,也可能是从磁盘缓存中读取的,还可能是从网络拉取的。图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式来获取图 片,这个时候ImageLoader内部需要自己在线程中加载图片并将图片设置给所需的 ImageView。图片压缩的作用更毋庸置疑了,这是降低OOM概率的有效手段, ImageLoader必须合适地处理图片的压缩问题。内存缓存和磁盘缓存是ImageLoader的核心,也是ImageLoader的意义之所在,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户所造成的流量消耗,只有当这 两级缓存都不可用时才需要从网络中拉取图片。
我们先一睹为快,这是ImageLoader的整个代码实现:

public class ImageLoader {

    private static final String TAG = "ImageLoader";

    public static final int MESSAGE_POST_RESULT = 1;

    private static final int CPU_COUNT = Runtime.getRuntime()
            .availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    private static final int TAG_KEY_URI = R.id.imageloader_uri;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreated = false;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue(), sThreadFactory);
    
    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        };
    };

    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache mMemoryCache;
    private DiskLruCache mDiskLruCache;

    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * build a new instance of ImageLoader
     * @param context
     * @return a new instance of ImageLoader
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * load bitmap from memory cache or disk cache or network async, then bind imageView and bitmap.
     * NOTE THAT: should run in UI Thread
     * @param uri http url
     * @param imageView bitmap's bind object
     */
    public void bindBitmap(final String uri, final ImageView imageView) {
        bindBitmap(uri, imageView, 0, 0);
    }

    public void bindBitmap(final String uri, final ImageView imageView,
            final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {

            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    /**
     * load bitmap from memory cache or disk cache or network.
     * @param uri http url
     * @param reqWidth the width ImageView desired
     * @param reqHeight the height ImageView desired
     * @return bitmap, maybe null.
     */
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
            return bitmap;
        }

        try {
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null) {
                Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
            Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (bitmap == null && !mIsDiskLruCacheCreated) {
            Log.w(TAG, "encounter error, DiskLruCache is not created.");
            bitmap = downloadBitmapFromUrl(uri);
        }

        return bitmap;
    }

    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
            int reqHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
                    reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }

        return bitmap;
    }

    public boolean downloadUrlToStream(String urlString,
            OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.e(TAG, "downloadBitmap failed." + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(out);
            MyUtils.close(in);
        }
        return false;
    }

    private Bitmap downloadBitmapFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
        }
        return bitmap;
    }

    private String hashKeyFormUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    public File getDiskCacheDir(Context context, String uniqueName) {
        boolean externalStorageAvailable = Environment
                .getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }

        return new File(cachePath + File.separator + uniqueName);
    }

    @TargetApi(VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    private static class LoaderResult {
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}

在ImageLoader的构造方法中会选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作。在 ImageLoader初始化时,会创建LruCache和DiskLruCache,在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需的大小,一般是指用户的手机空间已经不足了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。在上面的代码实现中,ImageLoader的内存缓存的容量为当前进程可用内存的1/8,磁盘缓存的容量为50MB。

从loadBitmap的实现可以看出,其工作过程遵循如下几步:首先尝试从内存缓存中读 取图片,接着尝试从磁盘缓存中读取图片,最后才从网络中拉取图片。另外,这个方法不 能在主线程中调用,否则就抛出异常。这个执行环境的检查是在loadBitmapFromHttp中实 现的,通过检查当前线程的Looper是否为主线程的Looper来判断当前线程是否是主线程, 如果不是主线程就直接抛出异常中止程序。以上就ImageLoader的工作原理。

总结

今天我们主要讲了Android系统应用层常用的缓存算法:LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算 法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个 具有很高实用价值的ImageLoader。通过这篇文章的学习想必大家对整个Android操作系统中的文件缓存已经有了一个比较深刻的认识了。

问题反馈

在使用学习中有任何问题,请留言,或加入Android、Java开发技术交流群

你可能感兴趣的:(开发杂谈:说说Android应用开发中的缓存那些事儿)