/*
* Copyright (C) 2012 www.amsoft.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ab.cache;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import android.os.SystemClock;
import com.ab.global.AbAppConfig;
import com.ab.util.AbLogUtil;
import com.ab.util.AbStreamUtil;
// TODO: Auto-generated Javadoc
/**
*
* © 2012 amsoft.cn
* 名称:AbDiskBasedCache.java
* 描述:磁盘缓存
* @author 还如一梦中
* @date 2015年4月3日 上午9:59:42
* @version v1.0
*/
public class AbDiskBaseCache implements AbDiskCache {
/** 所有缓存文件. */
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<String, CacheHeader>(16, .75f, true);
/** 当前缓存大小. */
private long mTotalSize = 0;
/** 缓存根目录. */
private final File mRootDirectory;
/** 最大缓存字节数. */
private final int mMaxCacheSizeInBytes;
/** 缓存达到高水品的百分比. */
private static final float HYSTERESIS_FACTOR = 0.9f;
/** 文件头标识. */
private static final int CACHE_MAGIC = 0x20120504;
/**
* 指定缓存空间的构造.
* @param rootDirectory The root directory of the cache.
* @param maxCacheSizeInBytes The maximum size of the cache in bytes.
*/
public AbDiskBaseCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
initialize();
}
/**
* 默认缓存空间的构造.
* the default maximum cache size of 5MB.
* @param rootDirectory The root directory of the cache.
*/
public AbDiskBaseCache(File rootDirectory) {
this(rootDirectory, AbAppConfig.MAX_DISK_USAGE_INBYTES);
}
/**
* 初始化磁盘缓存文件.
*/
@Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
AbLogUtil.e(AbDiskBaseCache.class,"缓存目录创建失败,"+mRootDirectory.getAbsolutePath());
}
return;
}
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
putEntry(entry.key, entry);
} catch (Exception e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (Exception e) {
}
}
}
}
/**
* 清空所有磁盘缓存.
*/
@Override
public synchronized void clear() {
File[] files = mRootDirectory.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
mEntries.clear();
mTotalSize = 0;
AbLogUtil.d(AbDiskBaseCache.class,"Cache cleared.");
}
/**
* 获取缓存实体.
*
* @param key the key
* @return the entry
*/
@Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
File file = getFileForKey(key);
AbLogUtil.d(AbDiskBaseCache.class, "想要从缓存中获取文件"+file.getAbsolutePath());
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new FileInputStream(file));
CacheHeader.readHeader(cis); // eat header
byte[] data = AbStreamUtil.stream2Bytes(cis, (int) (file.length() - cis.bytesRead));
return entry.toCacheEntry(data);
} catch (Exception e) {
e.printStackTrace();
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (Exception ioe) {
ioe.printStackTrace();
return null;
}
}
}
}
/**
* 添加实体到缓存.
*
* @param key the key
* @param entry the entry
*/
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
FileOutputStream fos = new FileOutputStream(file);
CacheHeader e = new CacheHeader(key, entry);
e.writeHeader(fos);
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
AbLogUtil.d(AbDiskBaseCache.class,"缓存文件删除失败"+file.getAbsolutePath());
}
}
/**
* 从缓存中移除实体.
*
* @param key the key
*/
@Override
public synchronized void remove(String key) {
boolean deleted = getFileForKey(key).delete();
removeEntry(key);
if (!deleted) {
AbLogUtil.d(AbDiskBaseCache.class,"缓存文件删除失败");
}
}
/**
* 从key中生成文件名.
* @param key The key to generate a file name for.
* @return A pseudo-unique filename.
*/
private String getFileNameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
/**
* 从key中得到文件.
*
* @param key the key
* @return the file for key
*/
public File getFileForKey(String key) {
return new File(mRootDirectory, getFileNameForKey(key));
}
/**
* Prunes the cache to fit the amount of bytes specified.
* @param neededSpace The amount of bytes we are trying to fit into the cache.
*/
private void pruneIfNeeded(int neededSpace) {
//可以缓存
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
//释放部分空间
long before = mTotalSize;
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
//删除
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
} else {
AbLogUtil.d(AbDiskBaseCache.class,"Could not delete cache entry for key=%s, filename=%s",
e.key, getFileNameForKey(e.key));
}
iterator.remove();
prunedFiles++;
//删除缓存到这个级别
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
if (AbLogUtil.D) {
AbLogUtil.d(AbDiskBaseCache.class,"pruned %d files, %d bytes, %d ms",
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
}
}
/**
* 将实体加入到缓存中.
* @param key The key to identify the entry by.
* @param entry The entry to cache.
*/
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
}
/**
* 从缓存中移除某个实体.
*
* @param key the key
*/
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null) {
mTotalSize -= entry.size;
mEntries.remove(key);
}
}
/**
* 缓存头部信息.
*/
static class CacheHeader {
/** 内容大小 */
public long size;
/** 实体的key. */
public String key;
/** ETag仅仅是一个和文件相关的标记. */
public String etag;
/** 缓存时间 总毫秒数. */
public long serverTimeMillis;
/** 失效日期 总毫秒数. */
public long expiredTimeMillis;
/** 响应头信息. */
public Map<String, String> responseHeaders;
/**
* 构造.
*/
private CacheHeader() { }
/**
* 构造.
*
* @param key The key that identifies the cache entry
* @param entry The cache entry.
*/
public CacheHeader(String key, Entry entry) {
this.key = key;
this.size = entry.data.length;
this.etag = entry.etag;
this.serverTimeMillis = entry.serverTimeMillis;
this.expiredTimeMillis = entry.expiredTimeMillis;
this.responseHeaders = entry.responseHeaders;
}
/**
* Reads the header off of an InputStream and returns a CacheHeader object.
*
* @param is The InputStream to read from.
* @return the cache header
* @throws IOException Signals that an I/O exception has occurred.
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = AbStreamUtil.readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
entry.key = AbStreamUtil.readString(is);
entry.etag = AbStreamUtil.readString(is);
if (entry.etag.equals("")) {
entry.etag = null;
}
entry.serverTimeMillis = AbStreamUtil.readLong(is);
entry.expiredTimeMillis = AbStreamUtil.readLong(is);
entry.responseHeaders = AbStreamUtil.readStringStringMap(is);
return entry;
}
/**
* Creates a cache entry for the specified data.
*
* @param data the data
* @return the entry
*/
public Entry toCacheEntry(byte[] data) {
Entry e = new Entry();
e.data = data;
e.etag = etag;
e.serverTimeMillis = serverTimeMillis;
e.expiredTimeMillis = expiredTimeMillis;
e.responseHeaders = responseHeaders;
return e;
}
/**
* Writes the contents of this CacheHeader to the specified OutputStream.
*
* @param os the os
* @return true, if successful
*/
public boolean writeHeader(OutputStream os) {
try {
AbStreamUtil.writeInt(os, CACHE_MAGIC);
AbStreamUtil.writeString(os, key);
AbStreamUtil.writeString(os, etag == null ? "" : etag);
AbStreamUtil.writeLong(os, serverTimeMillis);
AbStreamUtil.writeLong(os, expiredTimeMillis);
AbStreamUtil.writeStringStringMap(responseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
AbLogUtil.d(AbDiskBaseCache.class,"%s", e.toString());
return false;
}
}
}
/**
* 计数.
*/
private static class CountingInputStream extends FilterInputStream {
/** The bytes read. */
private int bytesRead = 0;
private CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int result = super.read();
if (result != -1) {
bytesRead++;
}
return result;
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
int result = super.read(buffer, offset, count);
if (result != -1) {
bytesRead += result;
}
return result;
}
}
}