Android异步加载全解析之引入二级缓存

Android异步加载全解析之引入二级缓存

为啥要二级缓存

前面我们有了一级缓存,为啥还要二级缓存呢?说白了,这就和电脑是一样的,我们电脑有内存和硬盘,内存读取速度快,所以CPU直接读取内存中的数据,但是,内存资源有限,所以我们可以把数据保存到硬盘上,这就是二级缓存,硬盘虽然读取速度慢,但是人家容量大。

Android的缓存技术也是使用了这样一个特性,总的来说,使用二级缓存的方案,就是先从一级缓存——内存中拿,没有的话,再去二级缓存——手机中拿,如果还没有,那就只能去下载了。

有了DiskLruCache,我们就可以很方便的将一部分内容缓存到手机存储中,做暂时的持久化保存,像我们经常用的一些新闻聚合类App、ZARKER等,基本都利用了DiskLruCache,浏览过的网页,即使在没有网络的情况下,也可以浏览。

DiskLruCache

配置

DiskLruCache,听名字就知道是LruCache的兄弟,只不过这个应该是Google的私生子,还没有像LruCache一样添加到API中,所以我们只能去官网上下载DiskLruCache的代码,其实也就一个类。下载地址:

https://developer.android.com/samples/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.html#l22

在工程中使用DiskLruCache非常简单,只需要在项目中新建一个libcore.io的包,并将DiskLruCache.java文件copy过去即可。

初始化

在使用DiskLruCache之前,我们需要对缓存的目录进行下配置,DiskLruCache并不需要限定缓存保存的位置,但一般情况下,我们的缓存都保存在缓存目录下: /sdcard/Android/data/package name/cache,当然,如果没有sdcard,那么我们就使用内置存储的缓存区域:/data/data/package name/cache。

在设置好缓存目录后,我们就可以使用DiskLruCache.open方法来创建DiskLruCache:

File cacheDir = getFileCache(context, "disk_caches");
if (!cacheDir.exists()) {
    cacheDir.mkdirs();
}
try {
    mDiskCaches = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
    e.printStackTrace();
}

private File getFileCache(Context context, String cacheFileName) {
    String cachePath;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
            || !Environment.isExternalStorageRemovable()) {
        cachePath = context.getExternalCacheDir().getPath();
    } else {
        cachePath = context.getCacheDir().getPath();
    }
    return new File(cachePath + File.separator + cacheFileName);
}

DiskLruCache.open方法有这样几个参数:

缓存目录

程序版本号:版本更新后,缓存清0

valueCount

缓存大小:随意,但也不能太任性,按字节算

应该不用解释了,唯一值得说的是valueCount这个参数,它是说同一个key可以对应Value的个数,一般都是1,基本没用。最后我们来看看最后返回的:

return new File(cachePath + File.separator + cacheFileName)

这里通过cacheFileName在缓存目录下再创建一个目录是干嘛呢?这个目录是用来对不同的缓存对象进行区分的,例如images、text等等。我们可以通过size()方法来获取所有缓存数据的大小。也可以使用delete()方法来删除所有缓存。

写入缓存

权限:

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

都说了写缓存,那读写权限肯定是不能少了。

DiskLruCache写入缓存与使用SharedPreferences方法类似,需要使用Editor对象:

DiskLruCache.Editor editor = mDiskCaches.edit(key);

传入的key,就是我们需要下载的url地址,例如图片的地址,但是,url经常具有很多非法字符,这些会对我们的解析工作造成很多困难,而且,有时候我们的url地址也是需要保密的,所以我们经常通过MD5来进行url的加密,这样不仅可以加密,而且可以让所有的URL都变为规则的十六进制字符串。下面我们展示一个经典的写入缓存模板代码:

String key = toMD5String(url);

