Android开罐头———外部存储与内部存储完全解析

context.getExternalFilesDir(),Environment.getExternalStorageDirectory(),傻傻分不清?到底什么算安卓手机的external storage?

image.png

一、存储概述

Android提供了几种保存application持久化数据的选择。而开发者根据

  • 数据是否为app私有
  • 数据是否可以暴露给其他app
  • 数据大小情况

来选择不同的方式存储数据。

1.1 存储方式

安卓提供了如下存储的方式选择:

类别 作用
Shared Preferences 以键值对形式在xml中存储配置
Internal Storage (内部存储) 设备中的私有数据
External Storage (外部存储) 共享的外部存储中存公有数据
SQLite Databases 私有数据库存储格式化的数据
Network Connection 在开发者个人网络服务器端存储数据

1.2 content provider

安卓提供了一种甚至将app的私有数据提供给其他app的途径————content provider。而android官方guide中不把它归为Strage Options中的一种。

二、Internal Storage 内部存储

2.1 内部存储定义

You can save files directly on the device's internal storage. By default, files saved to the internal storage are private to your application and other applications cannot access them (nor can the user). When the user uninstalls your application, these files are removed.

白话文版本:
注意内部存储不是内存。内部存储位于系统中很特殊的一个位置(类似沙盒),如果你想将文件存储于内部存储中,那么文件默认只能被你的应用访问到,且一个应用所创建的所有文件都在和应用包名相同的目录下。
也就是说应用创建于内部存储的文件,与这个应用是关联起来的。当一个应用卸载之后,内部存储中的这些文件也被删除。 内部存储空间十分有限,因而显得可贵,另外,它也是系统本身和系统应用程序主要的数据存储所在地,一旦内部存储空间耗尽,手机也就无法使用了。所以对于内部存储空间,我们要尽量避免使用。Shared Preferences和SQLite数据库都是存储在内部存储空间上的。内部存储一般用Context来获取和操作。

2.2 创建并写入内部存储

  1. Call openFileOutput() with the name of the file and the operating mode. This returns a FileOutputStream.
  1. Write to the file with write().
  2. Close the stream with close().

安卓还为我们提供了一个简便方法Context类的 openFileOutput()来读写应用在内部存储空间上的文件,下面是一个向文件中写入文本的例子:

String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();

读取同理,把write()换成read()罢了。

**openFileOutput()或者Context.getFileDir()通过Android Device Monitor工具可以找到此目录,为/data/data/com.xxx.xxx/files/。 **


拓展优化:
使用java流,借助FileOutputStream构建出一个OutputStreamWriter对象,接着再用BufferedWriter对象封装:

FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos));
writer.write(string);

2.3 内部存储其他操作

  • 列出所有的已创建的文件,这个可能不容易想到,Context居然有这样的方法。
String[] files = Context.fileList();
for(String file : files) {
Log.e(TAG, "file is "+ file);
}
  • 删除文件,能创建就要能够删除,当然也会提供了删除文件的接口,它也非常简单,只需要提供文件名
