安卓开发有时候需要保存数据到手机本地,这样就可以减少重复请求后台获取数据的麻烦。
安卓缓存方式有5种,远端缓存数据就是后台服务器帮我们保存数据;近端的客户端缓存有三种。
第一种就是SharePreference缓存,这也是我们安卓开发用的最多的客户端缓存方式,特别就是比较简单:
val sharedPreferences = getSharedPreferences("fenji", Context.MODE_PRIVATE)
val edit = sharedPreferences.edit()
edit.putString("save_string","保存本地数据")
edit.commit() //必须提交才能生效
val cacheString = sharedPreferences.getString("save_string", "")
LogUtils.e(">>>>>>>>>>>>>>>>", "保存的SharePreferences数据:$cacheString")
这里我们发现ContextWrapper是我们Application的直接父类,而且Application的生命周期是跟随我们的App从打开创建到App被退出到后台被杀死一直存在的。然后ContextWrapper的直接父类是Context,这里我们就可以知道只要我们获取到Context对象就可以获取到SharedPreferences对象进行本地缓存,缓存方式类似与Java的Map集合,一个Key对应一个Value进行保存。
优点:简单、方便。
第二种:数据库缓存。
因为安卓Framwork框架内置了SQlite轻量级数据库,虽然增删改查的操作都依赖与sdl语句的执行,但是随着安卓开源框架的增加,安卓也有很多解决写sql语句痛点的开源框架:Geendao.
这个数据库缓存集成简单,管理除了后期数据库升级会删除之前数据库的所有数据,需要自己手动的迁移之前数据库的表之外,其他的都是直接配置就好了。
/**
* 创建一个GreenDao实体类,实现对数据的增删改查
*/
public class GreenDaoUtils {
private final DaoSession mDaoSession;
private static GreenDaoUtils daoInstance;
private static KeyValueEntityDao mDaoGuideEntityDao;
/**
* 初始化GreenDao数据库
* @param context
*/
private GreenDaoUtils(Context context){
MyDevOpenHelper devOpenHelper = new MyDevOpenHelper(context, "fj_reading_db", null);
DaoMaster daoMaster = new DaoMaster(devOpenHelper.getWritableDatabase());
mDaoSession = daoMaster.newSession();
mDaoGuideEntityDao = mDaoSession.getKeyValueEntityDao();
}
/**
* 获取单例实例对象
* @param context
* @return
*/
public static GreenDaoUtils getDaoInstance(Context context){
if(daoInstance == null){
synchronized (GreenDaoUtils.class){
if(daoInstance == null){
daoInstance = new GreenDaoUtils(context);
}
}
}
return daoInstance;
}
/**
* 查询引导实例对象
*/
public String queryValueEntityByKey(String guideName){
try {
List entityList = mDaoGuideEntityDao.queryBuilder().list();// 获取表的所有数据列表
for (KeyValueEntity guideEntity : entityList) {
if (guideName.equals(guideEntity.getKey())) {
return guideEntity.getValue();
}
}
}catch (Exception e){
LogUtils.e(">>>>>>>>>>>",e.getMessage());
}
return "";
}
/**
* 创建根据Key,保存Value的方法
*/
public void setEntityDaoValueByKey(String key,String value){
KeyValueEntity valueEntity = new KeyValueEntity(key, value);
mDaoGuideEntityDao.insertOrReplace(valueEntity);
}
}
其实GreenDao可以解决安卓客户端所有的数据缓存场景,而且也是非常方便的。只要把我们需要缓存的数据对象放入到你指定的生成greendao文件目录的entity文件夹下,然后缓存数据类加入注解:
@Entity
public class KeyValueEntity {
然后重新编译项目GreenDao就会自动给你的缓存数据对象生成一个Dao文件类,类名是你的缓存文件的类名+Dao。然后我们通过这个Dao对象进行数据的增删改查操作。
第三种:文件缓存。
如果说安卓数据库、SharePreference缓存的存放目录是存在手机内置内存的,那么文件缓存则是保存数据到手机外置内存卡的。把文件保存到外置内存的好处就是我们可以随时查看,手机内置内存除非是手机被root的情况下才能查看,要不然一般用户是无法查看的。
文件保存除了需要创建一个文件的保存目录以外,还需要让手机获取到读写内存卡的权限。
然后让你需要缓存的数据实体类继承Serializable接口表示这个数据实体类是可以序列化二进制流的,然后编写一个数据缓存工具类:
/**
* 保存服务器的数据到本地,单例模式
*/
class CacheServerDataUtils private constructor() {
/**
* 缓存一个序列化对象到本地文件
*/
fun saveServerDataObjectToLocal(context: Context?, `object`: Any?, fileName: String?) {
try {
val filePath = createFileToSaveServerData(context, fileName) //获取文件最后的修改时间为文件名
val fs = FileOutputStream(filePath)
val os = ObjectOutputStream(fs)
os.writeObject(`object`)
os.close()
fs.close()
} catch (ex: Exception) {
LogUtils.e("缓存对象数据异常:" + ex.message)
}
}
/**
* 反序列化一个本地文件对象
*/
fun getLocalFileToDataObject(context: Context?, fileName: String?): Any? {
var newPerson: Any? = null
try {
val filePath = createFileToSaveServerData(context, fileName) //获取本地文件的文件绝对路径
val file = File(filePath)
if (file.exists()) {
val fileInputStream = FileInputStream(file)
val oin = ObjectInputStream(fileInputStream)
newPerson = oin.readObject()
oin.close()
fileInputStream.close()
}
} catch (e: Exception) {
LogUtils.e("反序列化对象数据异常:" + e.message)
}
return newPerson
}
/**
* 提供一个方法,来判断文件的最后修改时间
*/
private fun getFileLastModifiedTime(context: Context?, fileName: String?): Long {
val filePath = createFileToSaveServerData(context, fileName)
val cacheFile = File(filePath)
if (cacheFile.exists()) {
val cal = Calendar.getInstance()
cal.timeInMillis = cacheFile.lastModified()
val time = cal.time
return time.time
}
return 0
}
/**
* 提供一个方法,判断文件的修改时间是否大于刷新时间,来刷新界面
*/
fun isNeedRefreshLocalCacheData(context: Context?, fileName: String?): Boolean {
//获取一个当前的时间
val currentTimeMillis = System.currentTimeMillis()
//获取一个文件最后修改时间
val lastModifiedTime = getFileLastModifiedTime(context, fileName)
//判断文件的最后保存时间是否大于5分钟
return currentTimeMillis - lastModifiedTime > 1000 * 60 * 5
}
/**
* 创建一个文件夹保存到本地
*/
private fun createFileToSaveServerData(context: Context?, fileName: String?): String {
//获取sdk路径
val phoneSDPath = SystemUtil.getPhoneCacheSDPath(context)
var cacheFileName = ""
if (ObjectUtils.isNotEmpty(phoneSDPath)) {
//获取用户的唯一标识
val userId = UserPreferences.keyUserId
val filePath = (phoneSDPath + File.separator + AppUtils.getAppVersionName()
+ File.separator + userId + File.separator)
//创建一个文件
val cacheFile = File(filePath)
// 如果文件不存在,则创建一个新文件
try {
if (!cacheFile.exists()) {
cacheFile.mkdirs() // 可以创建文件多层目录
}
} catch (e: Exception) {
LogUtils.e("创建一个文件夹异常:" + e.message)
}
cacheFileName = cacheFile.absolutePath + File.separator + "$fileName.txt"
}
return cacheFileName
}
/**
* 根据文件名删除指定的缓存文件
*/
fun deleteCacheFileByName(context: Context?, fileName: String?){
val cacheFileName = createFileToSaveServerData(context, fileName)
LogUtils.e("cacheFileName:$cacheFileName")
try{
val cacheFile = File(cacheFileName)
if(cacheFile.exists()){
cacheFile.delete()
}
}catch (e: Exception){
LogUtils.e("删除缓存文件夹异常:" + e.message)
}
}
companion object {
private var cacheInstance: CacheServerDataUtils? = null
fun getDataCacheInstance(): CacheServerDataUtils {
if (cacheInstance == null) {
synchronized(CacheServerDataUtils::class.java) {
if (cacheInstance == null) {
cacheInstance = CacheServerDataUtils()
}
}
}
return cacheInstance as CacheServerDataUtils
}
}
}
到这里我们就可以实现文件缓存了,但是如果是外置内存卡缓存数据,这里就可以保存很多的数据,只要你不卸载App,缓存到外置内存的数据是不会被清理掉的。一般的App都会在设置界面,有一个显示手机内存的选项,用户可以点击按钮进行手机外置内存的清除操作。
/**
* 缓存清理工具类,文件缓存分临时和文件缓存
* 1)分享保存到手机的图片为临时缓存,getCacheDir()。
* 2)文章内容的数据对象为文件缓存,用户退出登录的时候清除所有用户数据 context.getExternalCacheDir()。
* 3)用户在App点击清理缓存是删除本地缓存的文件夹,就是getCacheDir()文件下的缓存文件。
* 该工具类管理缓存文件的目录和删除本地缓存文件的方法
*/
public class AppCacheFileManager {
/**
* 获取App缓存目录下的所有文件的大小
* @param context
* @return
*/
public static String getTotalCacheSize(Context context) {
long cacheSize = 0;
try {
cacheSize += getFolderSize(context.getCacheDir());
//当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
cacheSize += getFolderSize(context.getExternalCacheDir());
}
} catch (Exception e) {
e.printStackTrace();
}
return getFormatSize(cacheSize);
}
/**
* 清除app临时缓存数据(系统内部/data/data/.../cache/)目录下的数据
*/
public static void cleanAppCacheDirFlies(Context context) {
File cacheCacheDir = context.getCacheDir();
deleteDirFiles(cacheCacheDir);
}
/**
* 清除app所有缓存数据(SD上的/data/data/.../cache/及系统内部/data/data/.../cache/)
* @param context
*/
public static void clearAllCache(Context context) {
cleanAppCacheDirFlies(context);
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File externalCache = context.getExternalCacheDir();
deleteDirFiles(externalCache);
}
}
private static boolean deleteDirFiles(File dirFile) {
if (ObjectUtils.isNotEmpty(dirFile) && dirFile.isDirectory()) {
String[] children = dirFile.list();
for (String str: children) {
boolean success = deleteDirFiles(new File(dirFile, str));
if (!success) {
return false;
}
}
} else if (ObjectUtils.isNotEmpty(dirFile)) {
return dirFile.delete();
}
return false;
}
// 获取文件
// Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/
// 目录,一般放一些长时间保存的数据
// Context.getExternalCacheDir() -->SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据
public static long getFolderSize(File file) {
long size = 0;
File[] fileList = file.listFiles();
for (File localFile: fileList) {
// 如果下面还有文件
if (localFile.isDirectory()) {
size = size + getFolderSize(localFile);
} else {
size = size + localFile.length();
}
}
return size;
}
/**
* 格式化单位
* @param size
*/
public static String getFormatSize(double size) {
double kiloByte = size / 1024;
if (kiloByte < 1) {
return (String.valueOf(size)).concat("Byte");
}
double megaByte = kiloByte / 1024;
if (megaByte < 1) {
BigDecimal result1 = new BigDecimal(Double.toString(kiloByte));
return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString().concat("KB");
}
double gigaByte = megaByte / 1024;
if (gigaByte < 1) {
BigDecimal result2 = new BigDecimal(Double.toString(megaByte));
return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString().concat("MB");
}
double teraBytes = gigaByte / 1024;
if (teraBytes < 1) {
BigDecimal result3 = new BigDecimal(Double.toString(gigaByte));
return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString().concat("GB");
}
BigDecimal result4 = new BigDecimal(teraBytes);
return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString().concat("TB") ;
}
}
这里有一个问题就是,我们保存的数据到底该放在手机内存卡的那个位置?
扩展资料:
1)Android 文件外/内部存储的获取各种存储目录路径
2)android路径获取
这里我们知道安卓手机App创建的时候会有一个内部存储空间,路径就是手机内部存储内存的data/data/应用程序包名。
开发人员可以借助android studio的Device File Explorer工具查看,打开这个工具的位置在As面板的最左下角。App内置内存路径是我们查看的时候是data/data,但是打印出来就可以是下面这个dada/user/0,都是指向的App的内置内存。
/data/user/0/
外置内存我们会发现Api多了一个External描述,意思就是扩展。获取打印的路径是:
/storage/emulated/0
其中emulated的意思是模拟器,这里会让我们产生误解,以为这是模拟器的内存空间。然后我开启了一个模拟器,同样的打印以下代码:
//内部路径
val dataDirectory = Environment.getDataDirectory() //data
LogUtils.e(">>>>>>>>>>>>>", "dataDirectory:$dataDirectory")
val downloadCacheDirectory = Environment.getDownloadCacheDirectory()//cache
LogUtils.e(">>>>>>>>>>>>>", "downloadCacheDirectory:$downloadCacheDirectory")
val rootDirectory = Environment.getRootDirectory() //system
LogUtils.e(">>>>>>>>>>>>>", "rootDirectory:$rootDirectory")
val cacheDir = context.getCacheDir()//data/data/com.penngo.test/cache
LogUtils.e(">>>>>>>>>>>>>", "cacheDir:$cacheDir")
val filesDir = context.getFilesDir()//data/data/com.penngo.test/files
LogUtils.e(">>>>>>>>>>>>>", "filesDir:$filesDir")
// 存储卡路径
val externalStorageDirectory = Environment.getExternalStorageDirectory() //storage/sdcard0
LogUtils.e(">>>>>>>>>>>>>", "externalStorageDirectory:$externalStorageDirectory")
// 与context.getFilesDir(相似,删除应用时会同时删除
val externalFilesDir = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) //storage/sdcard0/Android/data/com.penngo.test/files
LogUtils.e(">>>>>>>>>>>>>", "externalFilesDir:$externalFilesDir")
// 与context.getCacheDir()相似,删除应用时会同时删除
val externalCacheDir = context.getExternalCacheDir()//storage/sdcard0/Android/data/com.penngo.test/cache
LogUtils.e(">>>>>>>>>>>>>", "externalCacheDir:$externalCacheDir")
安卓手机实体机和模拟器打印的输出日志都是下面的日志信息:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:45)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = dataDirectory:/data
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.160 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:47)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = downloadCacheDirectory:/data/cache
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.161 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:49)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = rootDirectory:/system
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.162 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:51)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = cacheDir:/data/user/0/com.fenjiread.learner/cache
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.163 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:53)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = filesDir:/data/user/0/com.fenjiread.learner/files
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.168 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:57)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = externalStorageDirectory:/storage/emulated/0
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.170 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:60)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = externalFilesDir:/storage/emulated/0/Android/data/com.fenjiread.learner/files/Movies
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2019-06-19 15:14:29.171 3699-3699/com.fenjiread.learner E/TestActivity:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ main, com.fenjiread.learner.activity.TestActivity$initListeners$1.onClick(TestActivity.kt:63)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ args[0] = >>>>>>>>>>>>>
│ args[1] = externalCacheDir:/storage/emulated/0/Android/data/com.fenjiread.learner/cache
总结一下:
安卓的缓存机制有5种:
1)网络缓存。依赖接口实现
2)SharePreferenced缓存,简单、方便、Key和Value对应存储,默认保存在手机内置内存中。
3)SqlLite数据库存储,原生实现比较复杂,需要熟练Sql语句。第三方GreenDao集成方便,升级需要手动的迁移之前的表。默认也是保存在手机内置内存的。
4)文件存储。这种方式区别与上面的两种方式是不是android framwork框架提供的存储方式,而是通过让存储数据实体实现Serializable接口来实现序列化到本地的文件。然后我们一般保存的路径一般都是带有External的Api,表示的是获取手机的外置内存。
比较常用的就是下面的两个缓存路径:
context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)=/storage/emulated/0/Android/data/com.fenjiread.learner/files/Movies
context.getExternalCacheDir()=/storage/emulated/0/Android/data/com.fenjiread.learner/cache`
5)对于什么情况下才会用到自定义的ContentProvider,官方文档的Dev Guide是这样描述的:
如果你想要提供以下的一种或几种特性的时候你才需要构造一个ContentProvider:
你想要为其它的应用提供复杂的数据或者文件;
你想允许用户从你的应用中拷贝复杂的数据到其它的应用中;
你想要使用搜索框架来提供自定义的搜索策略。
你完全不需要ContentProvider来调用一个SQLite数据库,如果这种调用完全在你自己的应用之中。
也就是说,ContentProvider的作用是为别的应用调用本应用中的数据或者文件提供接口,而它也是唯一的跨应用数据传递的接口。如果仅仅是同一个应用中的数据传递,则完全没有必要使用到自定义的ContentProvider。