Android 数据存储的方式有很多种,其中文件存储就是我们经常会使用的一种方式,在了解文件存储之前,我们先来了解一下我们手机的存储空间。
手机的存储空间可以分为三个部分:系统分区、程序分区、公共存储空间。
系统分区:就是手机操作系统所占用的分区,是内存空间目录下的 system 目录,其中系统自带的 apk 就在 system/app 目录下,需要注意的是,手机普通用户对于 system 目录是没有写的权限的。
程序分区:就是我们自己安装程序所占的分区,是内存空间目录下的 data 目录,安装的 apk 在 data/app 目录下,而 应用所产生的私有数据在 data/data 目录下,需要注意的是,系统应用所产生的私有数据也在该目录下。
公共存储空间:就是我们用来存储一些公共的资源的时候所占的分区,是内存空间目录下的 storage/sdcard 目录下。
一、项目内文件
比如当我们需要使用一些数据的时候,我们可以选择直接读写文件,也可以在项目里面使用文件,在项目里面使用文件有两种方式,一个是 res/raw 目录,一个是 main/assets 目录,这两种方式都会在打包时被放到 apk 中。如果这两个目录不存在的话都可以新建,raw 目录直接右键 res 新建一个文件就行,而 assets 这个方法不太好创建,我们右键 app,新建一个 Folder,选择 Assets Folder:
1. 读取 raw 目录下的文件资源,我们先在目录下创建一个文件资源 word.txt 写入一些字符:
private String getByRaw(){
// 获取输入流
InputStream inputStream = this.getResources().openRawResource(R.raw.word);
// 使用 reader 一行一行读取,并添加到 StringBuffer 中拼接出来
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuffer buffer = new StringBuffer();
String s = null;
try {
while ((s = reader.readLine())!=null ){
buffer.append(s);
}
} catch (IOException e) {
e.printStackTrace();
}
// 打印出来
Log.i("raw:",buffer.toString());
return buffer.toString();
}
结果:
需要注意的是,在 raw 目录下,不论后缀名是否相似,都不能有相同的文件名,要不然会出错,也不能在 raw 目录下继续创建文件夹。
2. 读取assets 目录下文件资源,和 raw 不同的是,assets 文件目录下是可以新建二级目录的,比如我们在 assets 目录下新建一个 word 目录,然后再新建一个 word.txt 文件:
private String getByAssets(){
StringBuffer buffer = new StringBuffer();
// 获取输入流
try {
// 默认文件目录为 assets 根目录
InputStream inputStream = getAssets().open("word/word.txt");
// 使用 reader 一行一行读取,并添加到 StringBuffer 中拼接出来
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
while ((s = reader.readLine())!=null ){
buffer.append(s);
}
} catch (IOException e) {
e.printStackTrace();
}
Log.i("Assets:",buffer.toString());
return buffer.toString();
}
结果:
二、私有存储空间
而我们应用的私有数据是在 data/data 目录下的,一般会使用应用程序包名里面的两个文件夹,files 和 cache,看名字大家也能够知道知道这两个文件拿来干嘛的,files 用来读写数据的,cache 用来读写缓存的。
1. files 文件操作
private void saveByFiles(){
try {
// 获取输出流,传入文件名和模式
OutputStream outputStream = this.openFileOutput("word",MODE_PRIVATE);
String s = "Hello World!";
byte[] bytes = s.getBytes("utf-8");
// 写入
outputStream.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
上面我们可以看到,创建输出流的时候会给文件创建模式,这个模式又是什么呢?这个模式有四种:
模式名 | 说明 |
MODE_PRIVATE | 默认写法,代表私有,而且每次写入文件会覆盖原来的内容 |
MODE_APPEND | 每次写文件不会覆盖原来的内容,而是从最后继续写 |
MODE_READABLE | 使文件能被别的应用读取 |
MODE_WRITEABLE | 使文件能被别的应用写入 |
我们说了,这个 data/data 文件目录下都是一些应用的私有数据,为什么还会有模式来允许别的应用来读写, 这个有时候是为了同一公司或个人出品的应用进行数据共享,要使用这个模式来读写首先需要你的应用的签名是一致的,而且在 Manifest 文件中定义的 shareId 也要一致。虽然这种方法能进行进程间文件资源共享,但是是不鼓励这么做的,我们可以通过其它方式进行进程间文件资源共享,比如说数据库、网络、ContentProvider 等。
我们来打开 files 目录,会看到有一个 word 的文件,打开文件:
我们会看到我们写入的内容,那我们怎么读取呢?
private String getByFile(){
StringBuffer buffer = new StringBuffer();
try {
// 通过文件名读取
InputStream inputStream = this.openFileInput("word");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
while((s = reader.readLine())!=null){
buffer.append(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
Log.i("Files:",buffer.toString());
return buffer.toString();
}
这样就可以读取私有文件 file 里的文件内容了。
2. cache 文件操作
private void saveByCache(){
// 获取 cache 文件
File cacheFile = this.getCacheDir();
// 新文件,文件路径为 cache 文件夹的绝对路径里面的 wordCache 文件
File file = new File(cacheFile.getAbsolutePath() + "/wordCache");
try {
FileOutputStream outputStream = new FileOutputStream(file);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.write("Hello Cache");
writer.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
然后我们打开 data/data 里面项目包名里的 cache 文件目录,发现里面有一个 wordCache 文件,打开该文件:
然后我们来获取文件内容:
private String getByCache(){
File cacheFile = this.getCacheDir();
File file = new File(cacheFile.getAbsolutePath() + "/wordCache");
StringBuffer buffer = new StringBuffer();
try {
FileInputStream inputStream = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
while ((s = reader.readLine())!=null){
buffer.append(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
Log.i("Cache:",buffer.toString());
return buffer.toString();
}
结果:
这就是私有缓存空间的一些文件操作,需要注意的是私有缓存会在内存不足时自动清除,有时候会在我们不知道的情况下清除一些数据,如果我们想要避免这种操作,就可以使用规定缓存大小的缓存,大家可以使用 DiskLruCache,这里大家可以看看我的这一篇博客——Android 缓存策略,这里有面有对几种缓存的一个分享。
三、公共存储空间
1. 拓展空间
上面我们说到,公共存储空间就是 storage/sdcard 目录下的空间。其实,在 sdcard 目录下,有一个 android/data 目录,可以看到下面也是一些项目包名,包名目录下也有 file 和 cache 目录,其实这个是应用程序的扩展空间,因为应用程序的私有空间是有限的,相对来说手机的存储空间是很大的,我们就可以把一些相对来说不是那么敏感的数据放到拓展空间中。
而且使用拓展空间,不会产生垃圾文件,当应用卸载或者清除缓存和数据时,除了私有空间的 file 和 cache 会被清除以外,拓展程序的 file 和 cache 也会被清除。其次,放在拓展空间的程序文件不会被媒体扫描器扫描,有一定隐私性。而且,Google 也在 API 19 时,使用拓展空间是不需要在 manifest 中声明内存权限的。注意:使用拓展存储空间也是会被自动清理的,所以也可以使用 DiskLruCache 等。
private void getByExternalFile(){
// 使用拓展文件,需要传入 String 类型的参数
// null 代表就是 file 目录下
File file1 = this.getExternalFilesDir(null);
// "word" 代表是 file/word/ 目录
File file2 = this.getExternalFilesDir("word");
// 使用静态常量,代表 file/Music/ 目录,有很多的常量,大家可以去 Environment 源码中看看
File file3 = this.getExternalFilesDir(Environment.DIRECTORY_MUSIC);
// 下面的操作和上面是一样的
}
private void getByExternalCache(){
File file = this.getExternalCacheDir();
}
2. 公共空间
使用公共空间时的操作方式是和 Java 中的 IO 操作一样的,和上面的文件操作也是类似的,就不做详细说明了:
// 获取公共存储空间的目录
File sFile = Environment.getExternalStorageDirectory();
我们怎么在公共空间来隐藏自己的文件呢?1. 将文件名改为 .xxx,2. 将文件根目录添加 .nomedia 文件,这样文件扫描的时候就会把这些文件夹作为隐藏文件而不扫描。
注意:这里就会有一个问题,当用户手机存储只有通过 sd 卡 存储时,即当 sd 卡拔出时,你应用所有使用的资源就全为空了,这个时候应用程序就会出现异常或者影响使用效果,我们就需要判断 sd 卡是否可用来告诉用户。
private boolean isSdCardUsable(){
String state = Environment.getExternalStorageState();
if (state == Environment.MEDIA_MOUNTED){
return true;
}else{
return false;
}
}
我们也可以使用广播来通知 SD 卡拔出或者插入,这里大家就可以看一下广播的内容。