重学Android-文件的IO操作

1,544 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

Android文件IO操作的几种方式

上一篇文章我们讲到了文件的路径操作。在高版本和低版本如何操作与管理文件的路径。

这一篇我们讲文件的IO操作,拿到文件的路径之后我们改如何读写文件,有哪些方式?常用的文件操作为读取文件,写入文件,复制文件。

输入输出流有很多的分类:

  • 输入流: InputStream FileInputStream BufferedInputStream OutputStreamReader FileReader BufferedReader 等等

  • 输出流: OutputStream FileOutputStream BufferedOutputStream OutputStreamWriter FileWriter BufferedWriter 等等

我们从不同的方式来看看如何使用IO流。

一、IO流的方式

我们先理解基本的概念:

输入与输出我们叫IO,通常我们使用输入流读取数据,使用输出流写入数据。不管是操作本地文件,还是网络获取数据,都是这个逻辑。

缓冲区(Buffer),加了Buffer的一些流,就是加入了缓冲区的流,内部有专门为传输数据的一块内存区,当向一个缓冲流写入数据时,系统将数据发送到缓冲区,而不是直接发送到外部设备,当缓冲区满时,系统将数据全部发送到相应的外部设备,当从一个缓冲流中读取数据时,系统实际是从缓冲区中读取数据,当缓冲区为空时,系统就会从相关外部设备自动读取数据,并读取尽可能多的数据填满缓冲区。

