【Android】数据缓存

部分摘自产品设计:Android应用-开发技术【数据缓存】Android客户端缓存机制(文字缓存和多媒体文件缓存)


博主最近在写一个类似新闻阅读的App,里面需要用到缓存以支持用户离线阅读,所以就在网上找了一些关于Android数据缓存机制的文章和代码。


为什么要有缓存

无论大型或小型应用,灵活的缓存不仅大大减轻了服务器的压力,而且方便了用户。90%乃至99%的应用并不需要实时更新的(即时通讯类的除外:QQ),而且诟病于蜗牛般的移动网速,与服务器的数据交互是能少则少,这样用户体验才更好。

以博主写的新闻App为例,服务器端可能半小时到1小时更新一次新闻,用户实时更新新闻列表就显得没有必要了。另外,有了缓存用户还可以离线阅读新闻,用户体验能得到较大提高。数据缓存就有如下好处:

  • 减小服务器的压力
  • 提高客户端的响应速度(本地数据提取嘛)
  • 一定程度上支持离线浏览(参考网易的那个新闻应用,个人感觉离线阅读做得非常棒)

那么,什么样的App应当增加缓存管理呢:

  • 提供网络服务的应用
  • 数据更新不需要实时更新,哪怕是3-5分钟的延迟也是可以采用缓存机制
  • 缓存的过期时间是可以接受的

缓存机制
参考了文章上方链接里的文章,博主自己画了一个缓存机制的图。后面给出的有关缓存的代码就是依据这个图实现的,需要说明,博主采用的是文件缓存的方式,数据库缓存还没有仔细想过。


文字的缓存方式
这里暂时不讨论图片或视频的缓存方式。
1. 文件缓存
博主采用的就是文件缓存。文字缓存分为两种,根据两者的更新频率区分它们的过期时间。
  • 更新比较频繁的区域:缓存设置过期时间(如:过期时间为应用程序内,即应用程序从打开到关闭的这段时间),有专门的缓存文件夹存放该类缓存文件,并且及时清空过期缓存。
  • 更新不频繁的区域:缓存不设置过期时间,而是提供按钮或Menu,让用户选择手动更新。我的好友列表、我的订阅、我的分享等模块就是这样的区域。
通常情况下客户端与服务器交互,数据都是采用Json格式,Json格式的数据其实就是一段字符串,将这些字符串写入一个文件中,保存在SD卡里,每次将字符串读出来以后解析Jason。 判断缓存是否过期时,用File.lastModified()方法得到文件的最后修改时间,与当前时间对比,从而实现缓存效果。

关于 缓存文件的命名,博主说说自己的做法—— 对发送网络请求的URL进行MD5加密,加密后的字符串作为对应缓存文件的文件名。比如博主App中新闻列表是保存在一个缓存文件中,每次请求新闻列表的URL都是相同的,加密后的字符串也就是相同的。每当用户请求列表时,根据文件名,就能定位到上次请求缓存下来的文件,如果已经过期,就发送网络请求,数据更新就完成了。当然,有些数据缓存下来以后,可能不会再被访问到,还是需要通过“删除”操作清理掉。

缓存文件删除策略
  • 每一个模块在每次客户端自动或者用户手动更新的时候删除相应模块的缓存文件,并重新下载新的缓存文件。
  • 在设置界面中提供删除缓存的功能,点击后删除本机所有缓存。
这些缓存管理办法还得依App的具体功能而定,博主的认识也很浅,如果看到更好的方法,再记录下来。

优点:操作简单,本身处理不容易带来其它问题,代价低廉。
缺点:实现上只能使用这一个属性,没有为其它的功能提供技术支持的可能。
2. 数据库缓存
这种方式相对文件缓存复杂一些,博主暂时还没用过,先把文章上方第一个链接里的知识贴下来,以后用到了再总结。

这种方法是在下载完数据文件后,把文件的相关信息,如URL、路经、下载时间、过期时间等存放到数据库,当然我个人建议把URL作为唯一的标识。下次下载的时候根据URL先从数据库中查询,如果查询到当前时间并未过期,就根据路径读取本地文件,从而实现缓存的效果。

优点:灵活存放文件的属性,进而提供了很大的扩展性,可以为其它的功能提供一定的支持。
缺点:从操作上需要创建数据库,每次查询数据库,如果过期还需要更新数据库,清理缓存的时候还需要删除数据库数据,稍显麻烦,而数据库操作不当又容易出现一系列的性能,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]);
        }
    }
}
上面代码用到了FileUtils这个文件处理工具类,代码如下

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;
	}
}

     
     
    
    
   
   

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值