Android 文件存储路径探究

目录

  1. Internal Storage
  2. External Storage
  3. 路径示意图
  4. 解惑一个路径问题
  5. 参考

Android有两种文件存储空间:内部存储(internal storage)和外部存储(external storage),这主要是因为早期Android设备存储主要由内置的非易失性内存和可插拔的存储(比如micro SD卡)两部分组成,而随着现在设备提供商把存储都内置到手机中,已经没有外部存储一说了,但系统级别还是保持这两种存储空间。

Internal Storage

Internal storage总是可用的,默认只允许你的App访问,当用户删除App时,系统会自动把internal storage数据一并清除。App安装时默认安装在internal storage,但是可以在manifest文件中指定属性android:installLocation进行改变。从技术上来讲如果你在创建内部存储文件的时候将文件属性设置成可读,其他App能够访问自己应用的数据,前提是他知道你这个应用的包名,如果一个文件的属性是私有(private),那么即使知道包名其他应用也无法访问。Internal storage大小相对比较宝贵,其大小由设备提供商根据官方推荐的最小值进行设定,可以从官方提供的compatibility document 7.6节查看,但并没有规定单个App最大可以使用多少。Internal storage是系统本身和系统应用程序主要的数据存储所在地,一旦内部存储空间耗尽,手机也就无法使用,要尽量避免使用。SharedPreferencesSQLite数据库以及webview一些缓存数据等都储存在internal storage中。一般用Context来获取和操作。internal Storage读写是不需要权限声明的,应用本身有读写internal storage目录的权限。系统提供两个api获取internal storage的对应File对象:getFilesDir()getCacheDir(),注意系统处于low storage时会删除后者的应用数据,但没有warning。根据官方文档,Android提供两个快捷方法读写internal storage文件:

1
2
3
4
5
6
7
8
9
10
11
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;

try {
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}

1
2
3
4
5
6
7
8
9
10
public File getTempFile(Context context, String url) {
File file;
try {
String fileName = Uri.parse(url).getLastPathSegment();
file = File.createTempFile(fileName, null, context.getCacheDir());
catch (IOException e) {
// Error while creating file
}
return file;
}

External Storage

通常来说,手机连接电脑,能被电脑识别的部分一定是external storage,在Mac下可以安装官方提供的工具Android File Transfer查看。读写external storage需要声明以下两个权限:

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

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

官方文档对此有如下解释:App必须获得WRITE_EXTERNAL_STORAGE权限才能写external storage,声明该权限默认会认为App也获得READ_EXTERNAL_STORAGE权限;当前App不需要专门指定READ_EXTERNAL_STORAGE就可以读external storage,但是为了兼容性,建议最好显示声明该权限。读写external storage至少要保证处于可用的状态,系统提供如下方法:

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;
}

External storage有两种类型文件(目录):public filesprivate files。前者可以存储各个App都可以访问,且用户删除应用后保留的文件,比如说照相的相片;后者数据理论上只属于你的App去使用,而且uninstall App后也会随着被删除,比如说App下载的一些临时媒体文件。系统分别提供两个方法,使用Demo如下:

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;
}

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

注意:1. 尽量使用系统规定的文件类型(DIRECTORY_PICTURES)命名文件夹,可以方便系统(比如media scanner)识别文件类型;2. api 8以下的版本在操作文件的时候没有专门为以上操作提供api支持,只能使用Environment.getExternalStorageDirectory(),不带参数,也就不能自己创建一个目录,只是返回外部存储的根路径,然后自行操作。

路径示意图

注意:1. Context.getExternalCacheDir()当空间不足时不会实时被删除,可能返回空对象;2. Context.getExternalFilesDir()从Android4.4之后,App不需要声明读写权限就可访问,并且MediaStore不会扫描该路径。File类提供两个API用于获取系统存储空间大小,分别是:getFreeSpace()getTotalSpace();因为在存储空间90%被占用时,即使所存文件小于剩余空闲空间也无法保证正确执行,因为存储过程中有可能产生大量的中间文件,所以官方文件建议没必要检测剩余空间,只需要捕获IOException并处理即可。

解惑一个路径问题

用代码查看各个路径名时发现输出都有个0的文件层级,而使用adb shell却看不到该路径层级(这一部分内容参照网上相关Blog,并没有查看全部相关源码)。简单来说,Android4.2之后,Google搞出了多用户的概念,0可以理解为主用户,然后依次是10、11等,如图所示,/data/media是内置SD卡的数据存储位置,使用FUSE技术将其虚拟成/dev/fuse设备,被同时挂载在/mnt特定目录下。

以我的手机Nexus 5简单查看SD卡Mount的设置。首先看一下init.rc文件,其中import /init.${ro.hardware}.rc;然后我们在/dev/socket输入getprop看到[ro.boot.hardware]: [hammerhead],实际上源码中ro.hardware最后使用的是ro.boot.hardwawre值。查看init.hammerhead.rc文件,如下:

创建两个存储目录,其中,/mnt/shell/emulated是实际挂载sdcard的目录,/storage/emulated是每个用户为他们自己挂载sdcard的目录。SD卡实际挂载到的是/mnt/shell/emulated/0, 其他的/storage/emulated/legacy//sdcard//mnt/sdcard//storage/sdcard0/都是它的链接。Linux 2.6之后引入了一个新的技术unshare,它可以使某个应用程序创建的挂载点等不共享,只有自己可见。dalvik_system_Zygote.cpp中的mountEmulatedStorage()函数会在Zygote创建一个新的java进程时运行,先应用unshare,然后再以MS_BIND参数把/mnt/shell/emulated/0再挂载到/storage/emulated/0下;其中0是用户ID,新建用户往后排,所以Environment得到external storage路径会有用户ID这个层级。adb shell不是通过 Zygote来创建的,没有此挂载点,所以不能访问/storage/emulated/0

参考

  1. http://developer.android.com/training/basics/data-storage/files.html
  2. https://source.android.com/devices/storage/index.html
  3. http://developer.android.com/guide/topics/data/data-storage.html#filesInternal
  4. http://www.code06.com/mobile/xuhui_7810/78048.html
  5. http://zzqhost.com/?post=15