原文地址:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1209/2136.html
离线缓存就是在网络畅通的情况下将从服务器收到的数据保存到本地,当网络断开之后直接读取本地文件中的数据。
将网络数据保存到本地:
你可以自己写一个保存数据成本地文件的方法,保存在android系统的任意目录(当然是有权限的才行),但是在这种情况下使用Context的openFileOutput方法最简便也最符合我们的场景,下面的saveObject方法演示了如何用openFileOutput将数据保存在本地的一个文件中:
saveObject
1 public static boolean saveObject(Serializable ser, String file) { 2 FileOutputStream fos = null; 3 ObjectOutputStream oos = null; 4 try { 5 fos = AppContext.getInstance().openFileOutput(file, AppContext.getInstance().MODE_PRIVATE); 6 oos = new ObjectOutputStream(fos); 7 oos.writeObject(ser); 8 oos.flush(); 9 return true; 10 } catch (Exception e) { 11 e.printStackTrace(); 12 return false; 13 } finally { 14 try { 15 oos.close(); 16 } catch (Exception e) { 17 } 18 try { 19 fos.close(); 20 } catch (Exception e) { 21 } 22 } 23 }
openFileOutput可以直接获得一个和应用关联的文件路径(在/data/data/<package name>/files下面),然后使用java io中的ObjectOutputStream将序列化的对象写入(writeObject
)到得到的文件中,你可以看到上面的实现过程有两个关键方法:openFileOutput、writeObject
以及调用它们的两个关键对象Context和ObjectOutputStream
。关于序列化可以参看这篇文章:Java对象的序列化和反序列化实践
这是将一个序列化的对象保存在本地,跟我们的离线缓存保存网络数据有什么关系呢?
有关系,因为网上获取的数据大多可以转换成String类型的字符串,现在服务端返回的数据一般是json格式的字符串。而String类型的字符串其实就是可序列化的对象。下面是一个服务器返回json数据的例子(其实就是jcodecraeer):
1 {"url":"http://jcodecraeer.com/uploads/soft/android/CodeBox.apk","versionCode":"7","updateMessage":"增加离线缓存,分类筛选功能修正了版本兼容性问题 "}
用上面的saveObject方法我们可 以将数据保存在本地,为了能够取出这个文件我们必须想好如何为这个保存的文件命名,如果是单纯的一篇文章的数据,我们可以直接将文件名命名为这篇文章的 id,因为id是唯一的,为了尽可能的不和其他数据发生冲突,你还可以在这个id之前加一个前缀,比如这篇文章是java栏目下的我们可以这样 arc_java_id。如果是文章列表我们可以这样命名:文章类别_分页页码,总之命名的原则是能和其他离线数据区别,有唯一性。为什么不用url作为 文件名呢?url肯定是唯一的,但是url不一定符合文件的命名规范。
下面来讲解如何读取本地缓存的数据
读取缓存的时候我们只需要知道文件名就可以了,下面的readObject方法实现了根据文件名读取缓存数据。其实很多东西是和上面保存数据对应的。
readObject
1 /** 2 * 读取对象 3 * 4 * @param file 5 * @return 6 * @throws IOException 7 */ 8 public static Serializable readObject(String file) { 9 FileInputStream fis = null; 10 ObjectInputStream ois = null; 11 try { 12 fis = AppContext.getInstance().openFileInput(file); 13 ois = new ObjectInputStream(fis); 14 return (Serializable) ois.readObject(); 15 } catch (FileNotFoundException e) { 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } finally { 19 try { 20 ois.close(); 21 } catch (Exception e) { 22 } 23 try { 24 fis.close(); 25 } catch (Exception e) { 26 } 27 } 28 return null; 29 }
运用
下面的代码演示了如何用上面的知识存储和读取网络数据
1 String key = "codelist_" + mCategory.getValue() + "_" + + page ; 2 String result = ""; 3 //cache 4 if (HttpUtil.isNetworkConnected()) { 5 result = HttpUtil.http_get(AppContext.getInstance(), url ); 6 HttpUtil.saveObject(result, key); 7 result = (String) HttpUtil.readObject(key); 8 } else { 9 result = (String) HttpUtil.readObject(key); 10 if (result == null) 11 result = "erro"; 12 }
当网络畅通时,从服务器获取数据( HttpUtil.http_get(AppContext.getInstance(), url )
),同时将数据保存到本地(HttpUtil.saveObject
),而当网络不可用时,直接从本地读取缓存的数据,不跟服务器发生交互。
其中HttpUtil
是跟网络相关的工具类,这里涉及到它的三个方法:
1 isNetworkConnected()判断网络是否可用 2 saveObject上面已经给出了实现 3 readObject上面已经给出了实现 4 http_get读取指定url的服务器数据
而
AppContext.getInstance()
是我自己写的,是为了方便在HttpUtil
的静态方法中获得Context对象。
这里的key就是文件名。
额外的需求
有时候我们还有这样的需求,当用户在指定间隔时间内读取同一数据源时,从本地获取,超过这个时间间隔从网络获取,这样做的目的是节省用户的流量,同时也避免了每次从网络获取数据造成的界面延迟。
下面实现了如何根据时间间隔判断是否需要刷新服务器数据,true表示不需要,false表示需要(很别扭是吧,这跟isCacheDataFailure
这个命名有关系):
1 public static boolean isCacheDataFailure(String cachefile) { 2 boolean failure = false; 3 File data = AppContext.getInstance().getFileStreamPath(cachefile); 4 if (data.exists() 5 && (System.currentTimeMillis() - data.lastModified()) > CACHE_TIME) 6 failure = true; 7 else if (!data.exists()) 8 failure = true; 9 return failure; 10 }
将当前时间和文件的修改时间做比较 ,CACHE_TIME是一个固定值(毫秒),你可以替换成任意int类型。
将这个判断条件加入,然后上面的代码改成:
1 String key = "codelist_" + mCategory.getValue() + "_" + + page ; 2 String result = ""; 3 //cache 4 if (HttpUtil.isNetworkConnected() && HttpUtil.isCacheDataFailure(key)) { 5 result = HttpUtil.http_get(AppContext.getInstance(), url ); 6 HttpUtil.saveObject(result, key); 7 result = (String) HttpUtil.readObject(key); 8 } else { 9 result = (String) HttpUtil.readObject(key); 10 if (result == null) 11 result = "erro"; 12 }
完善
上面的步骤对于一般应用来说已经够用了,但是在要求比较高的情况下,我们还得考虑随着时间的流逝,缓存数据会越来越多,因此我们需要增加删除过期缓存的功能,原理就是设置一个阀值,在保存缓存的时候,判断当前缓存的总量是否大于阀值,如果是则删除时间较早的缓存。
这个实现起来有点复杂,可以考虑更简单的方案,定期检查(或者用户每打开一次程序)缓存总量,当大于阀值,提示用户主动删除。具体实现就不多说了。
注:openFileOutput()方 法的第一参数用于指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data/<package name>/files目录,如: /data/data/cn.itcast.action/files/itcast.txt ,通过点击Eclipse菜单“Window”-“Show View”-“Other”,在对话窗口中展开android文件夹,选择下面的File Explorer视图,然后在File Explorer视图中展开/data/data/<package name>/files目录就可以看到该文件。
openFileOutput()方法的第二参数用于指定操作模式,有四种模式,分别为: Context.MODE_PRIVATE = 0
Context.MODE_APPEND = 32768
Context.MODE_WORLD_READABLE = 1
Context.MODE_WORLD_WRITEABLE = 2
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
如果希望文件被其他应用读和写,可以传入:
openFileOutput(“itcast.txt”, Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE);