Android二级缓存之物理存储介质上的缓存DiskLruCache



Android二级缓存之物理存储介质上的缓存DiskLruCache

Android DiskLruCache属于物理性质的缓存,相较于LruCache缓存,则DiskLruCache属于Android二级缓存中的最后一级。通常Android缓存分为两级,第一级是内存缓存,第二级是物理缓存也即DiskLruCache。顾名思义,DiskLruCache就是将数据缓存到Android的物理介质如外部存储器存储卡、内部存储器存储卡上。

关于LruCache缓存即内存缓存,我在之前写过一系列文章,详情请见附录文章2,3。本文介绍Android硬件级的缓存策略:DiskLruCache。

DiskLruCache的Android谷歌官方实现代码链接:

DiskLruCache.java Android谷歌官方源代码实现链接:https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

事实上,由于DiskLruCache实现原理和过程透明公开,有不少第三方实现,在github上有一个比较流行的DiskLruCache开源实现版本,其项目主页:https://github.com/JakeWharton/DiskLruCache

本文将基于JakeWharton实现的DiskLruCache开源库为例说明。使用JakeWharton实现的DiskLruCache,需要先将github上的代码下载,下载后,直接复制到自己项目代码java目录下作为自己的源代码直接使用即可。

(1)DiskLruCache的初始化。

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize);

DiskLruCache使用前首先需要从一个静态方法open创建一个DiskLruCache实例。

一个缓存目录directory,缓存目录directory可以用Android系统提供的默认缓存目录,也可以自己指定一个显而易见的目录。

DiskLruCache在open缓存目录时候,如果前后appVersion不同则销魂缓存。

valueCount类似于指明一个数组的长度,通常是1,是1 的话,那么在后面写缓存newOutputStream时候是newOutputStream(0),因为类似数组下标。长度为1的数组,那么数组中只有一个元素且该元素的下标是0。同样,读的时候也是getInputStream(0)。

maxSize意义简单,指定DiskLruCache缓存的大小,总不能让DiskLruCache无限制缓存吧。所以一般要给DiskLruCache指定一个适当的缓存尺寸和限制,一般是10 * 1024 * 1024,10MB。

我写的初始化例子:

