DispatchIO
DispatchIO 对象提供一个操作文件描述符的通道。简单讲你可以利用多线程异步高效地读写文件。
发起读写操作一般步骤如下:
- 创建 DispatchIO 对象,或者说创建一个通道,并设置结束处理闭包。
- 调用 read / write 方法
- 调用 close 方法关闭通道
- 在 close 方法后系统将自动调用结束处理闭包
文件描述符使用 open方法创建:open(_ path: UnsafePointer, _ oflag: Int32, _ mode: mode_t) -> Int32,第一个参数是 UnsafePointer 类型的路径,oflag 、mode 指文件的操作权限,一个是系统 API 级的,一个是文件系统级的,可选项如下:
| Flag | 备注 | 功能 |
|---|---|---|
| O_RDONLY | 以只读方式打开文件 | 此三种读写类型只能有一种 |
| O_WRONLY | 以只写方式打开文件 | 此三种读写类型只能有一种 |
| O_RDWR | 以读和写的方式打开文件 | 此三种读写类型只能有一种 |
| O_CREAT | 打开文件,如果文件不存在则创建文件 | 创建文件时会使用Mode参数与Umask配合设置文件权限 |
| O_EXCL | 如果已经置O_CREAT且文件存在,则强制open()失败 | 可以用来检测多个进程之间创建文件的原子操作 |
| O_TRUNC | 将文件的长度截为0 | 无论打开方式是RD,WR,RDWR,只要打开就会把文件清空 |
| O_APPEND | 强制write()从文件尾开始不care当前文件偏移量所处位置,只会在文件末尾开始添加 | 如果不使用的话,只会在文件偏移量处开始覆盖原有内容写文件 |
创建的通道有两种类型:
- 连续数据流:DispatchIO.StreamType.stream,这个方式是对文件从头到尾完整操作的。
- 随机片段数据:DispatchIO.StreamType.random,这个方式是在文件的任意一个位置(偏移量)开始操作的。
let filePath: NSString = "test.zip"
// 创建一个可读写的文件描述符
let fileDescriptor = open(filePath.utf8String!, (O_RDWR | O_CREAT | O_APPEND), (S_IRWXU | S_IRWXG))
let queue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue")
let cleanupHandler: (Int32) -> Void = { errorNumber in
}
let io = DispatchIO(type: .stream, fileDescriptor: fileDescriptor, queue: queue, cleanupHandler: cleanupHandler)
文件路径方式
let io = DispatchIO(type: .stream, path: filePath.utf8String!, oflag: (O_RDWR | O_CREAT | O_APPEND), mode: (S_IRWXU | S_IRWXG), queue: queue, cleanupHandler: cleanupHandler)
数据块大小阀值
DispatchIO 支持多线程操作的原因之一就是它将文件拆分为数据块进行并行操作,你可以设置数据块大小的上下限,系统会采取合适的大小,使用这两个方法即可:setLimit(highWater: Int)、setLimit(lowWater: Int),单位是 byte。
io.setLimit(highWater: 1024*1024)
数据块如果设置小一点(如 1M),则可以节省 App 的内存,如果内存足够则可以大一点换取更快速度。在进行读写操作时,有一个性能问题需要注意,如果同时读写的话一般分两个通道,且读到一个数据块就立即写到另一个数据块中,那么写通道的数据块上限不要小于读通道的,否则会造成内存大量积压无法及时释放。
读操作
ioRead.read(offset: 0, length: Int.max, queue: ioReadQueue) { doneReading, data, error in
if (error > 0) {
print("读取文件发生错误了,错误码:\(error)")
return
}
if (data != nil) {
// 使用数据
}
if (doneReading) {
ioRead.close()
}
}
offset 指定读取的偏移量,如果通道是 stream 类型,值不起作用,写为 0 即可,将从文件开头读起;如果是 random 类型,则指相对于创建通道时文件的起始位置的偏移量。 length 指定读取的长度,如果是读取文件全部内容,设置 Int.max 即可,否则设置一个小于文件大小的值(单位是 byte)。 每读取到一个数据块都会调用你设置的处理闭包,系统会提供三个入参给你:结束标志、本次读取到的数据块、错误码:
- 在所有数据读取完成后,会额外再调用一个闭包,通过结束标志告诉你操作结束了,此时 data 大小是 0,错误码也是 0。
- 如果读取中间发生了错误,则会停止读取,结束标志会被设置为 true,并返回相应的错误码,错误码表参考稍后的【关闭通道】小节:
写操作
ioWrite.write(offset: 0, data: data!, queue: ioWriteQueue) { doneWriting, data, error in
if (error > 0) {
print("写入发生错误了,错误码:\(error)")
return
}
if doneWriting {
//...
ioWrite.close()
}
}
写操作与读操作的唯一区别是:每当写完一个数据块时,回调闭包返回的 data 是剩余的全部数据。同时注意如果是 stream 类型,将接着文件的末尾写数据。
关闭通道
当读写正常完成,或者你需要中途结束操作时,需要调用 close 方法,这个方法带一个 DispatchIO.CloseFlags 类型参数,如果不指定将默认值为 DispatchIO.CloseFlags.stop。 这个方法传入 stop 标志时将会停止所有未完成的读写操作,影响范围是所有 I/O channel,其他 DispatchIO 对象进行中的读写操作将会收到一个 ECANCELED 错误码,rawValue 值是 89,这个错误码是 POSIXError 结构的一个属性,而 POSIXError 又是 NSError 中预定义的一个错误域。 因此如果要在不同 DispatchIO 对象中并行读取操作互不影响, close 方法标志可以设置一个空值:DispatchIO.CloseFlags()。如果设置了 stop 标志,则要做好不同 IO 之间的隔离,通过任务组的enter、leave、wait 方法可以做到较好的隔离。
ioWrite.close() // 停止标志
ioWrite.close(flags: DispatchIO.CloseFlags()) // 空标志