IOS开发中的IO流

2,359 阅读8分钟

往期文章

寻找IOS相册中相似图片
Multipeer Connectivity 近场多点通信
实现一个简单沙盒文件浏览器
实现一个简单的面包屑导航

IO流

简介

Input/Output 即输入和输出,以内存为中心

Input是指从外部读入数据到内存,如从磁盘或者网络读取数据到内存

Output是指从内存写入数据到外部,如从内存写入数据到网络

数据必须要放到内存里面才能处理,因为代码在内存中运行,数据也必须读到内存,最终显示是byte字节数组,或者字符串数组

IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。

使用场景

我们的程序将文件从一个地方拷贝的另一个地方的时候,往往需要文件读取到内存当中

直接读取

如果我们才用下图的方式,先将网络视频转为NSData保存到内存,然后再将内存中的NSData写入沙盒当中保存,

使用这种方式,在视频只有几十M的情况下,不会有太大的影响,如果视频是几个G,那么我们的内存中就会临时写入几 个G大小的NSData数据,这时候就会占用非常多的运行内存,在手机运行内存不足的情况下,就会造成crash。

image.png

使用流

好比如我们用桶提水,如果一次需将装很多水,我们就要使用更大的桶。这样我们提起来就相当费劲,这时候如果我们能有一根管道将出水口和池子连起来那么是不是就会轻松很多,也不需要费太大的力气就可以将水从一个地方搬到另一个地方。

数据流也是这样一个概念,我们把数据比作水,流比作管子,内存相当于一个水泵提供动力。这样一来既不会因为数据太多而造成内存暴涨,也可以利用内存快速的读取性能提高效率。

所以流就相当于在内存中打了一条管道让输出文件和输入文件可以实现数据的单向流动。

image.png

IOS中的IO流

NSStream

是一个抽象类,定义了iOS中的流的操作接口。在一切皆为对象的objC中流同样可以理解为一个对象

提供了一种以独立于设备的方式从各种媒体读取和写入数据的简单方法。 您可以为位于内存、文件或网络(使用套接字)中的数据创建流对象,并且您可以使用流对象而无需一次将所有数据加载到内存中

定义的接口如下

打开(open)和关闭(close)

open 以打开流进行读取或写入,如果流对象被添加在runloop上,流对象的delegate方法才会生效

close 以关闭流,注意关闭之后的流,不能在再次打开了

delegate

流对象的委托,注意这里流对象必须添加到runloop上,被委托方才能受到委托方(流对象)传递过来的信息。

scheduleInRunLoop:forMode: 和 removeFromRunLoop:forMode:

scheduleInRunLoop:forMode:将流对象添加到runloop中

removeFromRunLoop:forMode:将流对象从runloop中移除

propertyForKey: 和 setProperty:forKey

设置和获取NSStream流对象的中的属性值

streamStatus 和 streamError

流对象的传输状态以及错误码

NSSInputStream

通过上面的讲解,我们知道NSStream是一个抽象类,本身不具备实例化对象的能力,只是定义了一套实现标准。

而其子类NSSInputStream实现了流对象的读取功能,相当于我们水管模型中的抽水口功能。主要用于流对象写入数据的功能。

主要使用的方法如下

读取缓冲区

以水管模型为例,在现实生活中我们可以真正的拥有一截水管,但是在内存中我们不可能真正的去搭一条管子,所以我们就需要利用缓冲区来实现管子的效果。缓存区有大小以及读取长度的限制,也就是水管的大小以及水流的速度。只有读入缓冲区的数据才能被发送到写入端

read:maxLength:

读取数据方法,

  • 第一个参数:buf 代表读取缓冲区,是一个指针,因为我们知道8位为一个字节,所以是一个指向Int8的数组指针。

  • 第二个参数:maxLength 代表每次从缓冲区读取数据的最大长度。

  • 返回值:表示本次读取的字节数,一般来说是等于maxLength本次最大的读取长度,但如果缓冲区剩余的数据小于maxLength那么,返回值也就是小于maxLength,说明缓冲区的数据以及被读取完毕了。

