补充: | Android FileProvider详细解析和踩坑指南
---------------------------------------
参考: | Android中的文件读写操作
参考: | Android常见文件路径介绍
参考: | Android SD卡简单的文件读写操作
参考: | Android 文件的读取和写入
参考: | Linux中drwxr-xr-x.的意思和权限
参考: | Android studio中关于模拟器/data目录不能显示的解决办法
注:本文不严谨。
目录
1 权限
2 获取存储路径
2.1 获取应用内部存储区路径方法 - Internal
2.2 获取应用外部存储区路径方法 - External
2.2.1 区域A:使用Context获取package_name相关路径
2.2.2 区域B:使用Environment类获取外部路径
3 读写内外存储空间的文件
3.1 读写App内部存储空间的文件
3.1.1 读文件 - Internal
3.1.2 写文件 - Internal
3.2 读写App外部存储空间的文件
3.2.1 读文件 - External
3.2.2 写文件 - External
4 验证读写功能及权限
4.1 读写Internal文件
4.1.1 写入Internal文件
4.1.2 读出Internal文件
4.2 读写External包名区文件
4.2.1 写入External包名区文件
4.2.2 读出External包名区文件
4.3 读写External Storage自由区的文件
4.3.1 向External Storage自由区写入文件
4.3.2 从External Storage自由区读出文件
1 权限
<!--文件读写权限--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
2 获取存储路径
android可能有两个地方可以存储文件,相对于应用程序来说,可分为内部和外部两个位置,外部又分为私有和公共两个区域:
存储位置 | 解释 | 文件管理器 | 访问权限 | 应用卸载后 | 外应用访问 | 其他别称 |
Internal | 应用安装存储区 | 不可见 | 不需权限 | 文件丢失 | 不可访问 | |
External_Package | 外部私有包名区 | 可见 | 不需权限 | 文件丢失 | 不可访问 | 同下统称 |
External_Storage | 外部公共自由区 | 可见 | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE | 文件留存 | 可以访问 | 存储盘/SD卡/ |
Internal storage:
总是可用的
这里的文件默认只能被我们的app所访问。
当用户卸载app的时候,系统会把internal内该app相关的文件都清除干净。
Internal是我们在想确保不被用户与其他app所访问的最佳存储区域。External storage:
并不总是可用的,因为用户有时会通过USB存储模式挂载外部存储器,当取下挂载的这部分后,就无法对其进行访问了。
是大家都可以访问的,因此保存在这里的文件可能被其他程序访问
当用户卸载您的应用时,只有通过 getExternalFilesDir() 将您的应用的文件保存在目录中时,系统才会从此处删除您的应用的文件
External是在不需要严格的访问权限并且希望这些文件能够被其他app共享或是允许用户通过电脑访问时的最佳存储区域。
2.1 获取应用内部存储区路径方法 - Internal
获取应用内部存储区路径只能通过Context来获取,不需要额外敏感权限
// 位置【data/data/<package_name>/cache】
String pathCache = getCacheDir().getAbsolutePath();
// 位置【data/data/<package_name>/files】
String pathFile = getFilesDir().getAbsolutePath();
其真实目录为:
根目录\data\data\<package_name>\cache // getCacheDir().getAbsolutePath();方法获取到的
根目录\data\data\<package_name>\files // getFilesDir().getAbsolutePath();方法获取到的根目录\data\data\<package_name>\database // 数据库存储位置
根目录\data\data\<package_name>\shared_prefs // SharedPreferences存储目录
打开Android Studio右下角的 Device File Explorer 可以看到手机存储的目录结构
获取到的文件所在的存储路径是App内部存储(不是“外部存储空间”),因此对于用户通过手机的文件管理是看不到该目录的:
2.2 获取应用外部存储区路径方法 - External
获取应用外部存储区路径需要声明读取外部存储区的读写权限
<!--文件读写权限--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
获取应用外部存储区有两种途径:
①使用Context获取package_name相关路径; // 应用卸载后,文件被删除
②使用Environment类获取package_name无关路径。 // 应用卸载后,文件仍存在
2.2.1 区域A:使用Context获取package_name相关路径
获取Cache路径的方法:
// 真实位置【/mnt/sdcard/Android/data/<package_name>/cache】
// 模拟位置【/storage/emulated/0/Android/data/<package_name>/cache】
String pathExternalCache = getExternalCacheDir().getAbsolutePath();
获取到的目录为:
真实位置:根目录\mnt\sdcard\Android/data\<package_name>\cache
模拟位置:设备/storage/emulated/0/Android/data/<package_name>/cache
获取Files路径的方法:
// 真实位置【/mnt/sdcard/Android/data/<package_name>/files/Movies】
// 模拟位置【/storage/emulated/0/Android/data/<package_name>/files/Movies】
String pathExternalFiles = getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath();
获取到的目录:
真实位置:根目录\mnt\sdcard\Android/data\<package_name>\files\具体目录
模拟位置:设备/storage/emulated/0/Android/data/<package_name>/files/具体目录
getExternalFilesDir(String type);方法中可以传入Environment的type参数,可用于指定其具体目录。
此方法获取目录路径的文件目录如下,因位于App外部存储空间,是可以在手机自带的 “文件管理” 中看到的:
左图是在设备中的真实路径,右图为通过手机“文件管理”看到的目录。此途径应用卸载后存储在该目录下的文件会被清除掉
2.2.2 区域B:使用Environment类获取外部路径
方式①:获取公共目录
// 真实位置【/mnt/sdcard/Pictures】
// 模拟位置【/storage/emulated/0/Pictures】
String pathExternalPublic = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_XXX).getAbsolutePath();
真实位置:根目录\mnt\sdcard\具体公共目录
模拟位置:设备/storage/emulated/0/具体公共目录
其中,获取公共目录的Environment的Type参数有:
Environment的Type参数 | 对应模拟路径 | 解释说明 |
DIRECTORY_MOVIES | /storage/emulated/0/Movies | 电影 |
DIRECTORY_DCIM | /storage/emulated/0/DCIM | 相册 |
DIRECTORY_ALARMS | /storage/emulated/0/Alarms | 铃音 |
DIRECTORY_DOCUMENTS | /storage/emulated/0/Documents | 文件 |
DIRECTORY_DOWNLOADS | /storage/emulated/0/Download | 下载文件 |
DIRECTORY_MUSIC | /storage/emulated/0/Music | 音乐 |
DIRECTORY_NOTIFICATIONS | /storage/emulated/0/Notifications | 通知 |
DIRECTORY_PICTURES | /storage/emulated/0/Pictures | 图片 |
DIRECTORY_PODCASTS | /storage/emulated/0/Podcasts | 播客 |
DIRECTORY_RINGTONES | /storage/emulated/0/Ringtons | 铃声 |
方式②:获取自由目录
// 真实位置【/mnt/sdcard】
// 模拟位置【/storage/emulated/0】
String pathExternalRoot = Environment.getExternalStorageDirectory().getAbsolutePath();
// 真实位置【/mnt/sdcard/DCIM】
// 模拟位置【/storage/emulated/0/DCIM】
String pathExternalDcim = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM";
// 真实位置【/mnt/sdcard/DCIM/Camera/test.jpg】
// 模拟位置【/storage/emulated/0/DCIM/Camera/test.jpg】
String pathExternalTest = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/Camera/test.jpg";
目录:
Environment.getExternalStorageDirectory().getAbsolutePath();
真实位置:根目录\mnt\sdcard
模拟位置:设备/storage/emulated/0/Environment.getExternalStorageDirectory().getAbsolutePath() + "/xxx/xxx/xxx.txt";
真实位置:根目录\mnt\sdcard\具体目录
模拟位置:设备/storage/emulated/0/具体目录
看一下目录结构:
这也便是我们熟悉的 “文件管理” 界面(右图)展示的目录的真实结构(左图)
这个目录下为即为所谓的相对于App应用的“外部存储空间”。
为什么右图写的是“内部存储设备”呢?这是因为分类标准不同,因现在手机机身存储都集成在手机之内,相对于外部可插拔的SD卡而言,机身存储即为“内部存储设备”。但因本文讨论App的存储位置,因此本文不做特殊说明的话,均是指以相对于App而言的“内”和“外”。
除此之外,Environment类 还有其他的目录结构获取方法,如下:
3 读写内外存储空间的文件
3.1 读写App内部存储空间的文件
3.1.1 读文件 - Internal
代码
/**
* 读Internal中文件的方法
*
* @param filePathName 文件路径及文件名
* @return 读出的字符串
* @throws IOException
*/
public static String readInternal(String filePathName) throws IOException {
StringBuffer stringBuffer = new StringBuffer();
// 打开文件输入流
FileInputStream fileInputStream = new FileInputStream(filePathName);
byte[] buffer = new byte[1024];
int len = fileInputStream.read(buffer);
// 读取文件内容
while (len > 0) {
stringBuffer.append(new String(buffer, 0, len));
// 继续将数据放到buffer中
len = fileInputStream.read(buffer);
}
// 关闭输入流
fileInputStream.close();
return stringBuffer.toString();
}
3.1.2 写文件 - Internal
代码
/**
* 写Internal中文件的方法
*
* @param filePathName 文件路径及文件名
* @param content 要写进文件中的内容
* @throws IOException
*/
public static void writeInternal(String filePathName, String content) throws IOException {
// 打开文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(filePathName);
// 写数据到文件中
fileOutputStream.write(content.getBytes());
// 关闭输出流
fileOutputStream.close();
}
3.2 读写App外部存储空间的文件
3.2.1 读文件 - External
代码
/**
* 从External文件目录下读取文件
*
* @param filePathName 要读取的文件的路径+文件名
* @return
* @throws IOException
*/
public static String readExternal(String filePathName) throws IOException {
StringBuffer stringBuffer = new StringBuffer();
// 获取External的可用状态
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
// 当External的可用状态为可用时
// 打开文件输入流
FileInputStream fileInputStream = new FileInputStream(filePathName);
byte[] buffer = new byte[1024];
int len = fileInputStream.read(buffer);
// 读取文件内容
while (len > 0) {
stringBuffer.append(new String(buffer, 0, len));
// 继续把数据存放在buffer中
len = fileInputStream.read(buffer);
}
// 关闭输入流
fileInputStream.close();
}
return stringBuffer.toString();
}
3.2.2 写文件 - External
代码
/**
* 向External文件目录下写入文件
*
* @param filePathName 要写入的的文件的路径+文件名
* @param content 要写入的内容
* @throws IOException
*/
public static void writeExternal(String filePathName, String content) throws IOException {
// 获取External的可用状态
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
// 当External的可用状态为可用时
// 打开输出流
FileOutputStream fileOutputStream = new FileOutputStream(filePathName);
// 写入文件内容
fileOutputStream.write(content.getBytes());
// 关闭输出流
fileOutputStream.close();
}
}
4 验证读写功能及权限
首先选取一个高版本的测试机,众所周知,Android6.0增加了动态权限的申请步骤。本人用的MI2S为Android 5.0就先暂时放一边,换上一个Android8.0的Galaxy S7。本部分改变一下顺序,先写再读。
4.1 读写Internal文件
4.1.1 写入Internal文件
首先找一下 data/data/路径下没有“com.dandelion.abc”的包名
安装该测试应用后出现该包名,但cache目录下为空(下图左)。然后去应用设置中关闭本软件的读写权限,
之后执行写入命令,向cache/files目录下分别写入文件,然后查看文件路径(上图右)
String content = "Always believe that something wonderful is about to happen!";
// 获取文件在Internal中文件目录下的路径
String pathInternalCache = this.getCacheDir().getAbsolutePath() + File.separator + "InternalCacheTest.txt";
String pathInternalFiles = this.getFilesDir().getAbsolutePath() + File.separator + "InternalFilesTest.txt";
// 写入操作
try {
writeInternal(pathInternalCache, content);
writeInternal(pathInternalFiles, content);
} catch (IOException e) {
e.printStackTrace();
}
说明写入成功
4.1.2 读出Internal文件
现在把上一步写入的文件读出
// 获取文件在Internal中文件目录下的路径
String pathInternalCache = this.getCacheDir().getAbsolutePath() + File.separator + "InternalCacheTest.txt";
String pathInternalFiles = this.getFilesDir().getAbsolutePath() + File.separator + "InternalFilesTest.txt";
// 读出操作
String textReadInternalCache = null;
String textReadInternalFiles = null;
try {
textReadInternalCache = readInternal(pathInternalCache);
textReadInternalFiles = readInternal(pathInternalFiles);
} catch (IOException e) {
e.printStackTrace();
}
// 日志打印
log.d(TAG, pathInternalCache); // 路径,下同
log.d(TAG, textReadInternalCache); // 内容,下同
log.d(TAG, pathInternalFiles);
log.d(TAG, textReadInternalFiles);
输出结果:
说明读出成功,而此时,该应用并未开启外部存储读写权限,说明读写该目录下的文件不需要外部存储读写权限。
卸载该应用,该目录下的本应用包名及其文件均被删除。
4.2 读写External包名区文件
首先应该明确,该区域的存储位置不仅可以从Device File Explorer查看,还可以通过手机的文件管理查看
首先查找一下mnt/sdcard/Android/data/目录下的“com.dandeion.abc”的包名,发现无论从Android Studio的Device File Explorer还是手机的文件管理,均找不到“com.dandeion.abc”的包名,说明当前未向该区域存储文件
然后去应用设置中关闭本软件的外部存储读写权限。
4.2.1 写入External包名区文件
String content = "Always believe that something wonderful is about to happen!";
// 获取文件在External-Android/<package_name>/cache文件目录下的路径
String pathExternalCache = this.getExternalCacheDir().getAbsolutePath() + File.separator + "ExternalCacheTest.txt";
String pathExternalFiles = this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + File.separator + "ExternalFilesTest.txt";
// 写入操作
try {
writeExternal(pathExternalCache, content);
writeExternal(pathExternalFiles, content);
} catch (IOException e) {
e.printStackTrace();
}
查看目录(下图左:Device File Explorer;下图右:手机文件管理)
发现写入成功了,而此时该应用并未获取到外部读写权限。
为了说明这一点,继续做一点更绝的,打开AndroidManifest.xml文件,把该文件中的读写权限的声明一并删除,并在手机上卸载该应用后重新安装。
结果:
Internal应用区:data/data/<包名>/[cache|files] 目录写入成功!
External包名区:mnt/sdcard/Android/data/<包名>/[cache|files] 目录写入成功!
因此可以得出结论:
Internal应用区和External包名区的读写均不需要应用开发者声明和获取外部存储读写权限。该权限是指:
permission.READ_EXTERNAL_STORAGE
permission.WRITE_EXTERNAL_STORAGE
4.2.2 读出External包名区文件
读出
// 获取文件在External-Android/<package_name>/[cache|files]文件目录下的路径
String pathExternalCache = this.getExternalCacheDir().getAbsolutePath() + File.separator + "ExternalCacheTest.txt";
String pathExternalFiles = this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + File.separator + "ExternalFilesTest.txt";
// 读出操作
String textReadExternalCache = null;
String textReadExternalFiles = null;
try {
textReadExternalCache = readExternal(pathExternalCache);
textReadExternalFiles = readExternal(pathExternalFiles);
} catch (IOException e) {
e.printStackTrace();
}
// 日志输出
log.d(TAG, pathExternalCache);
log.d(TAG, textReadExternalCache);
log.d(TAG, pathExternalFiles);
log.d(TAG, textReadExternalFiles);
输出
说明读出成功,而此时,该应用并未开启外部存储读写权限,说明读写该目录下的文件不需要外部存储读写权限。
卸载该应用,该目录下的本应用包名及其文件均被删除。
4.3 读写External Storage自由区的文件
类似4.2,该区域的存储位置,可从Device File Explorer查看,还可以通过手机的文件管理查看
因实验打算向mnt/sdcard/Android/[ Ducument | Dandelion ] 目录下写入文件,因此先查看一下未写入时的目录情况,以留作比较
然后去应用设置中关闭本软件的外部存储读写权限。
4.3.1 向External Storage自由区写入文件
写入
String content = "Always believe that something wonderful is about to happen!";
// 获取文件在External自由区(/storage/emulated/0/具体目录)文件目录下的路径
String pathStorageRoot = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "StorageRootTest.txt";
String pathStoragePublic = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + File.separator + "StoragePublicTest.txt";
// 需要创建文件夹目录的路径
String dirStorageDandelion = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Dandelion";
String pathStorageDandelion = dirStorageDandelion + "/StorageDandelionTest.txt";
// 创建文件夹【注意:因本路径本身没有,需要单独创建,不创建就引用会报FileNotFoundException】
boolean dirExist = makeDirectory(dirStorageDandelion);
// 写入操作
try {
writeExternal(pathStorageRoot, content);
writeExternal(pathStorageDandelion, content);
if (dirExist) writeExternal(pathStorageDandelion, content); // 需要创建目录的路径
} catch (IOException e) {
e.printStackTrace();
}
执行完毕之后查看目录结构,发现与刚才的目录并无差异。是何原因?
未崩溃是因为所执行的步骤都在try--catch内部,自然不会崩溃报Error,所以找一下Warning吧~查找日志发现:
W/System.err: java.io.FileNotFoundException: /storage/emulated/0/StorageRootTest.txt (Permission denied)
Permission denied
外部存储读写权限的问题。所以,自此开始,就需要:
① 在AndroidManifest.xml文件中增添如下权限声明:
<!--文件读写权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
② 并在Java代码中,动态申请该敏感权限。动态申请部分此处略去一万字。
由此可知,读写该区域目录下的文件,是需要读写权限的。
添加“声明并动态申请权限”的代码后,继续操作,得出如下文件:
4.3.2 从External Storage自由区读出文件
读出操作
// 获取文件在External自由区(/storage/emulated/0/具体目录)文件目录下的路径
String pathStorageRoot = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "StorageRootTest.txt";
String pathStoragePublic = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + File.separator + "StoragePublicTest.txt";
String pathStorageDandelion = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Dandelion/StorageDandelionTest.txt";
// 读出操作
String textReadStorageRoot = null;
String textReadStoragePublic = null;
String textStorageDandelion = null;
try {
textReadStorageRoot = readExternal(pathStorageRoot);
textReadStoragePublic = readExternal(pathStoragePublic);
textStorageDandelion = readExternal(pathStorageDandelion);
} catch (IOException e) {
e.printStackTrace();
}
// 日志输出
log.d(TAG, pathStorageRoot);
log.d(TAG, textReadStorageRoot);
log.d(TAG, pathStoragePublic);
log.d(TAG, textReadStoragePublic);
log.d(TAG, pathStorageDandelion);
log.d(TAG, textStorageDandelion);
输出
说明读出成功,而此时,该应用必须开启外部存储读写权限,说明读写该目录下的文件必须获取外部存储读写权限。
卸载该应用,该目录下的本应用包名及其文件均未被自动删除,说明该目录下可长期存放某些文件。