前言
之前因为很喜欢 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 几个主要方法,下面这几个方法的实现流程图:
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开发者门户”。