我们在做各种程序,App稍微复杂点,都难免要进行读写文件。windows还好,虽然也有各种权限和安全机制,但就读写文件来说,还好,管理员权限的话,几乎可以读写任何文件了。。。废话少说,还是来说正题。
首先,我在做一款app的时候,就需要进行读写文件,在没有进行系统测试整理之前,总是一头雾水。 主要有两点:
1 各种getXXX函数返回的路径到底是啥?
2 各种存储位置都需要怎么样才能有读写权限?
关于问题1,已经有大神总结的很好了
不过对上述博文提出内部存储很珍贵,尽量使用外部存储,我现在持反对意见,本人是windows开发,由于工作需要也得做安卓开发,所以对系统级的东西了解不多,但在我看来内部存储和外部存储,都是处于同一个分区的,只是路径不一样。 根据是我计算内部存储和外部存储的总空间和剩余空间,一模一样。(小米8)
当然可能早期版本的安卓可能确实不一样吧,过老的手机,至少我是不考虑的。
现在通过评测 来说明问题2,我仅以评测结果来说明问题,如果不小心有Android linux大神路过,请批评指正。
既然是评测就尽量全面点,但实际上很多路径,普通App是不会涉及到的
我们先来定义两个简单的函数,用来验证读写是否成功:
private boolean readFile(String fileDir, String fileName)
{
String strFullPath = fileDir + "/" + fileName;
StringBuffer strBuffer = new StringBuffer();
byte[] buffer = new byte[1024];
try {
FileInputStream fi = new FileInputStream(strFullPath);
while (true)
{
int len = fi.read(buffer);
if(len>0) {
strBuffer.append(new String(buffer, 0, len));
}
else
{
break;
}
}
}
catch (IOException e)
{
Log.i(logTag, e.toString());
return false;
}
Log.i(logTag, "读出文件成功");
Toast.makeText(MainActivity.this, strBuffer, Toast.LENGTH_SHORT).show();
return true;
}
private boolean saveFile(String fileDir, String fileName)
{
File fDir = new File(fileDir);
if(!fDir.exists())
{
if(!fDir.mkdirs())
{
Toast.makeText(MainActivity.this,"创建目录失败:"+fileDir, Toast.LENGTH_SHORT).show();
return false;
}
Log.i(logTag, "创建目录"+fileDir+"成功");
}
else
{
Log.i(logTag, "目录"+fileDir+"已经存在");
}
//创建一个文件,用来写入测试数据
String fileFullPath = fileDir + "/" + fileName;
try {
FileOutputStream fo = new FileOutputStream(fileFullPath);
String strTest = "hero come here to test you";
fo.write(strTest.getBytes());
fo.close();
}
catch (IOException e)
{
Log.i(logTag, e.toString());
return false;
}
Log.i(logTag, "写入文件成功");
return true;
}
接下来我们按照计划,来验证各个存储位置实际的读写权限, 先来验证我们想象不需要任何权限的位置,也就是所谓APP私有存储空间(位置)
1. 内部存储的私有位置:
getFilesDir().getAbsolutePath()//--->/data/user/0/com.jsh.savefiletest/file
getCacheDir().getAbsolutePath()//--->/data/user/0/com.jsh.savefiletest/cach
getDir("myDir", MODE_PRIVATE).getAbsolutePath()//--->/data/user/0/com.jsh.savefiletest/app_myDir
通过测试 ,我们发现这3个位置,读写文件一切正常,无需特别指定任何权限,也是预期的结果,没啥可说的。
2.外部存储私有位置
getExternalFilesDir("myDir").getAbsolutePath()//--->/storage/emulated/0/Android/data/com.jsh.savefiletest/files/myDir
getExternalCacheDir().getAbsolutePath()//-->/storage/emulated/0/Android/data/com.jsh.savefiletest/cache
通过测试和内部存储的私有位置无区别
3.内部存储公有位置
Environment.getDataDirectory().getAbsolutePath()//-->/data
经测试, 读写写文件失败
4.外部存储公有位置
Environment.getExternalStorageDirectory().getAbsolutePath()//-->/storage/emulated/0
Environment.getExternalStoragePublicDirectory("myDir").getAbsolutePath();//-->/storage/emulated/0/myDir
经测试, 读写写文件失败
5.系统目录
Environment.getRootDirectory().getAbsolutePath()//-->/system
经测试, 读写写文件失败,这个就是随便试试,一般上层开发不会访问这个目录,底层开发单说,此文不研究
以上的测试并非所有的目录,但可以总结出来这样一个结论,不管是外部存储,还是内部存储只要是app的沙盒内部(私有位置),那就都是可以随便读写的,毕竟这就是系统专门分配给app自身使用的。沙盒外部则不能随意读写。个人认为能用私有位置的就用私有位置了,它并没有那么珍贵。比如下载apk 然后安装 然后删除apk,下载到私有位置一点毛病都没有。
下面讨论,如果真的有特殊需求,需要存储到公有位置,需要怎么处理?
先对安卓权限做个说明,安卓的权限分为3个等级
分别是:normal, signature, dangerous(普通,签名, 危险)
普通:低风险权限,只要申请了就可以使用(在AndroidManifest.xml中添加uses-permission标签),安装时不需要用户确认
签名: 这个似乎只有在自定义权限的时候会用到,用别人SDK的时候可能会用到,可能我理解不对,请大神指正。
还是贴官方的解释吧:
The system grants these app permissions at install time, but only when the app that attempts to use a permission is signed by the same certificate as the app that defines the permission.
危险: 必须在APP运行时明确获得用户许可
那么我们想读写外部存储对应的权限是:
android.permission.WRITE_EXTERNAL_STORAGE
很不幸,这个权限属于“危险”等级的权限需要明确获得用户许可才行。
下面来看看如何获取这个权限。其实讨论这个的文章非常多,我就简单把代码贴一下吧。我主要是想验证这些位置的访问权限,最终得出一些结论. 代码上,仅仅是为了演示
https://developer.android.google.cn/training/permissions/requesting?hl=zh-cn
代码:
在activity的oncreate中
if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
//用户拒绝或多次拒绝,我们来解释为啥需要这个权限,期望用户能够允许,但他仍然不允许,也没招
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.WRITE_EXTERNAL_STORAGE))
{
Toast.makeText(this, "由于我们需要测试外部存储的可用性,所以需要这个权限", Toast.LENGTH_SHORT).show();
}
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1234);
}
然后在重载一个activity的回调函数,来响应申请结果
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
if(requestCode==1234)
{
if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED)
{
Toast.makeText(this, "获取权限成功", Toast.LENGTH_SHORT).show();
}
else
{
Toast.makeText(this, "获取权限失败", Toast.LENGTH_SHORT).show();
}
}
}
此时我们运行app 系统会提示一个权限需求框,点击允许就okay了
似乎一切顺利,但是我们发现 仍然无法在外部存储读写文件,这是。。。
进一步查阅,发现安卓10后,读写文件有变化,说白了安卓10 非常不希望你读写和自己程序无关的文件,如果你非要读:
方案1:SAF,StorageAccessFramework
方案2:或者在配置中指定仍然使用老版本的存储架构
<manifest ... >
<!-- This attribute is "false" by default on apps targeting Android Q. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
这个最简单,经测试确实管用,但这并不是官方推荐的办法,因为安卓11将忽略这个标记,虽然11还没出,但官方文档已经说了,这个标记仅仅是为了测试.
官网原话 "当您将应用更新为以 Android 11 为目标平台后,系统会忽略 requestLegacyExternalStorage
标记"
Environment.getDataDirectory().getAbsolutePath()//-->/data 当然了这个路径仍然不行,但我觉的没啥时候需要像这里写入文件,就没再去研究了。
方案3:也是官方推荐的方案-->将文件存储到过滤视图中
// /Android/data/com.example.androidq/files/Documents
File dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
这其实还是写在了app的沙盒内部,这不需要任何读写权限。
最终总结:
凡是写入app沙盒内部,则不需要任何读写权限
写入外部则需要读写权限,6.0后需要动态申请读写权限,10.0即便动态申请也不行,具体参见上文。
那么对于我来说,写入这个位置足够了 getExternalFilesDir。我就是想写入程序崩溃日志,然后能够让用户或者QA转发给我。
最多定期清理下。 这不需要任何权限。 但凡你申请了“危险的权限” 在一些安全检测软件 都会报警告。比如我们给客户定制app的时候,申请了读写外部存储的权限,就报警告,折腾了好一阵子。