Android数据存储和Android7适配

Android提供了几种选项用于应用数据的持久化,你可以根据自己的需要来选择合适的方式来进行数据存储,比如你的数据是否是私有的,你的数据需要多大的空间,以及你的数据格式是什么样的。目前的存储方式有以下几种:

  1. SharedPerferences
  2. 内部存储
  3. 外部存储
  4. SQLite数据库
  5. 网络连接

SharedPerferences

SP主要适用于读取K-V结构的。你还可以用PreferenceActivity来创建一个设置用户偏好的页面,它会自动帮你处理K-V,定制型较差。SP也可以设置读取的权限,包含privateprivate是只允许对本应用访问创建的文件,readable、writeable则是其他应用对创建的文件只有可读、写权限,一般很少用这个。主要位于应用包下的shared_prefs文件下

1
2
3
4
5
6
7
8
9
10
11
public void setDataWithSP(String data) {
SharedPreferences sp = getSharedPreferences(FILE_NAME, MODE_WORLD_READABLE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("name", data);
editor.commit();
}
public String getDataWithSP() {
SharedPreferences sp = getSharedPreferences(FILE_NAME, MODE_WORLD_READABLE);
return sp.getString("name", "jiangzijian");
}

用内部存储

内部存储是通过对文件的读写来实现的,默认是private的,即操作该文件时会直接覆盖,MODE_APPEND是追加,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE在API17后废弃掉了,同时在Android7.0时不能够通过文件名分享私有文件也不能通过file://来分享,否则会直接crash。如果App要分享私有文件给其他app,需要通过FLAG_GRANT_READ_URI_PERMISSIONFileProvider。主要位于应用包名下的files文件下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void setDataWithInStorage(String data) {
try {
FileOutputStream fos = openFileOutput(In_STORAGE_FILE_NAME, MODE_APPEND);
fos.write(data.getBytes());
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public String getDataWithInStorage() throws FileNotFoundException {
FileInputStream fis = openFileInput(In_STORAGE_FILE_NAME);
String s = "";
try {
byte[] bytes = new byte[1024];
int n = fis.read(bytes);
s = new String(bytes, 0 , n);
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return s;
}

存储缓存文件

可以通过getCacheDir()来获取一个内部的目录,主要用于存储一些临时的缓存文件。当设备的内部存储空间不够时,Android会优先删除该目录下的文件,然而我们不能依赖于系统去清理,应该自己来维护。该文件主要位于应用包名下的cache文件下

用外部存储

每个Android设备都支持一个共享的外部存储,这个外部存储可以是一个sd卡,也可以是内部的存储,保存在外部存储的是全局可读的,并且可以在插上usb移动到电脑上。

为了能够读取或写入外部存储的文件,需要获取 READ_EXTERNAL_STORAGE 或者WRITE_EXTERNAL_STORAGE权限

1
2
3
4
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>

在对外部存储的文件做任何操作前都需要先检查下存储媒介的状态是否是可写或者可读的,不然会出问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}

文件能够被分享给其他app

一般来说,用户通过app产生的一些新文件应当保存在设备的公共的地方,比如说用户产生的图片、音频等等。执行该操作时需要使用共享的公共目录,比如Music/、Pictures等,这些是系统推荐的,通过将您的文件保存到相应的媒体类型目录,系统的媒体扫描程序可以在系统中正确地归类您的文件(例如铃声在系统设置中显示为铃声而不是音乐)。也可以自己添加合适的type。

1
2
3
4
5
6
7
8
9
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}

保存应用的私有文件

如果要保存的文件不希望被其他应用访问,可以通过调用getExternalFilesDir来使用外部存储来进行私有存储,此方法也可以通过type来指定子目录的类型。已分配某个内部存储器分区用作外部存储的设备可能还提供了 SD 卡槽。在使用运行 Android 4.3 和更低版本的这类设备时,getExternalFilesDir() 方法将仅提供内部分区的访问权限,而您的应用无法读取或写入 SD 卡。不过,从 Android 4.4 开始,可通过调用 getExternalFilesDirs() 来同时访问两个位置,该方法将会返回包含各个位置条目的 File 数组。 数组中的第一个条目被视为外部主存储;除非该位置已满或不可用,否则应该使用该位置。 如果希望在支持 Android 4.3 和更低版本的同时访问两个可能的位置,请使用支持库中的静态方法 ContextCompat.getExternalFilesDirs()。 在 Android 4.3 和更低版本中,此方法也会返回一个 File 数组,但其中始终仅包含一个条目。

保存缓存文件

与内部存储类似,外部存储也包含了一个用于存储缓存文件的目录,可以通过getExternalCacheDir()来得到,也可以通过与前述 ContextCompat.getExternalFilesDirs() 相似,您也可以通过调用 ContextCompat.getExternalCacheDirs() 来访问辅助外部存储(如果可用)上的缓存目录。

Android7权限适配

在Android7中强制使用了StrictMode,带来的影响就是禁止向自己App外的应用公开file://的URI,否则将会报FileUriExposedException异常,该异常是在startActivity的时候做的,所以如果你的App的target api是低于24的话,你的App并不会崩溃,如果你启动的应用尝试用你传过去的URI向其他应用(包括自己的应用)公开的话,也是会Crash的。正确的做法是发送一项content://URI,并授予URI临时访问权限。系统为我们封装了FileProvider类来简化操作。

比如下面的这段代码如果在Android7上且,target api是24时就会Crash:

1
2
3
4
5
6
7
8
9
10
11
12
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
path += File.separator + "PIC_" + System.currentTimeMillis() + ".JPG";
File file = new File(path);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
Uri imageUri = Uri.fromFile(file);
Log.e("uri", imageUri.toString());
Intent intent = new Intent();
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI
startActivityForResult(intent, 1006);

FileProvider

解决方案是官方提供了一个FileProvider类来将文件的路径转换.具体的使用方法:

  1. 在manifest中添加provider
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.kevin.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/wm_file_path" />
    </provider>

为了防止manifest文件merge冲突,可以用自己实现的FileProvider。

  1. 在res目录下创建xml目录,然后创建一个路径文件(如wm_file_path.xml)
    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <paths>
    <external-path
    name="camera_photos"
    path="Pictures/waimai2" />
    </paths>ß
    </resources>

<path> 结点下可以包括