Android 大规模图片缓存方案

简介: 话不多说,起因都是 ReLIFE 那部动画啊!

前言

之前因为很喜欢 ReLIFE 那部动画,所以从百度云上下载了 500MB 左右的漫画全集(此时应该喊 “完结撒花” 吗(笑)),打算在手机上看。可是直接用图库之类的软件看起来会很累,所以花了点时间做了个应用(程序员的唯一好处): ViewPager 嵌套 ListView,横向为每一话的漫画,纵向是该话的内容。

初步跑起来是可以看,但纵向滑动的时候很卡。经查发现是解码 jpg 为 bitmap 这个地方很慢,所以看来只能异步加载。试了一下,虽然流畅一点,可是列表在 “飘” 的时候,会看到很多默认图片,之后才显示出来漫画的图片。如果一次性全部解码到内存呢?我曾这么想过,但立刻被证实是愚蠢的想法,漫画的一话内容,可能会有几十张图片,张张大图,全部解码,内存必然会爆掉。幸亏做 rec 的时候,发现 bitmap 可以从 biyebuffer 中复制像素数据,也可以将像素数据复制到 bytebuffer 中, 所以试过 “愚蠢方案” 之后,我打算利用这个特性来做一个“基于虚拟内存的图片缓存方案”。

ByteBuffer

顾名思义,这是一个 “缓存” 之类的东西,但是我更愿意将它理解为 java 语言的“高级指针”。

平时我们也会经常使用缓存,比如一个 byte 数组。而 ByteBuffer 除了包含一块元素为 byte 类型的存储区域以外,还包含了一个指向这块区域某处的 position、这块区域结束位置的 limit,并且一个 bytebuffer 还能“生出(duplicate)”另一个与其共享存储区域,但 position 和 limit 完全独立的 bytebuffer 出来。

除此之外,bytebuffer 具备一些好玩的功能,比如虽然我们以一个 byte 数组来创建一个 bytebuffer,但却可以将其转为一个 intbuffer、charbuffer 之类,元素类型非 byte 的缓存对象,之前我对 musical.ly 的安卓半年崩溃日志做去重的时候就大量使用了 bytebuffer 和 charbuffer 互转的功能。

不过最厉害的应该是 java 在 FileChannel 类中提供了将磁盘文件的一个区域 map 到内存中作为一个 bytebuffer 的功能,而本文要介绍的“方案”就是基于这个功能来做。

**方案一:单文件缓存方案
**
流程图

我将这个工具叫做 “ImageBuffer” 。它主要包含 open、put、get 和 close 几个主要方法,下面这几个方法的实现流程图:

__20180725103403

imagebuffer 只是提供了 reset 的功能,但没有提供 remove,而且 reset 也只是将索引队列清空、将 raf 的位置归零而已。之所以不做 remove,是因为从流程图可以看出,像素数据都缓存在文件里面,remove 掉一个缓存元素,除了从索引中这个对象外,实际上对缓存文件没有任何影响,所以名不副实。

另一方面,imagebuffer 可以说是没有容量限制的,而且由于少了解码的操作,还原图片的速度甚至比所谓“多级缓存”的文件缓存要还要快——我就是为了比它快才做 imagebuffer。

从更长远来说,imangebuffer 可以不仅缓存在一个文件里面,而是每一个图片都缓存在一个单独的文件,并在其关闭前一直保留隐射,这样子只是在 put 时稍慢(因为要打开文件),但 get 不受影响,而且删除功能还可以实际实现。进一步,如果缓存文件可以不删除——或者采用 LRU 的方式删除长久不使用的,则只要保存索引,可以作为替代现在 mobtools 中多级缓存中的文件缓存方案。

当然,imagebuffer 有明显的缺点,因为缓存在文件中的是 rgb 的像素数据,所以缓存时文件会比 jpg 的大很多,很浪费磁盘空间。

源码

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.ArrayList;

public class ImageBuffer {
   private File file;
   private RandomAccessFile raf;
   private int position;
   private ArrayList<Image> buffers;
   private byte[] buffer;

   public void open() throws Throwable {
      open(null);
   }