getBuffer:length:

获取当前流中的数据以及大小

  • 第一个参数: 指向读取缓冲区的指针

  • 第二个参数: 读取缓冲区可用的字节数量

  • 返回值: 是一个BOOL类型 缓冲区可用,则为 YES,否则为 NO

hasBytesAvailable

如果流中有更多数据要读取,则返回 YES,如果没有,则返回 NO

使用

循环读取模式

这种方式利用最原始的循环,一次一次的去缓冲区里面读取InputStream中提取的文件数据。相当于把大文件分成一段一段的写入的缓冲区,缓冲区读完一段之后,在放入下一段。不过如果在主线程中会造成UI等待。因此还是要将其放入子线程中执行。


func setupInputStream(){
        // 设置读取文件的路径
        let input = InputStream.init(fileAtPath: path)
        input?.open()
        let readLength = 32;
        let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: readLength);

        while true {
            let bytes = input?.read(buf, maxLength: readLength)
            if bytes! < readLength {
                break
            }
        }
        input?.close()
        buf.deallocate()
    }

注意在swift中整型数组指针是使用的一个UnsafeMutablePointer来保存的,因此不会被自动GC,所以需要我们手动释放,如果不释放就会造成内存泄漏

Runloop模式

通过上面的模式,我们发现写入流对象(NSInputStream)需要通过缓存区一次一次的读取文件,因此需要一个循环来执行读取,但是据我们所知IOS中本身就有一个无比强大的循环RunLoop。所以我们就可以利用Runloop的特性来帮助我们使用有优质的循环来让流对象读取数据

当把流对象添加到runloop之中后,我就可以通过实现NSStream的委托来获取流读取的状态。

//获取资源地址
func setupInputStream(){
        let path = self.getResource()
       //创建输入流 设置读取文件的路径
        let inputStream = InputStream.init(fileAtPath: path)
        self.inputStream = inputStream;
        inputStream?.delegate = self;
        //给流对象添加运行循环
        inputStream?.schedule(in: RunLoop.current, forMode: .common)
        //打开输入流对象
        inputStream?.open();
}

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {

        switch eventCode {
        
        case .openCompleted: //流对象被成功打开

            print("流对象被成功打开")

        case .hasBytesAvailable: //流对象有数据可读 (NSSInputStream)
            let maxLength = 4*1024
            let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength);
            self.inputStream!.read(buf, maxLength: maxLength)
            buf.deallocate()
            buf.deinitialize(count: maxLength)
  
        case .hasSpaceAvailable: //还有空间提供给流对象写入缓冲区 (NSOuputStream)

        case .endEncountered:  //流对象完成数据读取
            aStream.close()
            aStream.remove(from: RunLoop.current, forMode: .common)
            
        case .errorOccurred: //流对象读取数据错误

            print("流对象读取数据错误")

  
        default: break
        }

    }

NSOutputStream

同样也是NSStream的子类,实现了流对象的写入功能。相当于水管模型当中的放水功能。

写入缓冲区

与读取缓冲区原理一致,用于提供给NSOutputStream写入数据的缓冲区。用于给外部提供NSOutputStream读取的数据

write:maxLength:

写入数据方法,

  • 第一个参数:buf 代表写入缓冲区,是一个指针,因为我们知道8位为一个字节,所以是一个指向Int8的数组指针。

  • 第二个参数:maxLength 代表每次从缓冲区读取数据的最大长度。

  • 返回值:代表本次写入的字节数,一般来说是等于maxLength本次最大的读取长度,但如果缓冲区剩余的数据小于maxLength那么,返回值也就是小于maxLength,说明缓冲区的数据以及被读取完毕了。

hasSpaceAvailable

YES表明当前流还有数据可以写入,NO表示不能

使用

循环读取模式

原理跟读取流一样,也是按照缓冲的大小一次一次的循环去读。同样需要注意释放掉不支持自动GC的变量。避免内存泄漏

