带你学习Android图片缓存机制

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发

一. 前言

我们为什么要学习图片缓存机制?简单来说,就是帮助用户省时省流量。当用户使用RecyclerView或者ListView的时候,频繁的发起网络请求不仅会消耗大量的流量,还会消耗大量的时间,毫无疑问,这会让用户的体验相当糟糕。虽然Glide等图片加载框架已经替我们处理好了图片缓存的问题,但是我们仍然有必要去了解和学习图片缓存机制,知其然才能知其所以然。

二. 思路

带你学习Android缓存机制.png

三. 简单了解Android图片缓存机制

在这里,我们只要了解Android图片的三级缓存机制就行了,何为三级缓存机制?

  1. 内存缓存,读取速度最快。
  2. 硬盘缓存(文件缓存),读取速度比内存缓存稍慢。
  3. 网络缓存,读取速度最慢。

所以,我们正确的图片的读取顺序应该是 内存缓存 > 硬盘缓存 > 网络缓存,讲到这里,我们是不是要手动开始撸码了?看官别急,我们先讲解一下基本的API。

四. 了解一下常用的API

缓存机制的通用调度算法是LRU(最近最久未使用),不熟悉的同学,可以自行谷歌,本篇不做介绍。与内存缓存和硬盘缓存对应的类分别是LruCacheDiskLruCache,Android在Android 3.1加入了LruCache缓存类,而DiskLruCache并非谷歌官方编写,所以我们在写程序的时候不能直接调用,好在Jake Wharton大神集成了库,我们直接用就好了,只要在build.gradle添加如下语句:

implementation 'com.jakewharton:disklrucache:2.0.2'

1.LruCache常用API介绍

方法 简介
LruCache(int maxSize) 构造方法,maxSize是缓存大小
put(@NonNull K key, @NonNull V value) 以键值对的方式存入内存缓存
get(@NonNull K key) 使用键取出存入的值
remove(@NonNull K key) 从内存缓存中移除指定键的值

2.DiskLruCache常用API介绍

介绍之前,我们需要了解DiskLruCache使用比LruCache复杂,我们不能直接使用构造方法直接创建一个DiskLruCache,而是使用open(File directory, int appVersion, int valueCount, long maxSize)这个静态方法创建。如果想要将数据存入缓存,需要通过一个key获取到DiskLruCache.Editor对象,然后使用Editor对象获取输出流将我们的数据存入硬盘缓存,最后使用flush更新journal文件。对于想要深入探究的同学,请移步郭神的Android DiskLruCache完全解析,硬盘缓存的最佳方案

方法 简介
open(File directory, int appVersion, int valueCount, long maxSize) directory是缓存目录,appVersion是版本号,valueCount是指定key可以对应多个缓存数量,
get(String key) 返回Snapshot对象,通过调用该对象的getInputStream(int index)方法可以获取输入流
edit(String key) 返回DiskLruCache.Editor对象
DiskLruCache.Editor的newOutputStream(int index) 创建一个输出流,可以用来存入数据
DiskLruCache.Editor的commit() 在使用输出流缓存数据后,使用commit()才会生效
DiskLruCache.Editor的abort() commit()方法相反,使用abort()终止缓存生效
flush() 同步缓存日志到journal文件

这些是我们常用的方法,当然还有计算当前缓存数据字节的size()方法、关闭DiskLruCache的close()方法和清空缓存的delete()方法等。

五. 手撸代码

网络请求这里我们使用Okhttp,同样需要在build.gradle中添加一行代码,如下:

implementation 'com.squareup.okhttp3:okhttp:3.12.1'

