如何使用android存储访问框架正确覆盖文件内容

Posted

技术标签:

【中文标题】如何使用android存储访问框架正确覆盖文件内容【英文标题】:How to properly overwrite content of file using android storage access framework 【发布时间】:2019-11-16 01:48:37 【问题描述】:

>> 背景

我想使用 SAF(存储访问框架字)将我的应用程序的数据文件保存到用户在存储媒体上所需的位置。我首先在应用程序专用文件夹中创建文件,然后将其复制到用户从文件选择器对话框中选择的文件中(代码稍后发布)。

此过程非常适用于新文件,但对于现有文件,尽管文件选择器会警告覆盖文件,但最终文件在写入之前不会被删除。

通过计算写入的字节数并使用十六进制编辑器调查文件,代码将正确的字节写入输出流,但是:如果现有文件的字节数多于要写入的字节数,则最终覆盖的文件已损坏(实际上并没有损坏,请参阅下一部分进行澄清)并且如果现有的字节数少于要写入的字节数,则最终覆盖的文件是正确的。

>>更多细节和代码

我使用下面的代码来显示问题(jpg 是作为示例): 我将尝试使用两个文件:

file1.jpg 166,907 bytes
file2.jpg 1,323,647 bytes
file3.jpg The final file with variable size

首先我将file1复制到用户选择的名为file3的文件夹(目标文件),然后用file2覆盖它,最后我将用file1再次覆盖它。看看代码是什么以及会发生什么:

调用文件选择器的代码:

val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply 
    addCategory(Intent.CATEGORY_OPENABLE)
    type = "image/jpeg"

startActivityForResult(intent, request)

现在在 onActivityResult() 我处理数据如下:

contentResolver.openOutputStream(fileUri)?.use output->
        val input = FileInputStream(File(getExternalFilesDir(null),"Pictures/file1.jpg"))
    // file1.jpg for first run, file2.jpg for 2nd run and file1.jpg again for 3rd run     
        copyStream(input, output)

    

以及复制流的代码:

@Throws(IOException::class)
fun copyStream(input: InputStream, output: OutputStream) 
    val buffer = ByteArray(1024)
    var bytesRead = input.read(buffer)
    while (bytesRead > 0) 
        output.write(buffer, 0, bytesRead)
        bytesRead = input.read(buffer)
    
    input.close()
    //The output will be closes by kotlin standard function "use" at previous code

现在第一次运行 file3.jpg 与 file1.jpg 完全相同。在第二次运行时,file3.jpg 也与 file2.jpg 相同。但在第三次运行时,用 file1.jpg 的内容覆盖 file3.jpg(比 file3.jpg 有 kess 字节),file3.jpg 的大小仍然为 1,323,647 字节,前 166,907 字节与 file1.jpg 相同,其余字节直到1,323,647 与第二次运行时写入的 file2.jpg 相同。

这是十六进制文件的内容:

file1.jpg

0000:0000 | FF D8 FF E1  09 49 45 78  69 66 00 00  49 49 2A 00 | ÿØÿá.IExif..II*.
0000:0010 | 08 00 00 00  09 00 0F 01  02 00 06 00  00 00 7A 00 | ..............z.
...
0002:8BE0 | 56 5E 2A EC  C7 36 6D B1  57 1C D5 CD  95 8A BB 2F | V^*ìÇ6m±W.ÕÍ..»/
0002:8BF0*| 36 6C 55 AD  F2 F3 65 60  43 FF D9*                | 6lU.òóe`CÿÙ     

file2.jpg

0000:0000 | FF D8 FF E0  00 10 4A 46  49 46 00 01  01 00 00 01 | ÿØÿà..JFIF......
0000:0010 | 00 01 00 00  FF E1 01 48  45 78 69 66  00 00 49 49 | ....ÿá.HExif..II
...
0002:8BC0 | F2 07 23 D4  57 CA 7E 13  FD A9 23 B5  86 2D 3E 4D | ò.#ÔWÊ~.ý©#µ.->M
0002:8BD0 | 66 7B 58 D1  42 A3 4D 6A  57 80 38 C9  CF EB 5E 93 | fXÑB£MjW.8ÉÏë^.
0002:8BE0 | E1 3F DA 36  CA EA 10 2E  7C 49 0B C4  E3 21 F6 8C | á?Ú6Êê..|I.Äã!ö.
0002:8BF0*| 9F D6 BB 63  8B A3 86 D5  34 B5 D9*E8  D2 E9 D7 AE | .Ö»c.£.Õ4µÙèÒé×®
0002:8C00 | B7 34 9F B5  85 18 C6 B5  DF 2E FA 6B  AD B6 5D BC | ·4.µ..Ƶß.úk.¶]¼
0002:8C10 | F7 3D 6E F3  C3 50 6B 56  32 D9 CC 14  AB AE 30 C3 | ÷=nóÃPkV2ÙÌ.«®0Ã
...
0014:3260 | E8 8B 0A CE  4E 47 AD 4A  92 B2 E4 E6  8B 3B 7F 34 | è..ÎNG.J.²äæ.;.4
0014:3270 | 1C 55 D8 6C  14 83 BA 88  AB 98 46 4D  33 FF D9    | .UØl..º.«.FM3ÿÙ 

file3.jpg (After the 3rd run)

0000:0000 | FF D8 FF E1  09 49 45 78  69 66 00 00  49 49 2A 00 | ÿØÿá.IExif..II*.
0000:0010 | 08 00 00 00  09 00 0F 01  02 00 06 00  00 00 7A 00 | ..............z.
...
0002:8BD0 | D9 B1 43 BA  E6 39 B7 CD  8A B5 97 9B  36 29 76 5E | Ù±Cºæ9·Í.µ..6)v^
0002:8BE0 | 56 5E 2A EC  C7 36 6D B1  57 1C D5 CD  95 8A BB 2F | V^*ìÇ6m±W.ÕÍ..»/
//content of file1 continues with content of file2 (Next line)
0002:8BF0*| 36 6C 55 AD  F2 F3 65 60  43 FF D9*E8  D2 E9 D7 AE | 6lU.òóe`CÿÙèÒé×®
0002:8C00 | B7 34 9F B5  85 18 C6 B5  DF 2E FA 6B  AD B6 5D BC | ·4.µ..Ƶß.úk.¶]¼
0002:8C10 | F7 3D 6E F3  C3 50 6B 56  32 D9 CC 14  AB AE 30 C3 | ÷=nóÃPkV2ÙÌ.«®0Ã
0002:8C20 | 8C F3 83 5E  55 3D 86 A1  F0 EB C5 72  E9 C6 62 E2 | .ó.^U=.¡ðëÅréÆbâ
...
0014:3260 | E8 8B 0A CE  4E 47 AD 4A  92 B2 E4 E6  8B 3B 7F 34 | è..ÎNG.J.²äæ.;.4
0014:3270 | 1C 55 D8 6C  14 83 BA 88  AB 98 46 4D  33 FF D9    | .UØl..º.«.FM3ÿÙ 