private void makeDiskLruCache() {
        try {
            File cacheDir = getDiskCacheDir(this, UNIQUENAME);

            if (!cacheDir.exists()) {
                Log.d(TAG, "缓存目录不存在,创建之...");
                cacheDir.mkdirs();
            } else
                Log.d(TAG, "缓存目录已存在,不需创建.");

            //第二个参数我选取APP的版本code。DiskLruCache如果发现第二个参数version不同则销毁缓存
            //第三个参数为1,在写缓存的流时候,newOutputStream(0),0为索引,类似数组的下标
            mDiskLruCache = DiskLruCache.open(cacheDir, getVersionCode(this), 1, DISK_CACHE_MAX_SIZE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(2)往DiskLruCache写缓存的一般过程。

DiskLruCache的缓存是<K,V>结构。缓存写入DiskLruCache,首先要从DiskLruCache获得一个DiskLruCache.Editor,用DiskLruCache.Editor的editor传递参数key进去,再获得一个OutputStream,拿到这个OutputStream,就可以把DiskLruCache当作一个接收数据的输出流往里面写数据。写完不要忘记commit。一个DiskLruCache写缓存的代码片段:

//把byte字节写入缓存DiskLruCache
    private void writeToDiskLruCache(String url, byte[] buf) throws Exception {
        Log.d(TAG, url + " : 开始写入缓存...");

        //DiskLruCache缓存需要一个key,我先把url转换成md5字符串,
        //然后以md5字符串作为key键
        String key=urlToKey(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);

        OutputStream os = editor.newOutputStream(0);
        os.write(buf);
        os.flush();
        editor.commit();

        mDiskLruCache.flush();

        Log.d(TAG, url + " : 写入缓存完成.");
    }

(3)从DiskLruCache读缓存的一般过程。

直接的以之前写缓存时候的key键从DiskLruCache中读取:DiskLruCache.get(key),获得一个快照DiskLruCache.Snapshot,如果这个DiskLruCache.Snapshot为null,则说明没有缓存,如果有,则说明已经缓存,然后从DiskLruCache.Snapshot组建一个输入流把缓存数据恢复出来即可。读缓存的代码:

//从DiskLruCache中读取缓存
    private Bitmap readBitmapFromDiskLruCache(String url) {
        DiskLruCache.Snapshot snapShot = null;
        try {
            //把url转换成一个md5字符串,然后以这个md5字符串作为key
            String key = urlToKey(url);

            snapShot = mDiskLruCache.get(key);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (snapShot != null) {
            Log.d(TAG, "发现缓存:" + url);
            InputStream is = snapShot.getInputStream(0);
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            Log.d(TAG, "从缓存中读取Bitmap.");

            return bitmap;
        } else
            return null;
    }

再写一个完整的简单例子说明。一个ImageView,ImageView需要加载一个网路图片,该图片是我的csdn博客头像。例子中,代码启动后,在为ImageView加载网络图片时候,会首先检查本地DiskLruCache中是否有缓存,如果有则直接使用缓存,如果没有,则重新开启一个线程下载图片资源,图片下载完成后,一方面要设置到ImageView中,同时要把图片数据写入DiskLruCache缓存中。

package zhangphil.app;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;

import com.jakewharton.disklrucache.DiskLruCache;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private Handler handler;

    private ExecutorService pool;
    // 默认的线程池容量
    private int DEFAULT_TASK_NUMBER = 10;

    private final int WHAT = 0xe001;

    private String TAG = "zhangphil_tag";

    private String UNIQUENAME = "zhangphil_cache";

    private DiskLruCache mDiskLruCache = null;

    //缓存大小
    private int DISK_CACHE_MAX_SIZE = 10 * 1024 * 1024;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化DiskLruCache,创建DiskLruCache实例
        makeDiskLruCache();

        //创建容量为 asyncTaskNumber 的线程池。
        pool = Executors.newFixedThreadPool(DEFAULT_TASK_NUMBER);

        final ImageView image = (ImageView) findViewById(R.id.image);

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case WHAT:
                        image.setImageBitmap((Bitmap) msg.obj);
                }
            }
        };

        //一个测试的URL连接,从这个链接下载一个图片加载到ImageView中
        String image_url = "http://avatar.csdn.net/9/7/A/1_zhangphil.jpg";

        getBitmap(image_url);
    }

    private void getBitmap(String url) {
        //首先从DiskLruCache读取缓存,缓存是否有该url的图片缓存
        Bitmap bmp = readBitmapFromDiskLruCache(url);

        if (bmp == null) {
            //如果缓存中没有,则创建一个线程下载
            Thread t = new DownloadThread(url);

            //把线程放到线程池中下载
            pool.execute(t);
        } else {
            //在DiskLruCache中发现缓存,直接复用
            sendResult(bmp);
        }
    }

    //从DiskLruCache中读取缓存
    private Bitmap readBitmapFromDiskLruCache(String url) {
        DiskLruCache.Snapshot snapShot = null;
        try {
            //把url转换成一个md5字符串,然后以这个md5字符串作为key
            String key = urlToKey(url);

            snapShot = mDiskLruCache.get(key);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (snapShot != null) {
            Log.d(TAG, "发现缓存:" + url);
            InputStream is = snapShot.getInputStream(0);
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            Log.d(TAG, "从缓存中读取Bitmap.");

            return bitmap;
        } else
            return null;
    }

    //把byte字节写入缓存DiskLruCache
    private void writeToDiskLruCache(String url, byte[] buf) throws Exception {
        Log.d(TAG, url + " : 开始写入缓存...");

        //DiskLruCache缓存需要一个key,我先把url转换成md5字符串,
        //然后以md5字符串作为key键
        String key=urlToKey(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);

        OutputStream os = editor.newOutputStream(0);
        os.write(buf);
        os.flush();
        editor.commit();

        mDiskLruCache.flush();

        Log.d(TAG, url + " : 写入缓存完成.");
    }

    private void makeDiskLruCache() {
        try {
            File cacheDir = getDiskCacheDir(this, UNIQUENAME);

            if (!cacheDir.exists()) {
                Log.d(TAG, "缓存目录不存在,创建之...");
                cacheDir.mkdirs();
            } else
                Log.d(TAG, "缓存目录已存在,不需创建.");

            //第二个参数我选取APP的版本code。DiskLruCache如果发现第二个参数version不同则销毁缓存
            //第三个参数为1,在写缓存的流时候,newOutputStream(0),0为索引,类似数组的下标
            mDiskLruCache = DiskLruCache.open(cacheDir, getVersionCode(this), 1, DISK_CACHE_MAX_SIZE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 开辟一个下载线程
    private class DownloadThread extends Thread {

        private String url;

        public DownloadThread(String url) {
            this.url = url;
        }

        @Override
        public void run() {

            try {
                byte[] imageBytes = loadRawDataFromURL(url);

                // 数据下载完毕,把新的bitmap数据写入DiskLruCache缓存
                writeToDiskLruCache(url, imageBytes);

                Bitmap bmp = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);

                sendResult(bmp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 发送消息通知:bitmap已经下载完成。
    private void sendResult(Bitmap bitmap) {
        Message message = handler.obtainMessage();
        message.what = WHAT;
        message.obj = bitmap;
        handler.sendMessage(message);
    }

    //从一个url下载原始数据,本例是一个图片资源。
    public byte[] loadRawDataFromURL(String u) throws Exception {
        Log.d(TAG, "开始下载 " + u);

        URL url = new URL(u);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        InputStream is = conn.getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        final int BUFFER_SIZE = 2048;
        final int EOF = -1;

        int c;
        byte[] buf = new byte[BUFFER_SIZE];

        while (true) {
            c = bis.read(buf);
            if (c == EOF)
                break;

            baos.write(buf, 0, c);
        }

        conn.disconnect();
        is.close();

        byte[] data = baos.toByteArray();
        baos.flush();

        Log.d(TAG, "下载完成! " + u);

        return data;
    }

    /*
    *
    * 当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,
    * 否则就调用getCacheDir()方法来获取缓存路径。
    * 前者获取到的就是 /sdcard/Android/data/<application package>/cache
    * 而后者获取到的是 /data/data/<application package>/cache 。
    *
    * */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath = null;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }

        File dir = new File(cachePath + File.separator + uniqueName);
        Log.d(TAG, "缓存目录:" + dir.getAbsolutePath());

        return dir;
    }

    //版本名
    public static String getVersionName(Context context) {
        return getPackageInfo(context).versionName;
    }

    //版本号
    public static int getVersionCode(Context context) {
        return getPackageInfo(context).versionCode;
    }

    private static PackageInfo getPackageInfo(Context context) {
        PackageInfo pi = null;

        try {
            PackageManager pm = context.getPackageManager();
            pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS);

            return pi;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return pi;
    }

    public String urlToKey(String url) {
        return getMD5(url);
    }

    /*
    * 传入一个字符串String msg,返回Java MD5加密后的16进制的字符串结果。
    * 结果形如:c0e84e870874dd37ed0d164c7986f03a
    */
    public static String getMD5(String msg) {
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        md.reset();
        md.update(msg.getBytes());
        byte[] bytes = md.digest();

        String result = "";
        for (byte b : bytes) {
            // byte转换成16进制
            result += String.format("%02x", b);
        }

        return result;
    }
}

涉及到Android网络操作和存储设备的读写,不要忘记加相关权限:

<!-- SDCard中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 向SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <uses-permission android:name="android.permission.INTERNET"></uses-permission>

附录文章:

1,《基于Java LinkedList,实现Android大数据缓存策略》链接地址:http://blog.csdn.net/zhangphil/article/details/44116885

2,《使用新式LruCache取代SoftReference缓存图片,Android异步加载图片》链接地址:http://blog.csdn.net/zhangphil/article/details/43667415

3,《使用Android新式LruCache缓存图片,基于线程池异步加载图片》链接地址:http://blog.csdn.net/zhangphil/article/details/44082287

4,《Java MD5(字符串)》链接地址:http://blog.csdn.net/zhangphil/article/details/44152077

5,《从一个URL下载原始数据,基于byte字节》链接地址:http://blog.csdn.net/zhangphil/article/details/43794837

6,《Android获取App版本号和版本名》链接地址:http://blog.csdn.net/zhangphil/article/details/43795099



时间: 2024-08-26 08:22:13

Android二级缓存之物理存储介质上的缓存DiskLruCache的相关文章

Android OkHttp与物理存储介质缓存:DiskLruCache(2)

 Android OkHttp与物理存储介质缓存:DiskLruCache(2) 本文在附录文章8,9的基础之上,把Android OkHttp与DiskLruCache相结合,综合此两项技术,实现基于OkHttp的物理存储介质缓存DiskLruCache. 用一个完整的例子加以说明.该例子的代码要实现这样的过程:代码启动后,要往一个ImageView里面加载一张网络图片,首先检查DiskLruCache是否已经存在该图片的缓存,如果存在,则直接复用缓存,如果不存在则使用OkHttp把图片异

android上的缓存、缓存算法和缓存框架

1.使用缓存的目的 缓存是存取数据的临时地,因为取原始数据代价太大了,加了缓存,可以取得快些.缓存可以认为是原始数据的子集,它是从原始数据里复制出来的,并且为了能被取回,被加上了标志. 在android开发中,经常要访问网络数据比如大量网络图片,如果每次需要同一张图片都去网络获取,这代价显然太大了.可以考虑设置本地文件缓存和内存 缓存,存储从网络取得的数据:本地文件缓存空间并非是无限大的,容量越大读取效率越低,可设置一个折中缓存容量比如10M,如果缓存已满,我们需要采用合 适的替换策略换掉一个已

IOS 区分缓存 内存 物理存储 逻辑存储

1. 存储器分为内部存储器(内存)和外部存储器(外存). ①内存 内存是电脑内部临时存放数据的地方,供CPU直接读取,存放在其中的数据要靠电来维持,一旦断电就会丢失.因此,在操作电脑时,应及时地将需要保存的信息进行保存. 内存的特点是:容量小,速度极快,临时存放数据. ②外存 外存包括软盘.硬盘和光盘,存放在其中的数据靠磁来维持,因此可永久保存数据. 外存的特点:容量很大,速度较慢,可永久保存数据. 2. 物理卷Physical Volume,称为PV:指物理上硬盘,一个硬盘就是一个PV逻辑卷组

蚂蚁金服技术专家分享25个分布式缓存实践与线上案例

前言: 本文主要介绍使用分布式缓存的优秀实践和线上案例.这些案例是笔者在多家互联网公司里积累并形成的优秀实践,能够帮助大家在生产实践中避免很多不必要的生产事故. 一.缓存设计的核心要素 我们在应用中决定使用缓存时,通常需要进行详细的设计,因为设计缓存架构看似简单,实则不然,里面蕴含了很多深奥的原理,如果使用不当,则会造成很多生产事故甚至是服务雪崩之类的严重问题. 1.容量规划 缓存内容的大小缓存内容的数量淘汰策略缓存的数据结构每秒的读峰值每秒的写峰值2.性能优化 线程模型预热方法缓存分片冷热数据

HTML5定稿了,终于有一种编程语言开发的程序可以在Android和IOS两种设备上运行了

2007 年 W3C (万维网联盟)立项 HTML5,直至 2014 年 10 月底,这个长达八年的规范终于正式封稿. 过去这些年,HTML5 颠覆了 PC 互联网的格局,优化了移动互联网的体验,接下来,HTML5 将颠覆原生 App 世界.这听起来有点危言耸听,但若认真分析 HTML5 的发展史,你会发现,这个世界的发展趋势确实就是这样. 熟知历史才能预知未来,先让我们来看看 HTML5 为什么诞生.这 8 年是怎么过来的. 一. HTML5 的诞生 自 W3C 于 1999 年发布 HTML

Android应用开发:Fragment与大型数据缓存

引言 在Android应用开发:Fragment的非中断保存setRetaineInstance一文中已经介绍过了如何让Fragment不随着Activity销毁从而保存数据的方法.在移动应用程序的架构设计中,界面与数据即不可分割又不可混淆.在绝大部分的开发经历中,我们都是使用Fragment来进行界面编程,即使保存数据基本上也只是界面相关控件的数据,很少做其他的数据保存,毕竟这样与开发原则相背,而今天这一篇博客就要来介绍一下Fragment的另类用法,只是用来保存数据而没有任何界面元素. 实现

Android之ListView异步加载网络图片(优化缓存机制)

网上关于这个方面的文章也不少,基本的思路是线程+缓存来解决.下面提出一些优化: 1.采用线程池 2.内存缓存+文件缓存 3.内存缓存中网上很多是采用SoftReference来防止堆溢出,这儿严格限制只能使用最大JVM内存的1/4 4.对下载的图片进行按比例缩放,以减少内存的消耗 具体的代码里面说明.先放上内存缓存类的代码MemoryCache.java: [java] view plain copy public class MemoryCache { private static final

Android游戏开发:物理游戏之重力系统开发--圆形自由落体Demo

本节为大家提供有关物理游戏的知识,讲解了一个简单的圆形自由落体Demo的编写.. Java代码 package com.himi; import java.util.Random; import java.util.Vector; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import a

移动应用开发(IOS/android等)中一个通用的图片缓存方案讲解(附流程图)

在移动应用开发中,我们经常会遇到从网络请求图片到设备上展示的场景. 如果每次都重复发起请求,浪费流量.浪费电量,用户体验也不佳: 将图片持久化到磁盘也不失为一种策略:但每次从文件读取图片也存在一定的io开销,就算采用此策略,我们也需要控制磁盘缓存的容量,以免占用过多系统资源. 其实没有一个方案可以说是完美的方案,只有最适合自己业务需求的方案,才可以说是一个好方案. 我们下面所讲解的方案具备很强的通用性,设计思路简单而清晰: 1.假设每个网络图片的url具有唯一性,如果网络上的图片变化了,会引起输