部分摘自产品设计:Android应用-开发技术【数据缓存】和Android客户端缓存机制(文字缓存和多媒体文件缓存)。
博主最近在写一个类似新闻阅读的App,里面需要用到缓存以支持用户离线阅读,所以就在网上找了一些关于Android数据缓存机制的文章和代码。
为什么要有缓存
无论大型或小型应用,灵活的缓存不仅大大减轻了服务器的压力,而且方便了用户。90%乃至99%的应用并不需要实时更新的(即时通讯类的除外:QQ),而且诟病于蜗牛般的移动网速,与服务器的数据交互是能少则少,这样用户体验才更好。
以博主写的新闻App为例,服务器端可能半小时到1小时更新一次新闻,用户实时更新新闻列表就显得没有必要了。另外,有了缓存用户还可以离线阅读新闻,用户体验能得到较大提高。数据缓存就有如下好处:
- 减小服务器的压力
- 提高客户端的响应速度(本地数据提取嘛)
- 一定程度上支持离线浏览(参考网易的那个新闻应用,个人感觉离线阅读做得非常棒)
那么,什么样的App应当增加缓存管理呢:
- 提供网络服务的应用
- 数据更新不需要实时更新,哪怕是3-5分钟的延迟也是可以采用缓存机制
- 缓存的过期时间是可以接受的
- 更新比较频繁的区域:缓存设置过期时间(如:过期时间为应用程序内,即应用程序从打开到关闭的这段时间),有专门的缓存文件夹存放该类缓存文件,并且及时清空过期缓存。
- 更新不频繁的区域:缓存不设置过期时间,而是提供按钮或Menu,让用户选择手动更新。我的好友列表、我的订阅、我的分享等模块就是这样的区域。
- 每一个模块在每次客户端自动或者用户手动更新的时候删除相应模块的缓存文件,并重新下载新的缓存文件。
- 在设置界面中提供删除缓存的功能,点击后删除本机所有缓存。
缺点:从操作上需要创建数据库,每次查询数据库,如果过期还需要更新数据库,清理缓存的时候还需要删除数据库数据,稍显麻烦,而数据库操作不当又容易出现一系列的性能,ANR问题,指针错误问题,实现的时候要谨慎。还有一个问题,缓存的数据库是存放在/data/data/<package>/databases/目录下,是占用内存空间的,如果缓存累计,容易浪费内存,需要及时清理缓存。
何时刷新
这块儿是文章上方第一个链接里作者给的建议。
开发者希望尽量读取缓存;用户一方面希望实时刷新,另一方面希望响应速度越快越好,流量消耗越少越好,这就是一个矛盾。其实,何时刷新我也不知道,这里提供两点建议:
1. 数据的最长多长时间不变,对应用无大的影响
比如,你的数据更新时间为4小时,则缓存时间设置为1~2小时比较合适。也就是更新时间/缓存时间=2,但用户个人修改、网站编辑人员等一些人为的更新就另说。一天用户总会看到更新,即便有延迟也好,视你产品的用途了;如果你觉得你是资讯类应用,再减少,2~4小时。如果你觉得数据比较重要或者比较受欢迎,用户会经常把玩,再减少,1~2小时,依次类推。
类似这个界面的数据我认为更新时间能多长就多长了,尽可能长。如果你拿后边那个有多少数据会变动来搪塞,我会告诉你:这个只是一个引导性的界面,你有多少款游戏跟用户半毛钱关系都没有,10亿也跟他没关,他只要确定这里能找到他要找的汤姆猫就行,否则你又失去了一个用户。
2. 提供刷新按钮
必要时候或最保险的方法使在相关界面提供一个刷新按钮,或者当下流行的下拉列表刷新方式。为缓存,为加载失败提供一次重新来过的机会。毕竟喝骨头汤的时候,我也不介意碗旁多双筷子。总而言之,一切用户至上,为了更好的用户体验,方法也会层出不穷。
下面给出博主用到的相关代码,再次说明这个代码来自产品设计:Android应用-开发技术【数据缓存】,感谢这位博主。
=============================================华丽丽的分割线===========================================
import java.io.File;
import java.io.IOException;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
/**
* 缓存工具类
*/
public class ConfigCacheUtils
{
private static final String TAG=ConfigCacheUtils.class.getName();
//App缓存文件根目录
private static final String ROOT="/Xiaoting/cache/";
public static final int CONFIG_CACHE_SHORT_TIMEOUT=1000 * 60 * 5; // 5分钟
public static final int CONFIG_CACHE_MEDIUM_TIMEOUT=1000 * 60 * 60 * 1; // 1小时
public static final int CONFIG_CACHE_ML_TIMEOUT=1000 * 60 * 60 * 24 * 1; // 1天
public static final int CONFIG_CACHE_MAX_TIMEOUT=1000 * 60 * 60 * 24 * 7; // 7天
/**
* CONFIG_CACHE_MODEL_LONG : 长时间(7天)缓存模式
* CONFIG_CACHE_MODEL_ML : 中长时间(1天)缓存模式
* CONFIG_CACHE_MODEL_MEDIUM: 中等时间(1小时)缓存模式
* CONFIG_CACHE_MODEL_SHORT : 短时间(5分钟)缓存模式
*/
public enum ConfigCacheModel
{
CONFIG_CACHE_MODEL_SHORT, CONFIG_CACHE_MODEL_MEDIUM, CONFIG_CACHE_MODEL_ML, CONFIG_CACHE_MODEL_LONG;
}
/**
* 获得缓存文件夹路径
* @return 缓存文件夹路径
*/
public static String getCachePath(String path)
{
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
//获取SD卡的目录
File sdDirFile = Environment.getExternalStorageDirectory();
String cachePath=null;
try
{
cachePath = sdDirFile.getCanonicalPath()+path;
FileUtils.CreateDir(cachePath);
} catch (IOException e)
{
e.printStackTrace();
}
return cachePath;
}
return null;
}
/**
* 获取缓存
* @param subPath 缓存文件子目录路径,比如缓存文件在"/Xiaoting/cache/news/"下,此处sebPath="news/"
* @param fileName 缓存文件名,如果有后缀,需要加上后缀
* @param model 缓存模式
* @param context 上下文环境
* @return 缓存数据
*/
public static String getCache(String subPath,String fileName, ConfigCacheModel model, Context context)
{
if(filaName == null)
return null;
String result=null;//存储缓存数据的字符串
boolean deleteCache=false;//标志缓存文件是否过期需要删除
//获取缓存文件
String path= getCachePath(ROOT+subPath) + MD5Utils.toMD5(fileName);
File file=new File(path);
if(file.exists() && file.isFile())
{
//缓存文件存储的时间
long expiredTime=System.currentTimeMillis() - file.lastModified();
// Log.i(TAG, file.getAbsolutePath() + " expiredTime:" + expiredTime / 60000 + "min");
//当网络是无效的,只能读缓存;网络有效时,要判断缓存是否过期
if(NetworkUtils.isNetworkAvailable(context) != false)
{
Log.i(TAG,"Network is abailable!");
if(expiredTime < 0)//系统时间不正确
return null;
if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_SHORT)
{
if(expiredTime > CONFIG_CACHE_SHORT_TIMEOUT)
deleteCache=true;
}
else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_MEDIUM)
{
if(expiredTime > CONFIG_CACHE_MEDIUM_TIMEOUT)
deleteCache=true;
}
else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_ML)
{
if(expiredTime > CONFIG_CACHE_ML_TIMEOUT)
deleteCache=true;
}
else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_LONG)
{
if(expiredTime > CONFIG_CACHE_MAX_TIMEOUT)
deleteCache=true;
}
else
{
if(expiredTime > CONFIG_CACHE_MAX_TIMEOUT)
deleteCache=true;
}
if(deleteCache==true)
{//如果文件已过期,删除缓存
clearCache(file);
return null;
}
}
try
{//读取缓存
result=FileUtils.readTextFile(file);
} catch(IOException e)
{
e.printStackTrace();
}
}
return result;
}
/**
* 写缓存文件
* @param subPath 缓存文件子目录路径,比如缓存文件在"/Xiaoting/cache/news/"下,此处sebPath="news/"
* @param data 要写入缓存的字符串
* @param fileName 缓存文件名,如果有后缀,需要加上后缀
*/
public static void setCache(String subPath,String data, String fileName)
{
String path= getCachePath(ROOT+subPath) + MD5Utils.toMD5(fileName);
File file=new File(path);
try
{
if(!file.exists())//如果文件不存在,就先创建它
file.createNewFile();
// 写缓存数据(覆盖原文件地)
FileUtils.writeTextFile(file, data);
} catch (IOException e)
{
e.printStackTrace();
}
}
/**
* 删除历史缓存文件
* @param cacheFile 要删除的文件获目录
*/
public static void clearCache(File cacheFile)
{
if(cacheFile == null)
{//如果cacheFile为空,就删除缓存文件根目录下的所有文件
File cacheDir=new File(getCachePath(ROOT));
if(cacheDir.exists())
clearCache(cacheDir);
}
else if(cacheFile.isFile())
{//如果cacheFile是文件,就删除它
cacheFile.delete();
}
else if(cacheFile.isDirectory())
{//如果cacheFile是目录,就删除目录下的所有文件
File[] childFiles=cacheFile.listFiles();
for(int i=0; i < childFiles.length; i++)
clearCache(childFiles[i]);
}
}
}
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.util.Log;
/**
* 文件处理工具类
*/
public class FileUtils
{
private static final String TAG=FileUtils.class.getName();
public static final long B = 1;
public static final long KB = B * 1024;
public static final long MB = KB * 1024;
public static final long GB = MB * 1024;
private static final int BUFFER = 8192;
/**
* 获取格式化文件大小,带有单位
* @param size 未格式化的文件大小
*/
public static String formatFileSize(long size)
{
StringBuilder sb = new StringBuilder();
String u = null;
double tmpSize = 0;
if (size < KB) {
sb.append(size).append("B");
return sb.toString();
} else if (size < MB) {
tmpSize = getSize(size, KB);
u = "KB";
} else if (size < GB) {
tmpSize = getSize(size, MB);
u = "MB";
} else {
tmpSize = getSize(size, GB);
u = "GB";
}
return sb.append(twodot(tmpSize)).append(u).toString();
}
/**
* 保留两位小数
* @param d 任意小数
*/
public static String twodot(double d)
{
return String.format("%.2f", d);
}
public static double getSize(long size, long u)
{
return (double) size / (double) u;
}
/**
* 判断sd卡是否挂载且可用
*/
public static boolean isSdCardMounted()
{
return android.os.Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED);
}
/**
* 递归创建文件目录
* @param path 文件目录的路径
* */
public static void CreateDir(String path)
{
if (!isSdCardMounted())
return;
File file = new File(path);
if (!file.exists())
{
try
{
file.mkdirs();
} catch (Exception e)
{
Log.e(TAG, "error on creat dirs:" + e.getStackTrace());
}
}
}
/**
* 读取文件
* @param file
* @throws IOException
*/
public static String readTextFile(File file) throws IOException
{
String text = null;
InputStream is = null;
try
{
is = new FileInputStream(file);
text = readTextInputStream(is);;
} finally
{
if (is != null)
is.close();
}
return text;
}
/**
* 从流中读取文件
* @param is
* @throws IOException
*/
public static String readTextInputStream(InputStream is) throws IOException
{
StringBuffer strbuffer = new StringBuffer();
String line;
BufferedReader reader = null;
try
{
reader = new BufferedReader(new InputStreamReader(is));
while ((line = reader.readLine()) != null)
strbuffer.append(line).append("\r\n");
} finally
{
if (reader != null)
reader.close();
}
return strbuffer.toString();
}
/**
* 将文本内容写入文件
* @param file
* @param str 要写入的文本
* @throws IOException
*/
public static void writeTextFile(File file, String str) throws IOException
{
DataOutputStream out = null;
try
{
out = new DataOutputStream(new FileOutputStream(file));
out.write(str.getBytes());
} finally
{
if (out != null)
out.close();
}
}
/**
* 获取一个文件夹大小
* @param f
*/
public static long getFileSize(File f)
{
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++)
if (flist[i].isDirectory())
size = size + getFileSize(flist[i]);
else
size = size + flist[i].length();
return size;
}
/**
* 删除文件
* @param file
*/
public static void deleteFile(File file)
{
if (file.exists())
{ // 判断文件是否存在
if (file.isFile()) // 判断是否是文件
file.delete();
else if (file.isDirectory())
{ // 否则如果它是一个目录
File files[] = file.listFiles(); // 声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) // 遍历目录下所有的文件
deleteFile(files[i]); // 把每个文件 用这个方法进行迭代
}
file.delete();
}
}
//以下两个方法博主暂时没有用到
//之后如果用到再调试
//此处仅作备用
/**
* 将Bitmap保存本地JPG图片
* @param url
* @return
* @throws IOException
*/
public static String saveBitmap2File(String url) throws IOException
{
BufferedInputStream inBuff = null;
BufferedOutputStream outBuff = null;
SimpleDateFormat sf = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
String timeStamp = sf.format(new Date());
File targetFile = new File(Constants.ENVIROMENT_DIR_SAVE, timeStamp
+ ".jpg");
File oldfile = ImageLoader.getInstance().getDiscCache().get(url);
try {
inBuff = new BufferedInputStream(new FileInputStream(oldfile));
outBuff = new BufferedOutputStream(new FileOutputStream(targetFile));
byte[] buffer = new byte[BUFFER];
int length;
while ((length = inBuff.read(buffer)) != -1) {
outBuff.write(buffer, 0, length);
}
outBuff.flush();
return targetFile.getPath();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inBuff != null) {
inBuff.close();
}
if (outBuff != null) {
outBuff.close();
}
}
return targetFile.getPath();
}
/**
* 读取表情配置文件
* @param context
* @return
*/
public static List
getEmojiFile(Context context) {
try {
List
list = new ArrayList
();
InputStream in = context.getResources().getAssets().open("emoji");// 文件名字为rose.txt
BufferedReader br = new BufferedReader(new InputStreamReader(in,
"UTF-8"));
String str = null;
while ((str = br.readLine()) != null) {
list.add(str);
}
return list;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}