   public synchronized void open(File file) throws Throwable {
      if (this.file == null) {
         if (file == null) {
            file = File.createTempFile("ib_", "");
         }
         if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
         }
         this.file = file;
         raf = new RandomAccessFile(file, "rw");
         position = 0;
         buffers = new ArrayList<Image>();
      }
   }

   public synchronized void reset() throws Throwable {
      raf.seek(0);
      position = 0;
      buffers = new ArrayList<Image>();
   }

   public synchronized void close() throws Throwable {
      if (file != null) {
         raf.close();
         file.delete();
         file = null;
         raf = null;
         buffer = null;
         position = 0;
         buffers = null;
      }
   }

   public synchronized boolean put(Bitmap bm) throws Throwable {
      if (file != null) {
         int bc = bm.getByteCount();
         if (buffer == null || buffer.length < bc) {
            buffer = new byte[bc];
         }
         ByteBuffer bb = ByteBuffer.wrap(buffer);
         bm.copyPixelsToBuffer(bb);
         return put(buffer, 0, bc, bm.getWidth(), bm.getHeight(), bm.getConfig());
      }
      return false;
   }

   public synchronized boolean put(byte[] pixels, int offset, int len, int width, int height, Config config)
         throws Throwable {
      if (file != null) {
         raf.write(pixels, offset, len);
         ByteBuffer bb = raf.getChannel().map(MapMode.READ_ONLY, position, len);
         position += len;

         Image image = new Image();
         image.config = config;
         image.width = width;
         image.height = height;
         image.buffer = bb;
         return buffers.add(image);
      }
      return false;
   }

   public synchronized Bitmap get(int index) throws Throwable {
      if (buffers == null || index < 0 || buffers.size() < index) {
         return null;
      }

      Image image = buffers.get(index);
      image.buffer.position(0);
      Bitmap bm = Bitmap.createBitmap(image.width, image.height, image.config);
      bm.copyPixelsFromBuffer(image.buffer);
      return bm;
   }

   public synchronized int size() {
      return buffers == null ? 0 : buffers.size();
   }

   private class Image {
      private int width;
      private int height;
      private ByteBuffer buffer;
      private Config config;
   }

}

方案二:多文件缓存方案

前段时间做了方案一,但是对于它不能删除失效缓存的特性很是不舒服,所以做了下面基于文件夹(多文件)的方案:

源码

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.HashMap;

public class ImageBuffer {
   private File folder;
   private byte[] buffer;
   private HashMap<String, Image> buffers;

   public ImageBuffer(String folder) {
      this(new File(folder));
   }

   public ImageBuffer(File folder) {
      this.folder = folder;
      buffers = new HashMap<String, Image>();
   }

   public void put(String key, Bitmap bm) throws Throwable {
      int bc = bm.getByteCount();
      ByteBuffer bb;
      synchronized (this) {
         if (buffer == null || buffer.length < bc) {
            buffer = new byte[bc];
         }
         bb = ByteBuffer.wrap(buffer);
      }
      bm.copyPixelsToBuffer(bb);
      put(key, buffer, 0, bc, bm.getWidth(), bm.getHeight(), bm.getConfig());
   }

   public void put(String key, byte[] pixels, int offset, int len, int width, int height,
         Config config) throws Throwable {
      saveImage(key, pixels, offset, len, width, height, config);
      openImage(key);
   }

   private void openImage(String name) throws Throwable {
      File file = new File(folder, name);
      synchronized (this) {
         if (!file.exists()) {
            return;
         }
      }

      int width = 0;
      int height = 0;
      Config config = null;
      RandomAccessFile raf = new RandomAccessFile(file, "r");
      ByteBuffer bb = raf.getChannel().map(MapMode.READ_ONLY, 0, file.length());
      int id = bb.getInt();
      while (id != -1) {
         switch (id) {
            case 0: {
               width = bb.getInt();
            } break;
            case 1: {
               height = bb.getInt();
            } break;
            case 2: {
               switch(bb.getInt()) {
                  case 1: config = Config.ALPHA_8; break;
                  case 3: config = Config.RGB_565; break;
                  case 4: config = Config.ARGB_4444; break;
                  case 5: config = Config.ARGB_8888; break;
               }
            } break;
         }
         id = bb.getInt();
      }
      raf.seek(0);

      Image image = new Image();
      image.file = file;
      image.raf = raf;
      image.buffer = raf.getChannel().map(MapMode.READ_ONLY, bb.position(), bb.remaining());
      image.width = width;
      image.height = height;
      image.config = config;

      synchronized (this) {
         buffers.put(name, image);
      }
   }

   private void saveImage(String key, byte[] pixels, int offset, int len, int width, int height, Config config)
         throws Throwable {
      Image image;
      synchronized (this) {
         image = buffers.remove(key);
      }
      if (image != null) {
         closeImage(image, true);
      }

      File file = new File(folder, key);
      synchronized (this) {
         if (!folder.exists()) {
            folder.mkdirs();
         }
      }
      FileOutputStream fos = new FileOutputStream(file);
      DataOutputStream dos = new DataOutputStream(fos);
      dos.writeInt(0);
      dos.writeInt(width);
      dos.writeInt(1);
      dos.writeInt(height);
      dos.writeInt(2);
      switch(config) {
         case ALPHA_8: dos.writeInt(1); break;
         case RGB_565: dos.writeInt(3); break;
         case ARGB_4444: dos.writeInt(4); break;
         case ARGB_8888: dos.writeInt(5); break;
      }
      dos.writeInt(-1);
      dos.write(pixels, offset, len);
      dos.flush();
      dos.close();
   }