if(Context.deleteFile(filename)) {
Log.e(TAG, "delete file "+ filename + " sucessfully“);
} else {
Log.e(TAG, "failed to deletefile " + filename);
}
  • 创建一个目录,需要传入目录名称,它返回一个文件对象用到操作路径
File workDir = Context.getDir(dirName, Context.MODE_PRIVATE);
Log.e(TAG, "workdir "+ workDir.getAbsolutePath();

2.4 内部存储缓存cache

你应该使用getCacheDir()来内部绝对缓存目录File,这些缓存将会是在存储不够时第一个被清除的,但是不会保证这些文件一定会被清除。通过Android Device Monitor工具可以找到此目录,为/data/data/com.xxx.xxx/cache/

Note:你不应该指望着系统来给你清理这些cache文件,你应该自己设定一个合理的最大上限,比如1MB。当用户卸载app时,这些文件同时被卸载。

2.5 内部存储总结

  1. 文件操作只需要向函数提供文件名,所以程序自己只需要维护文件名即可;
  2. 不用自己去创建文件对象和输入、输出流,提供文件名就可以返回File对象或输入输出流
  3. 对于路径操作返回的都是文件对象。
  4. Context类调用

三、External Storage 外部存储

3.1 外部存储定义

Every Android-compatible device supports a shared "external storage" that you can use to save files. This can be a removable storage media (such as an SD card) or an internal (non-removable) storage. Files saved to the external storage are world-readable and can be modified by the user when they enable USB mass storage to transfer files on a computer.

另外,关于外部存储,我觉得api中在介绍Environment.getExternalStorageDirectory()方法的时候说得很清楚:

Note: don't be confused by the word "external" here. This directory can better be thought as media/shared storage. It is a filesystem that can hold a relatively large amount of data and that is shared across all applications (does not enforce permissions). Traditionally this is an SD card, but it may also be implemented as built-in storage in a device that is distinct from the protected internal storage and can be mounted as a filesystem on a computer.

白话文版本:
最容易混淆的是外部存储,如果说pc上也要区分出外部存储和内部存储的话,那么自带的硬盘算是内部存储,U盘或者移动硬盘算是外部存储,因此我们很容易带着这样的理解去看待安卓手机,认为机身固有存储是内部存储,而扩展的TF卡是外部存储。比如我们任务16GB版本的Nexus 4有16G的内部存储,普通消费者可以这样理解,但是安卓的编程中不能,这16GB仍然是外部存储。
所有的安卓设备都有外部存储和内部存储,这两个名称来源于安卓的早期设备,那个时候的设备内部存储确实是固定的,而外部存储确实是可以像U盘一样移动的。但是在后来的设备中,很多中高端机器都将自己的机身存储扩展到了8G以上,他们将存储在概念上分成了"内部internal" 和"外部external" 两部分,但其实都在手机内部。所以不管安卓手机是否有可移动的sdcard,他们总是有外部存储和内部存储。最关键的是,我们都是通过相同的api来访问可移动的sdcard或者手机自带的存储(外部存储)

3.2 获取权限

manifest中必须添加:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

Android 6.0之后新加入了运行时权限,这里不作详解,可参考官方文档:

官方文档:https://developer.android.com/about/versions/marshmallow/android-6.0.html

3.3 检查状态

在使用外部存储之前,你必须要使用getExternalStorageState()方法,先检查外部存储的当前状态,以判断是否可用:

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

3.4 外部存储写入文件

获取了权限,检查了状态之后,终于可以写入了,而又有两种类型的文件:

  • 外部存储共有文件
  • 外部存储私有文件

3.4.1 外部存储共有文件

Saving files that can be shared with other apps

公共文件Public files:文件是可以被自由访问,目录任意,且文件的数据对其他应用或者用户来说都是由意义的,当应用被卸载之后,其卸载前创建的文件仍然保留。比如camera应用,生成的照片大家都能访问,而且camera不在了,照片仍然在。
举个栗子,下面的方法在公共的图片目录下创建了一个新的图片相册目录:

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

注意,如果你的api 版本低于8,那么不能使用getExternalStoragePublicDirectory(),而是使用Environment.getExternalStorageDirectory(),他不带参数,也就不能自己创建一个目录,只是返回外部存储的根路径。


躲避媒体扫描
在你的外部目录下放一个名为.nomedia的空文件,则会阻止媒体扫描器通过MediaStore content provider来读取你的媒体文件(比如相册中就不会显示)。但是更好的办法是用下面👇的外部存储私人模式。

3.4.2 外部存储私有文件

If you are handling files that are not intended for other apps to use (such as graphic textures or sound effects used by only your app), you should use a private storage directory on the external storage by calling getExternalFilesDir().

其实由于是外部存储的原因即是是这种类型的文件也能被其他程序访问,只不过一个应用私有的文件对其他应用其实是没有访问价值的(恶意程序除外)。外部存储上,应用私有文件的价值在于卸载之后,这些文件也会被删除。类似于内部存储。
所有应用程序的外部存储的私有文件都放在根目录的Android/data/下,目录形式为/Android/data/<package_name>/

Android 4.3及其以下使用getExternalFilesDir()方法可能只会读取手机内部空间,而非SD卡。
使用ContextCompat.getExternalFilesDirs(),或者4.4版本以上使用getExternalFilesDirs()会返回一个File数组,第一个是默认的外部存储。

创建应用私有文件的方法是Context.getExternalFilesDir(),如下:

public File getAlbumStorageDir(Context context, String albumName) {
// Get the directory for the app's private pictures directory.
File file = newFile(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if(!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
returnfile;
}

上面的代码创建了一个picture目录,并在这个目录下创建了一个名为albumName的文件,Environment.DIRECTORY_PICTURES其实就是字符串picture。

如果你的api 版本低于8,那么不能使用getExternalFilesDir(),而是使用Environment.getExternalStorageDirectory()获得根路径之后,自己再想办法操作/Android/data/<package_name>/下的文件。


注意,媒体扫描器也不能通过MediaStore content provider扫描内部存储私有文件,所以你不应在这里存储照片、音乐。但是别的应用可以读取到他们。

3.5 外部存储缓存

To open a File that represents the external storage directory where you should save cache files, call getExternalCacheDir(). If the user uninstalls your application, these files will be automatically deleted.

调用context的getExternalCacheDir()方法获取外部存储的私有缓存文件夹。

四、各种目录打印

最后为了弄清楚getFilesDirgetExternalFilesDirgetExternalStorageDirectorygetExternalStoragePublicDirectory等android文件操作方法,我将这些方法的执行结果打印出来,看看到底路径是啥样,在activity中执行以下代码:

Log.i("codecraeer", "getFilesDir = " + getFilesDir());
Log.i("codecraeer", "getExternalFilesDir = " + getExternalFilesDir("exter_test").getAbsolutePath());
Log.i("codecraeer", "getDownloadCacheDirectory = " + Environment.getDownloadCacheDirectory().getAbsolutePath());
Log.i("codecraeer", "getDataDirectory = " + Environment.getDataDirectory().getAbsolutePath());
Log.i("codecraeer", "getExternalStorageDirectory = " + Environment.getExternalStorageDirectory().getAbsolutePath());
Log.i("codecraeer", "getExternalStoragePublicDirectory = " + Environment.getExternalStoragePublicDirectory("pub_test"));

在log中看到如下结果:


image2.jpg

五、总结

一张图总结,我个人的理解是存储按照不同的角度分为两种:

image.png
  • 按照内外部:内部存储,外部存储
    • 区别:带External字眼则一定是外部存储的方法,如getExternalFilesDir(),外部存储需要运行时权限
  • 按照共有私有性质:公共文件,私有文件
    • 区别:公共文件是Environment调用函数,而私有文件(包括内部私有与外部私有)是Context调用函数,公共文件不会随着app卸载而删除,私有则会。私有文件不会被Media Scanner扫描到。

引用

android中的文件操作详解以及内部存储和外部存储

http://developer.android.com/training/basics/data-storage/files.html#InternalVsExternalStorage

http://developer.android.com/guide/topics/data/data-storage.html

https://developer.android.com/about/versions/marshmallow/android-6.0.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容