1.1 输出流保存文本
    public static void writeText(Context context, String fileName, String content) {

        if (content == null) content = "";

        try {
            File filesDir = context.getExternalFilesDir("texts");
            File newFile = new File(filesDir, fileName);
            FileOutputStream fos = new FileOutputStream(newFile, true);

            fos.write(content.getBytes());

            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

可以实现自定义路径的File文件

我们同样可以通过Api的方式打开一个 FileOutputStream 来写入文件。

    public static void writeText(Context context, String fileName, String content) {

        if (content == null) content = "";

        try {

            FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
            fos.write(content.getBytes());

            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

注意这里的路径在data/data下的沙盒文件下:

1.2 文本的读取

我们获取FileInputStream 对象,把输入流的内容填充到一个特定的输出流中,然后可以打印出来。

 public static String read(Context context, String fileName) {
        try {
            FileInputStream in = context.openFileInput(fileName);
            return readInStream(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";

    }


    private static String readInStream(FileInputStream inStream) {
        try {
            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[512];
            int length = -1;
            while ((length = inStream.read(buffer)) != -1) {
                outStream.write(buffer, 0, length);
            }

            outStream.close();
            inStream.close();
            return outStream.toString();
        } catch (IOException e) {
            Log.i("FileTest", e.getMessage());
        }
        return null;

    }

打印的效果:

1.3 文件的复制

上面我们已经可以操作到输入流和输出流,我们只是把输出流打印出来了,而把输出流输出到指定的File中,那么就实现了文件的复制。

 public static boolean copyFile(Context context, String fileName) {

        try {
            FileInputStream in = context.openFileInput(fileName);
            BufferedInputStream bis = new BufferedInputStream(in);

            File file = context.getExternalFilesDir("copy");
            File newFile = new File(file, "copy.txt");
            OutputStream out = new FileOutputStream(newFile);
            BufferedOutputStream bos = new BufferedOutputStream(out);

            byte[] buffer = new byte[512];
            int length = -1;
            while ((length = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, length);
            }

            bis.close();
            bos.close();
            return true;

        } catch (Exception ex) {
            return false;
        }
    }

效果:

到处我们实现一个简单的IO流操作,但是操作文件我们有更方便的Api。看看下面的

二、FileWriter 与 FileReader

可直接读写字符的IO流。本质上他们还是输入输出流,只是方法更便捷,语义化了。使用起来也方便了一些。

常用的方法:

//都是继承自Reader的方法
int read()
int read(char[] b)
int read(char[] b , int off ,int len)

//都是继承自Writer
void write(char[] b)
void write(char[] b, int off, int len)
void write(int b)

void write(String str)
void write(String str, int off, int len)
Writer append(char c)
Writer append(CharSequence csq)
Writer append(CharSequence csq, int start, int end)

这里需要注意write是覆盖写入,append是追加文本,下面看看示例:

读取一行文本的方法:

    public static void readLine(Context context, String fileName){
        String filePath = context.getFilesDir().getAbsolutePath() + "/" + fileName;
        File file = new File(filePath);
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
            String line;
            // 循环读取文件中的每一行并打印
            while ((line = bufferedReader.readLine()) != null) {
                YYLogUtils.i(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

同样我们可以通过 FileReaderFileWriter 来替代 InputStreamoutPutStream 实现文件的复制:

    public static boolean copyFile2(Context context, String fileName) {

        try {
            String file = context.getFilesDir().getAbsolutePath() + "/" + fileName;
            FileReader fr = new FileReader(file);


            File file2 = context.getExternalFilesDir("copy2");
            File newFile = new File(file2, "copy2.txt");
            FileWriter fw = new FileWriter(newFile);

            int len = -1;
            char[] buffer = new char[1024];
            while ((len = fr.read(buffer)) != -1) {
                fw.write(buffer, 0, len);
            }

            fr.close();
            fw.close();
            return true;

        } catch (Exception ex) {
            return false;
        }
    }

效果:

三、Kotlin扩展方法

Kotlin对常用的IO操作已经有一些扩展方法实现,使用起来更便捷。

常用的一些扩展方法:

//读取该文件的所有内容作为一个字符串返回
public fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)
//读取文件的每一行内容,存入一个List返回
public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String>
//读取文件的所有内容以ByteArray的方式返回
public fun File.readBytes(): ByteArray = inputStream().use { input ->
//覆盖写入ByteArray字节数组流
public fun File.writeBytes(array: ByteArray): Unit = FileOutputStream(this).use { it.write(array) }
//覆盖写入text字符串到文件中
public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset))
//在文件末尾追加写入text字符串
public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit = appendBytes(text.toByteArray(charset))
//在文件末尾追加写入ByteArray字节数组流
public fun File.appendBytes(array: ByteArray): Unit = FileOutputStream(this, true).use { it.write(array) }
3.1 输入输出流

举例说明:

    fun readText(context: Context, fileName: String): String {
        val filePath: String = context.filesDir.absolutePath + "/" + fileName
        val file = File(filePath)

        return file.readText()
    }

从IO流到Kotlin,使用起来是不是越来越简单了:

同样的方式通过File的扩展方法 readTextwriteText ,也可以实现文本的复制。

    fun writeText(context: Context, fileName: String) {
        val filePath: String = context.filesDir.absolutePath + "/" + fileName
        val file = File(filePath)

        val text = file.readText()

        val newFile = File(context.getExternalFilesDir("copy3"), "copy3.txt")
        newFile.writeText(text)

    }

当然了,这只是简单的文本复制可以使用writeText,如果是图片或者文件,大家使用byte即可。

3.2 use扩展自动释放资源

use扩展方法,是Closeable的扩展,可以对任意流进行扩展,传入一段代码块,内部进行try catch finally 的处理,finally 会进行资源的释放。

源码如下:

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}

我们可以通过IO流的一些对象都可以调用use方法,非常的方便,如下

file.reader().useLines {  }
file.bufferedReader().use {  }
file.bufferedWriter().use { }
file.inputStream().use {  }
file.outputStream().use {  }

写入缓冲区,读取缓冲区:

    fun writeText(context: Context) {

        val newFile = File(context.getExternalFilesDir("copy4"), "copy4.txt")

        //写入
        newFile.bufferedWriter().use { out -> out.write("writer something") }

        //读取
        newFile.bufferedReader().use { println(it.readText()) }

    }

同样的FileReaderFileWriter 都可以使用 use 扩展方法

    fun writeText(context: Context) {

        val newFile = File(context.getExternalFilesDir("copy4"), "copy4.txt")

        FileWriter(newFile).use {
            it.append("writer something...")
            it.append("\n")
            it.flush()
        }

    }
3.3 其他扩展方法

除了IO的操作,还有一些常用的扩展,如遍历文件树:

       val file = File(context.getExternalFilesDir(null))
        val walk = file.walk()
        walk.iterator().forEach {
            if (it.isFile) {
                println(it.absoluteFile)
                println(it.readText(Charset.forName("gb2312")))
            }
        }

还有一些不是太常用的,如URL类的两个扩展方法,readBytes和readText

四、Okio 的使用

相信大家网络请求应该都是基于OkHttp ,那么Okio一定不陌生了,OkIO的本质是Java IO的封装,代码编写时需要注意IO流的关闭,流操作完成后,最后调用close方法。

Skin Source Buffer 很多概率,第一次接触Okio的就很懵逼,和InputStream OutPutStream 怎么完全不一样了,不懂他们的意思,这里贴一张网图大家就懂了:

可以看到,无论读写,都是通过Buffer统一操作,底层还是使用了OutputSream、InputStream,本质上还是对Java IO相关的API的封装。Sink负责输出相关的操作,而Source负责输入相关的操作。

其内部使用分段存储的方式,采用双向链表可以在数据的复制、转移等操作场景显得十分高效。

下面我们演示一下代码的使用,基于Okio3.0版本,Kotlin的重构版本!

4.1 Okio的输入与输出

File的扩展方法,直接转为 Sink 输出流:

    fun okioWrite(context: Context, fileName: String) {
        val filePath: String = context.filesDir.absolutePath + "/" + fileName
        val file = File(filePath)

        //先转换为Sink 在转换为BufferedSink
        file.sink(true).buffer().use {
            it.writeUtf8("writer something..")
            it.writeUtf8("\n")
        }
    }

文件的复制,和 InputStream OutputStream 类似的 我们通过 SourceSink 的输入输出流来操作

    fun copyFile() {
        val inFile = File(commContext().filesDir.absolutePath + "/haha.txt")
        val inBuffer = inFile.source().buffer()

        val outFile = File(commContext().getExternalFilesDir("copy5"), "copy5.txt")
        outFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }

    }

以上是File的扩展方法可以很方便的转为SourceSink,那么在Android10以上,我们无法获取到File,我们一样可以通过Uri获取到输入流,然后转换为Source进行文件的复制:

    fun copyFile() {
        val fis: FileInputStream = commContext().openFileInput(commContext().filesDir.absolutePath + "/haha.txt")
        val inBuffer = fis.source().buffer()

        val outFile = File(commContext().getExternalFilesDir("copy5"), "copy5.txt")
        outFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }

    }
    fun copyFile() {
        val fis =  commContext().contentResolver.openInputStream(inUri)
        val inBuffer = fis.source().buffer()

        val outFile = File(commContext().getExternalFilesDir("copy5"), "copy5.txt")
        outFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }

    }

同样的可以完成文件的复制!

4.2 持续写入的优化

可以说IO流的操作最为简单,性能也更好,特别是3.0版本重构加入了很多扩展的方法,使用更加简单。性能比原生IO的性能要更好,但是在一些重复写入重大数据的场景会有性能压力,频繁的创建File 开启资源写入 关闭资源 会导致性能浪费,我们可以通过handle管理持续写入的场景。做到全部写完再释放资源。

原始代码:

     val file = File(context.filesDir.absolutePath + "/" + fileName)

       file.sink(true).buffer().use {
            it.writeUtf8("writer something..")
            it.writeUtf8("\n")
        }

如果循环调用1万次,写入的是大文本,那么频繁的创建file,打开输出流,再关闭输出流会导致性能问题。

优化方案:

    val file = File(commContext().filesDir.absolutePath + "/haha.txt")
    private var bufferedSink: BufferedSink? = null
    private var mHandle = Handler(Looper.getMainLooper()) { msg ->
        val sink = checkSink()
        if (msg.what == 1) {
            //写完,flush
            sink.use {
                it.flush()
                bufferedSink = null
            }
        } else {
            //持续写入
            sink.writeUtf8("writer something..")
            sink.writeUtf8("\n")
        }
        false
    }

    fun okioWrite2() {
        mHandle.run {
            removeMessages(1)
            mHandle.sendEmptyMessage(0)
            mHandle.sendEmptyMessageDelayed(1, 2000)
        }
    }

    private fun checkSink(): BufferedSink {
        if (bufferedSink == null) {
            bufferedSink = file.appendingSink().buffer()
        }
        return bufferedSink!!
    }

当然大家如果不喜欢用Okio,那么使用FileWriter也可以实现同样的效果,一样的会大幅提升性能。

总结

从基本的 InputStream OutputStreamOkio 的使用,到此就告一段落,如果有需求可以查看源码

总的来说如果大家项目中有使用到Okio,还是更推荐大家使用的。如果没有用到或者不喜欢使用,也可以看看上面的几种方法的实现!

好了,如果有更好的方案还请评论区交流,如有错漏还请指出!

如果觉得本文不错还请点赞支持。

到此完结!