func setupOutputStream() {
        let output = OutputStream.init(url: writeResource(), append: false)
        output?.open()
        let readLength = 32;
        let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: readLength);
        while true {
            let bytes = outputStream?.write(buf, maxLength: readLength)
            if bytes! < readLength {
                break
            }
        }
        output?.close()
        buf.deallocate()
    }

Runloop模式

func setupInputStream(){
        //创建 输出流输出文件地址
        let fileUrl = writeResource()
        //创建 输出流
        let outputStream = OutputStream.init(url: fileUrl as URL, append: false)
        self.outputStream = outputStream;
        outputStream?.delegate = self
        outputStream?.schedule(in: RunLoop.current, forMode: .common)
        outputStream?.open()
}

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {

        switch eventCode {
        
        case .openCompleted: //流对象被成功打开

            print("流对象被成功打开") 

        case .hasBytesAvailable: //流对象有数据可读 (NSSInputStream)

        case .hasSpaceAvailable: //还有空间提供给流对象写入 (NSOuputStream)

            let maxLength = 4*1024

            let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength);

            self.outputStream?.write(buf, maxLength: maxLength)

            buf.deallocate()

            buf.deinitialize(count: maxLength)


        case .endEncountered:  //流对象完成数据读取

            aStream.close()

            aStream.remove(from: RunLoop.current, forMode: .common)


        case .errorOccurred: //流对象读取数据错误

            print("流对象读取数据错误")

        default: break
        }
    }

完整IO流读取例子

这里我们通过读取一个大小为70M的视频,来测试使用和不使用IO流是否会导致运行内存暴涨

我们将工程资源目录中的视频,写入到应用目录当中,观察在使用IO流情况下内存的变幻,和不使用IO流内存的变幻

不使用IO流

直接将文件读入内存,然后再写入沙盒

我们可以观察到内存突然之间增长了几倍

image.png

使用IO流

在使用了IO流之后,我们发现内存变幻非常平稳,则表明没有大量的数据突然涌入内存,文件的读取都是通过我们IO流搭建好的管道一点一点的从发送位置到接收位置

企业微信截图_6bc82671-5048-4c13-9c6f-e4fadbdd82f7.png

读写流程代码

func openIOStream() {
        //获取资源地址

        let path = self.getResource()

//        //创建输入流

        let inputStream = InputStream.init(fileAtPath: path)

        self.inputStream = inputStream;

        inputStream?.delegate = self;

        //给流对象添加运行循环

        inputStream?.schedule(in: RunLoop.current, forMode: .common)

        //打开输入流对象
        inputStream?.open();

        //创建 输出流输出文件地址

        let fileUrl = writeResource()

        //创建 输出流

        let outputStream = OutputStream.init(url: fileUrl as URL, append: false)

        self.outputStream = outputStream;

        outputStream?.delegate = self

        outputStream?.schedule(in: RunLoop.current, forMode: .common)

        outputStream?.open()

}


func stream(_ aStream: Stream, handle eventCode: Stream.Event) {

        switch eventCode {

        case .openCompleted: //流对象被成功打开
            print("流对象被成功打开")
          
        case .hasBytesAvailable: //流对象有数据可读 (NSSInputStream)
            let maxLength = 4*1024
            
            let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength);

            self.inputStream!.read(buf, maxLength: maxLength)
           
            buf.deallocate()
            
            buf.deinitialize(count: maxLength)
        

        case .hasSpaceAvailable: //还有空间提供给流对象写入 (NSOuputStream)

            let maxLength = 4*1024

            let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength);

            self.outputStream?.write(buf, maxLength: maxLength)

            buf.deallocate()

            buf.deinitialize(count: maxLength)
            

        case .endEncountered:  //流对象完成数据读取

             self.inputStream?.close()

            self.inputStream?.remove(from: RunLoop.current, forMode: .common)

            self.outputStream?.close()

            self.outputStream?.remove(from: RunLoop.current, forMode: .common)
            

        case .errorOccurred: //流对象读取数据错误

            print("流对象读取数据错误")

        default: break

        }

 
    }