   private void closeImage(Image image, boolean delete) throws Throwable {
      if (image != null) {
         image.raf.close();
         if (delete) {
            image.file.delete();
         }
      }
   }

   public Bitmap get(String key) throws Throwable {
      Image image;
      synchronized(this) {
         image = buffers.get(key);
      }
      if (image == null) {
         openImage(key);
         synchronized(this) {
            image = buffers.get(key);
         }
         if (image == null) {
            return null;
         }
      }

      image.buffer.position(0);
      Bitmap bm = Bitmap.createBitmap(image.width, image.height, image.config);
      bm.copyPixelsFromBuffer(image.buffer);
      return bm;
   }

   public void remove(String key) throws Throwable {
      remove(key, false);
   }

   public void remove(String key, boolean delete) throws Throwable {
      Image image;
      synchronized (this) {
         image = buffers.remove(key);
      }
      closeImage(image, delete);
   }

   public void clear() throws Throwable {
      clear(false);
   }

   public void clear(boolean delete) throws Throwable {
      synchronized(this) {
         for (Image image : buffers.values()) {
            closeImage(image, delete);
         }
         buffers.clear();
      }
   }

   public int size() {
      synchronized(this) {
         return buffers.size();
      }
   }

   private class Image {
      private File file;
      private RandomAccessFile raf;
      private ByteBuffer buffer;      // id = -1
      private int width;              // id = 0
      private int height;             // id = 1
      private Config config;          // id = 2
   }

}

补充说明

1、分文件存储图片,每一个图片一个文件,故删除和清理有了实际意义,而打开、关闭和重置都没有意义了;

2、图片描述跟随缓存,因此每一个缓存文件都分头部和数据体;

3、删除和清理可以选择逻辑删除和物理删除,逻辑删除只会在内存中删除索引 Image 对象,物理删除就是删除文件了;

4、由于图片独立存储,故同步锁的粒度可以降低,某种程度上提高效率(吧……)。

原文发布时间为:2018-07-25
本文作者: 勋勋
本文来自云栖社区合作伙伴“ 安卓巴士Android开发者门户”,了解相关信息可以关注“ 安卓巴士Android开发者门户”。

相关文章
|
4月前
|
消息中间件 canal 缓存
项目实战:一步步实现高效缓存与数据库的数据一致性方案
Hello,大家好!我是热爱分享技术的小米。今天探讨在个人项目中如何保证数据一致性,尤其是在缓存与数据库同步时面临的挑战。文中介绍了常见的CacheAside模式,以及结合消息队列和请求串行化的方法,确保数据一致性。通过不同方案的分析,希望能给大家带来启发。如果你对这些技术感兴趣,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
257 6
项目实战:一步步实现高效缓存与数据库的数据一致性方案
|
4月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
5月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
48 2
|
3月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
101 15
Android 系统缓存扫描与清理方法分析
|
1月前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
54 3
|
4月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
83 20
Android经典面试题之图片Bitmap怎么做优化
|
4月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
89 8
|
4月前
|
开发框架 Dart 前端开发
Android 跨平台方案对比之Flutter 和 React Native
本文对比了 Flutter 和 React Native 这两个跨平台移动应用开发框架。Flutter 使用 Dart 语言,提供接近原生的性能和丰富的组件库;React Native 则基于 JavaScript,具备庞大的社区支持和灵活性。两者各有优势,选择时需考虑团队技能和项目需求。
458 8
|
4月前
|
Web App开发 网络协议 Android开发
Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【9月更文挑战第4天】本文详细对比了在Android平台上实现一对一音视频通话时常用的WebRTC、RTMP及RTSP三种技术方案。从技术原理、性能表现与开发难度等方面进行了深入分析,并提供了示例代码。WebRTC适合追求低延迟和高质量的场景,但开发成本较高;RTMP和RTSP则在简化开发流程的同时仍能保持较好的传输效果,适用于不同需求的应用场景。
233 1
|
5月前
|
存储 安全 API
Android经典实战之存储方案对比:SharedPreferences vs MMKV vs DataStore
本文介绍了 Android 开发中常用的键值对存储方案,包括 SharedPreferences、MMKV 和 DataStore,并对比了它们在性能、并发处理、易用性和稳定性上的特点。通过实际代码示例,帮助开发者根据项目需求选择最适合的存储方案,提升应用性能和用户体验。
182 1