如您所见,file3 以 file1 的内容开始,在第 0002:8BF0 行的第三组 file1 (FF D9) 的最后一个字节之后,它继续以 file2 (E8 D2) 的内容(星点)

我测试了直接在应用程序的专用文件夹中复制相同文件的过程,但结果是正确的,所有三个运行的 file3 都是正确的。问题只是针对新加坡武装部队。

【问题讨论】:

【参考方案1】:

经过三天的搜索和在这里询问一天后,我找到了答案。我没有删除这个问题,因为其他人可能会遇到同样的问题。 问题的本质是带我走错路。它不仅来自复制流,而且在编写例如4 字节 (bbbb) 用 8 字节 (aaaaaaaa) 覆盖文件。它会创建一个文件,其中包含前 4 个新字节,然后是 4 个旧字节! (bbbbaaaa)。

所以答案在 FileOutputStream() 中。 具有写入文件的字节大小 (input.channel.size()) 或 (output.cannel.position()) 并截断剩余字节 (output.channel.truncate(size))。

作为有问题的代码,我改为:

contentResolver.openOutputStream(fileUri)?.use output->
    output as FileOutputStream
    FileInputStream(File(getExternalFilesDir(null),"Pictures/file1.jpg")).useinput->
        copyStream(input, output)
        // this new line removes bytes beyond the input file size
        output.channel.truncate(input.channel.size())
        // or
        // output.channel.truncate(output.channel.position())
    

2019 年 9 月 15 日更新

感谢@mjanssen 的第一条评论,您还可以通过在复制文件之前添加output.channel.truncate(0) copyStream(input, output) 来获得相同的结果:

contentResolver.openOutputStream(fileUri)?.use 
    output-> output as FileOutputStream
    FileInputStream(File(getExternalFilesDir(null),"Pictures/file1.jpg")).useinput->
    output.channel.truncate(0)            
    copyStream(input, output)
    

就是这样

【讨论】:

因为这个问题,我今天实际上浪费了几个小时。只是一个小小的简化。 output.channel.truncate(0)之前的文件复制效果一样,不需要计算新的文件大小。 @mjanssen,是的!谢谢。这是解决问题的更直接的方法。所以我更新了响应。 copyStream 已弃用。你能更新你的代码吗? @t0m。 copyStream 是问题部分中描述的自定义函数。这可能是任何将一些字节写入输出流的东西。【参考方案2】:

花了几天时间试图理解为什么我在覆盖文件时会在文件末尾收到损坏的数据,幸好我找到了这篇文章。我的测试显示与 OP 相同的结果。当写入比现有文件内容短的新内容时,残留数据会留在文件末尾 - 导致“损坏”文件。我的错误假设是写入文件将完全删除/覆盖现有内容。

在“编辑文档”中使用 android 开发人员示例 ...(https://developer.android.com/training/data-storage/shared/documents-files#edit) 突出显示了问题,因为使用他们的代码示例只会覆盖第一条记录。 IE 一个 100 行的文本文件(例如),如果你使用他们的代码示例,仍然是一个 100 行的文本文件,只有第一条记录被更改。如果您只想“原位”修改特定记录,但如果您想完全替换文件内容,则很有用!

这是一个修改后的 java 版本的示例代码,它确保输出在被覆盖之前首先被“清空”(使用上述 OP 描述的方法)

    private void overwriteDocument(Uri uri) 
        try 
            ParcelFileDescriptor pfd = getActivity().getContentResolver().
                    openFileDescriptor(uri, "w");
            FileOutputStream fileOutputStream =
                    new FileOutputStream(pfd.getFileDescriptor());


            // Use this code to ensure that the file is 'emptied' 
            FileChannel fChan=fileOutputStream.getChannel();
            fChan.truncate(0);


            fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() +
                    "\n").getBytes());
            // Let the document provider know you're done by closing the stream.
            fileOutputStream.close();
            pfd.close();
         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
        
    

【讨论】:

以上是关于如何使用android存储访问框架正确覆盖文件内容的主要内容,如果未能解决你的问题,请参考以下文章

Android - 如何使用新的存储访问框架将文件复制到外部 sd 卡

存储访问框架获取正确的 Uri 路径删除/编辑/获取文件

在 Android 中使用存储访问框架保存文件

如果 Android API 级别低于 26,如何将存储访问框架与 MediaMuxer 一起使用

使用 SAF(存储访问框架)的 Android SD 卡写入权限

如何在存储访问框架中设置不常见的文件扩展名?