zoukankan      html  css  js  c++  java
  • 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

    
  • 相关阅读:
    python03-if
    python03
    基础知识梳理
    开篇话
    托管代码---> CLR --> 自宿主
    反射定义及基础案例
    c# 中委托的发展
    委托代码案例
    委托(实例)
    字节(Byte) 与 位(bit)
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6147283.html
Copyright © 2011-2022 走看看