我们在做各种程序,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的时候,申请了读写外部存储的权限,就报警告,折腾了好一阵子。