8月份新上了一个项目,涉及到多用户下的数据共享,Android手机的默认用户id是0,也就是我们平时使用的用户,当进入到其他用户时,虽然是同一app,但是处于不同的用户下,通过SharedPreferences共享数据肯定是做不到的,而且SharedPreferences不能存储Bitmap,而在项目中,需要用到视频的第一帧图片,综合考虑之后,决定在公共目录下通过写文件的形式存储Bitmap等数据。本着代码复用的原则,决定通过LruCache和DiskLruCache自定义一个具备二级缓存的加强版的SharedPreferences。
ps:在这里就不介绍LruCache和DiskLruCache的具体用法了,网上可以找到很多详细的解析。
首先定义一个操作类公共的接口:
public interface ICache {
void put(String key, Object value);
void remove(String key) throws IOException;
void clear() throws IOException;
Object getObject(String key);
int getInt(String key);
long getLong(String key);
double getDouble(String key);
float getFloat(String key);
boolean getBoolean(String key);
Bitmap getBitmap(String key);
String getString(String key);
byte[] getBytes(String key);
}
然后定义分别定义MemoryCache和DiskCache去实现ICache接口,采用LruCache、DiskLruCache来实现一二级缓存。
/**
* This class uses {@link LruCache} to cache data and does not allow null
* to be used as a key or value. The more information can be found on {@link LruCache}.
*
* Created by Feng Bangquan on 17-11-11
*/
public class MemoryCache implements ICache {
private LruCache<Object, Object> mLruCache;
/**
* @param maxMemorySize this is the maximum sum of the sizes of the entries
* in this cache.
*/
public MemoryCache(int maxMemorySize){
mLruCache = new LruCache<>(maxMemorySize);
}
/**
* Caches {@code value} for {@code key} calling {@link LruCache#put(Object, Object)}
*/
@Override
public void put(String key, Object value) {
mLruCache.put(key, value);
}
/**
* Removes the entry for {@code key} if it exists.
*/
@Override
public void remove(String key) {
mLruCache.remove(key);
}
/**
* Clear the MemoryCache, calling {@link LruCache#evictAll()}
*/
@Override
public void clear() {
mLruCache.evictAll();
}
/**
* Returns the value for {@code key} if it exists in the cache .
* The following methods call {@link #getObject(String)}
* to get the specific types' value:
* @see #getInt(String)
* @see #getBitmap(String)
* @see #getBoolean(String)
* @see #getBytes(String)
* @see #getDouble(String)
* @see #getFloat(String)
* @see #getLong(String)
* @see #getString(String)
*/
@Override
public Object getObject(String key) {
return mLruCache.get(key);
}
@Override
public int getInt(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (int) object;
}
@Override
public long getLong(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (long) object;
}
@Override
public double getDouble(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (double) object;
}
@Override
public float getFloat(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (float) object;
}
@Override
public boolean getBoolean(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (boolean) object;
}
@Override
public Bitmap getBitmap(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (Bitmap) object;
}
@Override
public String getString(String key) {
Object object = mLruCache.get(key);
return object == null ? null : String.valueOf(object);
}
@Override
public byte[] getBytes(String key) {
Object object = mLruCache.get(key);
return object == null ? null : (byte[]) object;
}
}
/**
* This class uses {@link DiskLruCache} to cache data and does not allow null
* to be used as a key or value. The more information can be found on {@link DiskLruCache}.
*
* Created by Feng Bangquan on 17-11-11
*/
public class DiskCache implements ICache {
private DiskLruCache mDiskLruCache;
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param appVersion
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
public DiskCache(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
mDiskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);
}
/**
* Caches {@code value} for {@code key} in {@code directory}
*/
@Override
public void put(String key, Object value) {
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OutputStream outputStream = editor.newOutputStream(0);
ObjectOutputStream objectOutputStream;
if (value instanceof Bitmap) {
outputStream.write(bitmapToBytes((Bitmap)value));
} else if (value instanceof byte[]) {
outputStream.write((byte[]) value);
} else {
objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(value);
objectOutputStream.close();
}
outputStream.close();
editor.commit();
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Calls {@link DiskLruCache#remove(String)} to drop the entry for {@code key}
* if it exists and can be removed.
*/
@Override
public void remove(String key) throws IOException {
mDiskLruCache.remove(key);
}
/**
* Calls {@link DiskLruCache#delete()} to close the cache and deletes all of its stored values.
* This will delete all files in the cache directory including files that weren't created by
* the cache.
*/
@Override
public void clear() throws IOException {
mDiskLruCache.delete();
}
/**
* Returns an object named {@code key}, or null if it doesn't exist is not currently readable.
* The following methods call {@link #getObject(String)} to get the specific types' value:
* @see #getInt(String)
* @see #getLong(String)
* @see #getDouble(String)
* @see #getFloat(String)
* @see #getBoolean(String)
* @return Object
*/
@Override
public Object getObject(String key) {
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
ObjectInputStream objectInputStream = new ObjectInputStream(snapshot.getInputStream(0));
return objectInputStream.readObject();
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public int getInt(String key) {
Object object = getObject(key);
return object == null ? null : (Integer) object;
}
@Override
public long getLong(String key) {
Object object = getObject(key);
return object == null ? null : (Long) object;
}
@Override
public double getDouble(String key) {
Object object = getObject(key);
return object == null ? null : (Double) object;
}
@Override
public float getFloat(String key) {
Object object = getObject(key);
return object == null ? null : (Float) object;
}
@Override
public boolean getBoolean(String key) {
Object object = getObject(key);
return object == null ? null : (Boolean) object;
}
/**
* Returns a Bitmap named {@code key}, or null if it doesn't exist is not currently readable.
* @return Bitmap
*/
@Override
public Bitmap getBitmap(String key) {
try {
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
InputStream inputStream = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
return bitmap == null ? null : bitmap;
} catch (IOException e) {
e.printStackTrace();
return null;
} catch (NullPointerException e) {
e.printStackTrace();
return null;
}
}
@Override
public String getString(String key) {
Object object = getObject(key);
return object == null ? null : (String)object;
}
/**
* Returns bytes[] named {@code key}, or null if it doesn't exist is not currently readable.
* @return
*/
@Override
public byte[] getBytes(String key) {
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
return snapshot == null ? null : streamToBytes(snapshot.getInputStream(0));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* Transforms Bitmap to bytes[]
* @param bitmap the Bitmap to be transformed
* @return a byte[] transforms from Bitmap
*/
private byte[] bitmapToBytes(Bitmap bitmap) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
try {
byteArrayOutputStream.flush();
byteArrayOutputStream.close();
if (!bitmap.isRecycled()){
bitmap.recycle();
}
} catch (IOException e) {
e.printStackTrace();
}
return byteArrayOutputStream.toByteArray();
}
/**
* Transforms InputStream to byte[]
* @param in the InputStream to be transformed
* @return a byte[] transforms from InputStream
*/
private byte[] streamToBytes(InputStream in) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int length;
byte[] bytes = new byte[1024];
try {
while ((length = in.read(bytes)) != -1) {
out.write(bytes, 0, length);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return out.toByteArray();
}
}
最后在Cache类中分别实例化MemoryCache和DiskCache,Cache.open()传入DiskCache的存储路径以及其它初始化参数,再通过putXXX(key, value)和getXXX(key, value)即可实现对数据的存储和读取。
public class Cache {
private static MemoryCache mMemoryCache;
private static DiskCache mDiskCache;
/**
* Opens the cache in {@code directory}, creating a cache if none exists there.
*
* @param directory a writable directory
* @param appVersion the value must be positive
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxDiskSize the maximum number of bytes this diskCache should use to store
* @param maxMemorySize the maximum number of kilobytes this memoryCache should be to store
* @throws IOException if reading or writing the cache directory fails
*/
public static void open(File directory, int appVersion, int valueCount, long maxDiskSize, int maxMemorySize) throws IOException {
mMemoryCache = new MemoryCache(maxMemorySize);
mDiskCache = new DiskCache(directory, appVersion, valueCount, maxDiskSize);
}
/**
* Removes the entry for {@code key} if it exists.
* @throws IOException
*/
public static void remove(String key) throws IOException {
mMemoryCache.remove(key);
mDiskCache.remove(key);
}
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
* @throws IOException
*/
public static void clear() throws IOException {
mMemoryCache.clear();
mDiskCache.clear();
}
/**
* Caches {@code value} for {@code key} calling {@link LruCache#put(Object, Object)}
* and {@link DiskCache#put(String, Object)}. The following methods putXXX(key, value)
* cache the specific types' data.
* @param key the value of key can not contain (" ") , ("\n"), ("\r")
* @param value it does not support generic types
*/
public static void put(String key, Object value) {
mMemoryCache.put(key, value);
mDiskCache.put(key, value);
}
public static void putString(String key, String value){
put(key, value);
}
public static void putInt(String key, int value) {
put(key, value);
}
public static void putFloat(String key, float value) {
put(key, value);
}
public static void putDouble(String key, double value){
put(key, value);
}
public static void putBytes(String key, byte[] value) {
put(key, value);
}
public static void putBoolean(String key, boolean value) {
put(key, value);
}
public static void putBitmap(String key, Bitmap value) {
put(key, value);
}
/**
* Returns the value for {@code key} if it exists in the MemoryCache or return
* the value if it exists in the DiskCache, otherwise return null. The following
* method getXXX(key, value) return specific types' value.
* @param key
* @return
*/
public static Object getObject(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getObject(key);
} else {
return mDiskCache.getObject(key);
}
}
public static int getInt(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getInt(key);
} else {
return mDiskCache.getInt(key);
}
}
public static long getLong(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getLong(key);
} else {
return mDiskCache.getLong(key);
}
}
public static double getDouble(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getDouble(key);
} else {
return mDiskCache.getDouble(key);
}
}
public static float getFloat(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getFloat(key);
} else {
return mDiskCache.getFloat(key);
}
}
public static boolean getBoolean(String key) {
if (mMemoryCache.getObject(key) != null) {
return mMemoryCache.getBoolean(key);
} else {
return mDiskCache.getBoolean(key);
}
}
public static Bitmap getBitmap(String key) {
Bitmap bitmap = mMemoryCache.getBitmap(key);
if (bitmap != null && !bitmap.isRecycled()) {
return bitmap;
} else {
return mDiskCache.getBitmap(key);
}
}
public static String getString(String key) {
if (mMemoryCache.getString(key) != null) {
return mMemoryCache.getString(key);
} else {
return mDiskCache.getString(key);
}
}
public static byte[] getBytes(String key) {
if (mMemoryCache.getBytes(key) != null) {
return mMemoryCache.getBytes(key);
} else {
return mDiskCache.getBytes(key);
}
}
}
下面是一个测试用例:
/**
* A demo activity to guide how to use Cache to store data
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private static final String KEY_INT = "int";
private static final String KEY_STRING = "string";
private static final String KEY_BOOLEAN = "boolean";
private static final String KEY_BITMAP = "bitmap";
private static final String KEY_BYTES = "bytes";
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10; // 100MB
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final int maxMemorySize = (int) (Runtime.getRuntime().maxMemory() / 1024);
try {
File cacheDir = getDiskCacheDir("mySharedPreferences");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
Cache.open(cacheDir, 1, 1, DISK_CACHE_SIZE, (maxMemorySize / 8));
} catch (IOException e) {
e.printStackTrace();
}
(findViewById(R.id.put)).setOnClickListener(this);
(findViewById(R.id.get)).setOnClickListener(this);
(findViewById(R.id.remove)).setOnClickListener(this);
(findViewById(R.id.clear)).setOnClickListener(this);
}
public File getDiskCacheDir(String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = getExternalCacheDir().getPath();
} else {
cachePath = getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
private void putCache() {
new Thread(new Runnable() {
@Override
public void run() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.picture, options);
Cache.putBitmap(KEY_BITMAP, bitmap);
Cache.putInt(KEY_INT, 1024);
Cache.putString(KEY_STRING, "read the source code");
Cache.putBoolean(KEY_BOOLEAN, true);
}
}).start();
}
private void getCache() {
Bitmap bitmap = Cache.getBitmap(KEY_BITMAP);
((ImageView) findViewById(R.id.image_view)).setImageBitmap(bitmap);
System.out.println("getInt: " + Cache.getInt(KEY_INT));
System.out.println("getString: " + Cache.getString(KEY_STRING));
System.out.println("getBoolean: " + Cache.getBoolean(KEY_BOOLEAN));
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.put) {
putCache();
}
if (v.getId() == R.id.get) {
getCache();
}
if (v.getId() == R.id.clear) {
try {
Cache.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
if (v.getId() == R.id.remove) {
try {
Cache.remove(KEY_INT);
Cache.remove(KEY_BYTES);
Cache.remove(KEY_BOOLEAN);
Cache.remove(KEY_BITMAP);
Cache.remove(KEY_BYTES);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ImageView上显示出了照片,说明bitmap能够正确存储和读取:
01-04 13:08:30.529 17505-17505/com.fengbangquan.cache I/System.out: getInt: 1024
01-04 13:08:30.534 17505-17505/com.fengbangquan.cache I/System.out: getString: read the source code
01-04 13:08:30.544 17505-17505/com.fengbangquan.cache I/System.out: getBoolean: true
这样,具有二级缓存,能够存储Bitmap的自定义SharedPreferences就完成了,需要注意的是Cache在多线程的操作不是线程安全的。完整代码下载地址:https://github.com/fengbangquan/Cache