直接说吧,Android数据持久化的形式最常用的有以下五种:
- 使用SharedPreferences存储数据
- 文件存储数据
- SQLite数据库存储数据
- 使用ContentProvider存储数据
- 网络存储数据
SharedPreferences
SharedPreferences是Android系统提供的一种轻量级的数据存取方式,数据存取是通过键值对的形式,存放到xml中。xml文件的存放路径为:/data/data/packageName/shared_prefs/目录
。
核心原理
SharedPreferences的本身实现就是分为两步,一步是内存,一部是磁盘,而主线程又依赖SharedPreferences的写入,所以可能当io成为瓶颈的时候,App会因为SharedPreferences变的卡顿,严重情况下会ANR,总结下来有以下几点:
- 存放在xml文件中的数据会被装在到内存中,所以获取数据很快
- apply是异步操作,提交数据到内存,并不会马上提交到磁盘
- commit是同步操作,会等待数据写入到磁盘,并返回结果
- 如果有同一个线程多次commit,则后面的要等待前面执行结束
- 如果多个线程对同一个sp并发commit,后面的所有任务会进入到QueuedWork中排队执行,且都要等第一个执行完毕
几宗罪
- 跨进程不安全。由于没有使用跨进程的锁,就算使用 MODE_MULTI_PROCESS,SharedPreferences 在跨进程频繁读写有可能导致数据全部丢失。根据线上统计,SharedPreferences 大约会有万分之一的损坏率。
- 加载缓慢。SharedPreferences 文件的加载使用了异步线程,而且加载线程并没有设置优先级,如果这个时候读取数据就需要等待文件加载线程的结束。这就导致主线程等待低优先线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50 ~ 100ms,并且建议大家提前用预加载启动过程用到的 SP 文件。
- 全量写入。无论是 commit() 还是 apply(),即使我们只改动其中一个条目,都会把整个内容全部写到文件。而且即使我们多次写同一个文件,SP 也没有将多次修改合并为一次,这也是性能差的重要原因之一。
- 卡顿。由于提供了异步落盘的 apply 机制,在崩溃或者其它一些异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 SharedPreferences 对象的数据落地到磁盘。如果没有落地完成,这时候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是ANR,从线上数据来看 SP 卡顿占比一般会超过 5%。
优化建议
- 不要存放大的 key 或 value 在 SharedPreferences 中,否则会一直存储在内存中(Map 容器中)得不到释放,内存使用过高会频繁引发 GC,导致界面丢帧甚至 ANR。
- 不相关的配置选项最好不要放在一起,单个文件越大加载时间越长。(参照 SharedPreferences 初始化时会开启异步线程读取对应文件,如果此时耗时较长,当对其进行相关数据操作时会导致线程等待)
- 读取频繁的 key 和 不频繁的 key 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)
- 不要每次都 edit 操作,每次 edit 都会创建新的 EditorImpl 对象,最好批量处理统一提交。否则每次 edit().commit() 都会创建新的 EditorImpl 对象并进行一次 I/O 操作,严重影响性能。
- commit 提交发生在 UI 线程,apply 提交发生在工作线程,对于数据的提交最好是批量操作统一提交。虽然 apply 任务发生在工作线程(不会因为 I/O 阻塞 UI 线程),但是如果添加过多任务也有可能带来其它”严重后果“(参照系统源码 ActivityThread - handlePauseActivity 方法实现)。
- 尽量不要存放 JSON 或 HTML 类型数据,这种可以直接文件存储。
- 最好能够提前初始化 SharedPreferences,避免 SharedPreferences 第一次创建时读取文件内容线程未结束而出现的等待情况,参照优化点第 2 条。
- 不要指望它能够跨进程通信:Context.MODE_MULTI_PROCESS。
文件存储数据
在很多情况下,您的应用会创建其他应用不需要访问或不应访问的文件。系统提供以下位置,用于存储此类应用专属文件:
- 内部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。系统会阻止其他应用访问这些位置,并且在 Android 10(API 级别 29)及更高版本中,系统会对这些位置进行加密。这些特征使得这些位置非常适合存储只有应用本身才能访问的敏感数据。
- getFilesDir(),
data/data/com.companyname.appname/files/
,用于持久文件 - getCacheDir(),
data/data/com.companyname.appname/cache/
,用于暂存文件
- getFilesDir(),
- 外部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。虽然其他应用可以在具有适当权限的情况下访问这些目录,但存储在这些目录中的文件仅供您的应用使用。如果您明确打算创建其他应用能够访问的文件,您的应用应改为将这些文件存储在外部存储空间的共享存储空间部分。
- getExternalFilesDir(),
/mnt/sdcard/Android/data/com.companyname.appname/files/
,用于持久文件文件 - getExternalCacheDir(),
/mnt/sdcard/Android/data/com.companyname.appname/cache/
,用于暂存文件
- getExternalFilesDir(),
以上方式都是针对该应用,一旦应用卸载,这些文件也就不复存在了,如果想要长久保存,可存储到外置SD卡其他地方,参见共享存储空间。
Java常规读写
/**
* 从SD卡中读取文件
*/
public static String readFromSD(String filePath) {
String content = "";
ByteArrayOutputStream ops = null;
FileInputStream fis = null;
try {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File file = new File(filePath);
if (file.exists()) {
fis = new FileInputStream(file);
ops = new ByteArrayOutputStream();
int len = 0;
byte[] data = new byte[1024];
while ((len = fis.read(data)) != -1) {
ops.write(data, 0, len);
}
content = new String(ops.toByteArray());
}
}
} catch (Exception e) {
LoggerUtil.e(e.toString());
} finally {
if (ops != null) {
try {
ops.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content;
}
/**
* 写文件到SD卡
*
* @param append 是否追加写入
*/
public static boolean writeFile(String filePath, String content, boolean append) {
boolean flag = false;
FileOutputStream fos = null;
try {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File file = new File(Environment.getExternalStorageDirectory(), filePath);
if (!file.exists()) {
createNewFile(file);
}
fos = new FileOutputStream(file, append);
fos.write(content.getBytes());
flag = true;
}
} catch (Exception e) {
LoggerUtil.e(e.toString());
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return flag;
}
Android私有文件读写
Context提供了两个方法来打开数据文件里的文件IO流:
- openFileInput(String name),读文件
- openFileOutput(String name, int mode),写文件
其中openFileOutput的第二个参数指定打开文件的模式:
- MODE_PRIVATE,默认操作模式 ,当指定同样文件名的时候会覆盖原有文件内容。
- MODE_APPEND,如果文件不存在就创建文件,如果存在就往后面追加。
- MODE_WORLD_READABLE,允许其他程序对我们的文件进行修改,过于危险 Android4.2以后已被废除。
- MODE_WORLD_WRITEABLE,允许其他程序对我们的文件进行修改,过于危险 Android4.2以后已被废除。
public String read() {
try {
FileInputStream inStream = this.openFileInput("message.txt");
byte[] buffer = new byte[1024];
int hasRead = 0;
StringBuilder sb = new StringBuilder();
while ((hasRead = inStream.read(buffer)) != -1) {
sb.append(new String(buffer, 0, hasRead));
}
inStream.close();
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public void write(String msg){
// 步骤1:获取输入值
if(msg == null) return;
try {
// 步骤2:创建一个FileOutputStream对象,MODE_APPEND追加模式
FileOutputStream fos = openFileOutput("message.txt",
MODE_APPEND);
// 步骤3:将获取过来的值放入文件
fos.write(msg.getBytes());
// 步骤4:关闭数据流
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Android Q 沙盒模式
为了让用户更好地管理自己的文件并减少混乱,以 Android 10(Android Q,API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储(沙盒模式))。启用分区存储后,应用将无法访问属于其他应用的应用专属目录。另外,AndroidQ中不支持file://类型访问文件,只能通过uri方式访问。
SQLite数据库存储数据
SQLite是轻量级嵌入式数据库引擎,它支持 SQL 语言,并且只利用很少的内存就有很好的性能。现在的主流移动设备像Android、iPhone等都使用SQLite作为复杂数据的存储引擎,在我们为移动设备开发应用程序时,也许就要使用到SQLite来存储我们大量的数据,所以我们就需要掌握移动设备上的SQLite开发技巧。详细使用可参阅我之前总结的一篇文章《Android开发之SQLite使用详解》
相关ORM框架:
- greendao,已经停更
- room,官方jitpack推荐
ContentProvider存储数据
ContentProvider内容提供器,主要用于在不同应用程序之间实现数据的共享功能。
举例来说,我们开发一个应用程序,我们不可能只使用自己的数据,也会用到其他应用的数据,像手机中的通讯录联系人,图片,音乐等是使用到最多的。我们使用的SharedPreferences,文件存储以及数据库SQLite都是从存储的应用内部的数据,实现不同应用间的数据共享就要使用到ContentProvider。
ContentProvider使用方法有两种:
- 使用现有的内容提供器来读取和操作相应程序中的数据
- 创建自己的内容提供器给我们的应用提供外部访问接口
详细使用可参阅我之前总结的一篇文章《Android开发ContentProvider学习总结》
网络存储数据
通过网络提供的存储空间来存储、获取数据信息。这块不用细说了。
最后
我是i猩人,转载注明出处,喜欢本篇文章的童鞋欢迎点赞、关注哦。