1. 布局
布局这里挺简单,就是一个线性布局里面放一个RecyclerViewRecyclerView子布局里面就是一个ImageView,具体的可以看代码。
2. GridPhotoAdapter
这个适配器可以说是本文里面最重要的一个类了(需要继承自RecyclerView.Adapter),我们慢慢往下看。

    // 照片的网络路径
    private String[] urls;
    // 内存缓存
    private LruCache mMemoryCache;
    // 硬盘缓存
    private DiskLruCache mDisLruCache;
    // OkhttpClient
    private OkHttpClient okHttpClient;
    // 线程池 用来请求下载图片
    private ExecutorService service;
    // 主线程Handler 用来图片下载完成后更新ImageView
    private Handler mHandler;
    private Context mContext;

上面是我们需要用到的实例,作用已经在注释中标注出来了。接下来我们来介绍我们的构造函数和一些初始化工作:

    public GridPhotoAdapter(String[] urls, Handler mHandler, Context context) {
        this.urls = urls;
        this.mHandler = mHandler;
        this.mContext = context;
        init();
    }

    /*
        一些必要的初始化的工作
     */
    private void init() {
        okHttpClient = new OkHttpClient.Builder()
                .build();

        // 构建一定数量的线程池
        service = Executors.newFixedThreadPool(6);

        // 构建内存缓存
        // 取最大的1/8内存作为内存缓存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache(cacheSize){
            @Override
            protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                return value.getByteCount();
            }
        };

        // 构建硬盘缓存实例
        File file = getDiskCacheDir(mContext,"photo");
        if(!file.exists())
            file.mkdirs();
        try {
            mDisLruCache = DiskLruCache.open(file,getAppInfoVersion(),1,10*1024*1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
        根据传入的uniqueName获取唯一的硬盘的缓存路径
    */
    private File getDiskCacheDir(Context context, String uniqueName) {
        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 + uniqueName);
    }

    /*
        获取当前程序的应用版本号
    */
    private int getAppInfoVersion() {
        try {
            PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

init()中,我们初始化了okHttpClientservice(线程池)mMemoryCache(内存缓存)mDisLruCache(硬盘缓存)。需要注意的是,我们在构建硬盘缓存路径的时候调用了getDiskCacheDir(Context context, String uniqueName)函数,这个函数给我们的程序提供了一个缓存地址。介绍完了构造函数,我们再来看一下继承自RecyclerView.Adapter必须要复写的三个方法:

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View root = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycle_item_net_work,viewGroup,false);
        ViewHolder viewHolder = new ViewHolder(root);
        viewHolder.imageView = root.findViewById(R.id.grid_photo);
        // root.setTag(urls[i]);
        return viewHolder;
    }

    public class ViewHolder extends RecyclerView.ViewHolder{
        public ImageView imageView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    @Override
    public int getItemCount() {
        // 返回路径的长度
        return urls.length;
    }

onCreateViewHolder()getItemCount()很简单,这里就不再介绍了。这边我们着重介绍onBindViewHolder()方法:

    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
        ImageView imageView = viewHolder.imageView;
        String url = urls[i];
        // imageView.setTag(url);
        imageView.setImageResource(R.drawable.shape_item_empty);
        loadBitmaps(imageView, url);
    }

在上面的函数中,我们先找到控件ImageView和路径url,在真正的搜索图片缓存之前先设置一个占位图,然后到了我们真正进行图片请求的函数loadBitmaps(ImageView imageView, String url):

    /**
     * 加载Bitmap对象,如果Bitmap不在LruCache中,就开启线程去查询
     *
     * @param imageView 图片
     * @param url       地址
     */
    private void loadBitmaps(ImageView imageView, String url) {
        Bitmap bitmap = getBitmapFromMemoryCache(url);
        if (bitmap != null) {
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        } else {
            service.execute(new ImageRunnable(url,imageView));
        }
    }

    // 添加Bitmap到内存缓存中
    private void addBitmapToMemoryCache(Bitmap bitmap, String url) {
        if (getBitmapFromMemoryCache(url) == null)
            mMemoryCache.put(url, bitmap);
    }

loadBitmaps()函数中,我们先从内存缓存中查找是否有该路径的缓存,有的话就直接放到我们的ImageView中,没有就利用我们的线程池执行一个ImageRunnable,我们再来看看ImageRunnable的代码:

public class ImageRunnable implements Runnable {
        private String url;
        private Bitmap bitmap;
        private ImageView mView;

        public ImageRunnable(String url,ImageView imageView) {
            this.url = url;
            this.mView = imageView;
        }

        @Override
        public void run() {
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            DiskLruCache.Snapshot snapshot = null;
            // 对url进行加密得到key
            final String key = hashKeyForDisk(url);
            // 查找key对应的硬盘缓存
            try {
                snapshot = mDisLruCache.get(key);
                if (snapshot == null) {
                    // 如果对应的硬盘缓存没找到,就开始网络请求,并且写入缓存
                    DiskLruCache.Editor editor = mDisLruCache.edit(key);
                    if (editor != null) {
                        OutputStream outputStream = editor.newOutputStream(0);
                        if (downloadImage(url, outputStream)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    snapshot = mDisLruCache.get(key);
                }

                if (snapshot != null) {
                    fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                // 将缓存数据解析成Bitmap对象
                if (fileDescriptor != null)
                    bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                if (bitmap != null) {
                    // 将图片添加到内存缓存中
                    addBitmapToMemoryCache(bitmap, url);
                }
                if (bitmap != null)
                    // 在主线程中更新
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                           /* ImageView image = mRecyclerView.findViewWithTag(url);
                            if (image != null)
                                image.setImageBitmap(bitmap);*/
                           mView.setImageBitmap(bitmap);
                        }
                    });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

这里的逻辑其实也比较简单,首先通过url得到我们的key,然后利用key去取我们的硬盘缓存,取不到的情况下进行网络请求,利用输出流存到我们的硬盘路径下面,最后取到我们的硬盘缓存,在主线程中更新我们的ImageView。不要以为我们到此就结束了,我们的下载图片downloadImage(final String url, OutputStream outputStream)还没有看,哈哈~

    /*
        下载图片
     */
    private boolean downloadImage(final String url, OutputStream outputStream) {
        Request request = new Request.Builder()
                .url(url)
                .build();

        // 执行操作
        Call call = okHttpClient.newCall(request);
        Response response = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            response = call.execute();
            in = new BufferedInputStream(response.body().byteStream(), 8 * 1024);
            out = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

利用Okhttp写的同步下载图片请求,看代码就ok。我们再来回顾一下逻辑,用一张流程图概括吧:

循环结构流程图.png

3. NetWorkActivity

 public class NetWorkActivity extends AppCompatActivity {

    public final static String[] imageThumbUrls = new String[]{
            // 路径省略了 具体的可以看代码
    };

    private GridPhotoAdapter mAdapter;

    public static void show(Context context) {
        Intent intent = new Intent(context, NetWorkActivity.class);
        context.startActivity(intent);
    }

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

        initWidget();
    }

    private void initWidget() {
        RecyclerView mRecyclerView = findViewById(R.id.recycle);
        Handler mHandler = new Handler(Looper.getMainLooper());
        mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerView.setAdapter(mAdapter = new GridPhotoAdapter(imageThumbUrls, mHandler,this));
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 将日志同步到journal文件中
        mAdapter.flushCache();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 退出程序时结束所有的下载任务
        mAdapter.cancelDownloadImage();
    }
}

这里省略了相关urls,代码就是写RecyclerView的通用代码,同学们可以自行查看。写完效果就出来了,如图:

手机效果图.png

六. 总结

通过以上的学习,相信同学们可以对Android图片缓存机制有了更深入的了解,本人水平有限,如有错误,欢迎指出,Over~
地址:
Demo地址
引用:
Android照片墙完整版,完美结合LruCache和DiskLruCache

你可能感兴趣的:(带你学习Android图片缓存机制)