/////////////////////////////////////////////////////////////////////////////////
DiskLruCache.Editor editor = mDiskCaches.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(0);
    if (getBitmapUrlToStream(url, outputStream)) {
        editor.commit();
    } else {
        editor.abort();
    }
}
mDiskCaches.flush();
/////////////////////////////////////////////////////////////////////////////////

public String toMD5String(String key) {
    String cacheKey;
    try {
        final MessageDigest digest = MessageDigest.getInstance("MD5");
        digest.update(key.getBytes());
        cacheKey = bytesToHexString(digest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.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();
}

private static boolean getBitmapUrlToStream(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(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1) {
            out.write(b);
        }
        return true;
    } catch (final IOException e) {
        e.printStackTrace();
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        try {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
    return false;
}

这里唯一的需要注意的是,下载的方法与我们之前使用的方法有所不同,主要是为了通用性,DiskLruCache将对应URL的内容以流的形式进行存储,文件名就是MD5加密后的字符串。

读取缓存

读取缓存的方法大家应该也能想到了,自然是调用get方法:

DiskLruCache.Snapshot snapShot = mDiskCaches.get(key);

不过它返回的是DiskLruCache的Snapshot对象。当我们获取到了Snapshot对象,就可以从它里面获取输出流,从而取出缓存的数据:

DiskLruCache.Snapshot snapShot = mDiskCaches.get(key);
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);

移除缓存

移除缓存,我们可以猜到,我们需要使用remove方法来实现:

mDiskCache.remove(key);

当然,DiskLruCache并不希望我们手动去移除缓存,因为人家用了Lru算法,跟我们在内存中使用的算法一样,该死的时候,它自己会死。

与生命周期绑定

DiskLruCache在使用时,经常与我们的Activity的生命周期进行绑定,例如在onPause()方法中调用flush()方法,将内容与journal日志文件同步,在onDestroy()方法中去调用close()方法结束DiskLruCache的open。

日志同步

通过前面的方法,我们已经可以缓存一个来自网络的图片了。下面我们进入缓存的文件夹,并查看里面的数据:

我们可以发现,这些文件,就是以MD5命名的缓存文件,它的最后面,有一个journal文件,我们通过cat命令打开:

这里我们选取一类记录,这些记录总是以dirty开头,然后clean,最后read。这个是什么意思呢?第一行dirty代表我们准备开始缓存数据,clean代表我们缓存到数据了,后面的30405代表缓存的大小,最后的read代表进行了读取操作。

看到这里,相信大家已经想起了我们非常熟悉的sqlite,它实际上也是利用文件来进行存储的。DiskLruCache实际上就是模拟了一个简化的sqlite,它的实现机制与sqlite基本类似。

引入二级缓存

ok,我们回到原来的项目,给工程增加二级缓存,导入DiskLruCache的源文件,这里就不讲了。我们在前面一级缓存的基础上,修改下ImageLoaderWithCaches类,创建ImageLoaderWithDoubleCaches类,这里面我们只需要在构造方法中增加对DiskLruCache的初始化,在AsyncTask中,我们来修改二级缓存的逻辑。前面的步骤相同,在取图像的时候都从内存缓存中取,如果取不到,那么在AsyncTask在硬盘缓存中取,如果还取不到,那就去下载,同时,将下载好的图像加入内存缓存,如果硬盘缓存中有,那么就直接加入内存缓存。看起来其实还是非常简单的,只要修改下AsyncTask即可。

package com.imooc.listviewacyncloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;

import libcore.io.DiskLruCache;

public class ImageLoaderWithDoubleCaches {

    private Set<ASyncDownloadImage> mTasks;
    private LruCache<String, Bitmap> mMemoryCaches;
    private DiskLruCache mDiskCaches;
    private ListView mListView;

    public ImageLoaderWithDoubleCaches(Context context, ListView listview) {
        this.mListView = listview;
        mTasks = new HashSet<>();
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 10;
        mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };

        File cacheDir = getFileCache(context, "disk_caches");
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
        try {
            mDiskCaches = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void showImage(String url, ImageView imageView) {
        Bitmap bitmap = getBitmapFromMemoryCaches(url);
        if (bitmap == null) {
            imageView.setImageResource(R.drawable.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    public Bitmap getBitmapFromMemoryCaches(String url) {
        return mMemoryCaches.get(url);
    }

    public void addBitmapToMemoryCaches(String url, Bitmap bitmap) {
        if (getBitmapFromMemoryCaches(url) == null) {
            mMemoryCaches.put(url, bitmap);
        }
    }

    public void loadImages(int start, int end) {
        for (int i = start; i < end; i++) {
            String url = Images.IMAGE_URLS[i];
            Bitmap bitmap = getBitmapFromMemoryCaches(url);
            if (bitmap == null) {
                ASyncDownloadImage task = new ASyncDownloadImage(url);
                mTasks.add(task);
                task.execute(url);
            } else {
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private File getFileCache(Context context, String cacheFileName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + cacheFileName);
    }

    private static boolean getBitmapUrlToStream(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(), 8 * 1024);
            out = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public void cancelAllTasks() {
        if (mTasks != null) {
            for (ASyncDownloadImage task : mTasks) {
                task.cancel(false);
            }
        }
    }

    public String toMD5String(String key) {
        String cacheKey;
        try {
            final MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(key.getBytes());
            cacheKey = bytesToHexString(digest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.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 void flushCache() {
        if (mDiskCaches != null) {
            try {
                mDiskCaches.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> {

        private String url;

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

        @Override
        protected Bitmap doInBackground(String... params) {
            url = params[0];
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            DiskLruCache.Snapshot snapShot = null;
            String key = toMD5String(url);
            try {
                snapShot = mDiskCaches.get(key);
                if (snapShot == null) {
                    DiskLruCache.Editor editor = mDiskCaches.edit(key);
                    if (editor != null) {
                        OutputStream outputStream = editor.newOutputStream(0);
                        if (getBitmapUrlToStream(url, outputStream)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    snapShot = mDiskCaches.get(key);
                }
                if (snapShot != null) {
                    fileInputStream = (FileInputStream) snapShot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                Bitmap bitmap = null;
                if (fileDescriptor != null) {
                    bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                }
                if (bitmap != null) {
                    addBitmapToMemoryCaches(params[0], bitmap);
                }
                return bitmap;
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fileDescriptor == null && fileInputStream != null) {
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                    }
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTasks.remove(this);
        }
    }
}

整体代码与之前使用一级缓存的代码基本相同,大家只要在AsyncTask修改一定逻辑就好了。

再次运行程序,与之前使用一级缓存的图相同,这里就不贴了,只是这里在断网后,同样可以加载缓存中的图片。

以上,未完待续,后面我们会进一步优化>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

代码下载地址  http://download.csdn.net/detail/x359981514/8562525

时间: 2024-10-22 06:09:24

Android异步加载全解析之引入二级缓存的相关文章

Android异步加载全解析之引入一级缓存

Android异步加载全解析之引入缓存 为啥要缓存 通过对图像的缩放,我们做到了对大图的异步加载优化,但是现在的App不仅是高清大图,更是高清多图,动不动就是图文混排,以图代文,如果这些图片都加载到内存中,必定会OOM.因此,在用户浏览完图像后,应当立即将这些废弃的图像回收,但是,这又带来了另一个问题,也就是当用户在浏览完一次图片后,如果还要返回去再进行重新浏览,那么这些回收掉的图像又要重新进行加载,保不准就要那些无聊到蛋疼的人在那一边看你回收GC,一边看你重新加载.这两件事情,肯定是互相矛盾的

Android异步加载全解析之Bitmap

Android异步加载全解析之Bitmap 在这篇文章中,我们分析了Android在对大图处理时的一些策略--Android异步加载全解析之大图处理  戳我戳我 那么在这篇中,我们来对图像--Bitmap进行一个更加细致的分析,掌握Bitmap的点点滴滴. 引入 Bitmap这玩意儿号称Android App头号杀手,特别是3.0之前的版本,简直就是皇帝般的存在,碰不得.摔不得.虽然后面的版本Android对Bitmap的管理也进行了一系列的优化,但是它依然是非常难处理的一个东西.在Androi

Android异步加载全解析之使用AsyncTask

Android异步加载全解析之使用AsyncTask 概述 既然前面提到了多线程,就不得不提到线程池,通过线程池,不仅可以对并发线程进行管理,更可以提高他们执行的效率,优化整个App.当然我们可以自己创建一个线程池,不过这样是很烦的,要创建一个高效的线程池还是挺费事的,不过,Android系统给我吗提供了AsyncTask这样一个类,来帮助我们快速实现多线程开发,它的底层实现,其实就是一个线程池. AsyncTask初探 AsyncTask,顾名思义就是用来做异步处理的.通过AsyncTask,

Android异步加载全解析之大图处理

Android异步加载全解析之大图处理 异步加载中非常重要的一部分就是对图像的处理,这也是我们前面用异步加载图像做演示例子的原因.一方面是因为图像处理不好的话会非常占内存,而且容易OOM,另一方面,图像也比文字要大,加载比较慢.所以,在讲解了如何进行多线程.AsyncTask进行多线程加载后,先暂停下后面的学习,来对图像的异步处理进行一些优化工作. 为什么要对图像处理 为什么要对图像进行处理,这是一个很直接的问题,一张图像,不管你拿手机.相机.单反还是什么玩意拍出来,它就有一定的大小,但是在不同

Android异步加载全解析之IntentService

Android异步加载全解析之IntentService 搞什么IntentService 前面我们说了那么多,异步处理都使用钦定的AsyncTask,再不济也使用的Thread,那么这个IntentService是个什么鬼. 相对与前面我们提到的这两种异步加载的方式来说,IntentService有一个最大的特点,就是--IntentService不受大部分UI生命周期的影响,它为后台线程提供了一个更直接的操作方式.不过,IntentService的不足主要体现在以下几点: 不可以直接和UI做

Android异步加载全解析之开篇瞎扯淡

Android异步加载 概述 Android异步加载在Android中使用的非常广泛,除了是因为避免在主线程中做网络操作,更是为了避免在显示时由于时间太长而造成ANR,增加显示的流畅性,特别是像ListView.GridView这样的控件,如果getView的时间太长,就会造成非常严重的卡顿,非常影响性能. 本系列将展示在Android中如何进行异步加载操作,并使用ListView来作为演示的对象. 如何下载图像 下载自然是需要使用网络,使用网络就不能在主线程,在主线程就会爆炸.所以我们必须要在

我的Android进阶之旅------&amp;gt;android异步加载图片显示,并且对图片进行缓存实例

先来看看效果示意图: step1:新建项目DataAsyncLoad,如下图所示 step2:设置应用的UI界面 a.应用的主界面    main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="

Android异步加载图像(含线程池,缓存方法)

研究了android从网络上异步加载图像: (1)由于android UI更新支持单一线程原则,所以从网络上取数据并更新到界面上,为了不阻塞主线程首先可能会想到以下方法.      在主线程中new 一个Handler对象,加载图像方法如下所示 [java] view plaincopyprint? private void loadImage(final String url, final int id) {           handler.post(new Runnable() {  

Android 异步加载图片分析总结_Android

研究了android从网络上异步加载图像,现总结如下: (1)由于android UI更新支持单一线程原则,所以从网络上取数据并更新到界面上,为了不阻塞主线程首先可能会想到以下方法. 在主线程中new 一个Handler对象,加载图像方法如下所示 复制代码 代码如下: private void loadImage(final String url, final int id) { handler.post(new Runnable() { public void run() { Drawable