本文转自:http://southpeak.github.io/blog/2014/07/17/ioszhong-liu-stream-de-shi-yong/
流提供了一种简单的方式在不同和介质中交换数据,这种交换方式是与设备无关的。流是在通信路径中串行传输的连续的比特位序列。从编码的角度来看,流是单向的,因此流可以是输入流或输出流。除了基于文件的流外,其它形式的流都是不可查找的,这些流的数据一旦消耗完后,就无法从流对象中再次获取。
在Cocoa中包含三个与流相关的类:NSStream
、NSInputStream
和NSOutputStream
。NSStream
是一个抽象基类,定义了所有流对象的基础接口和属性。NSInputStream
和NSOutputStream
继承自NSStream
,实现了输入流和输出流的默认行为。下图描述了流的应用场景:
从图中看,NSInputStream
可以从文件、socket
和NSData
对象中获取数据;NSOutputStream
可以将数据写入文件、socket
、内存缓存和NSData
对象中。这三处类主要处理一些比较底层的任务。
流对象有一些相关的属性。大部分属性是用于处理网络安全和配置的,这些属性统称为SSL和SOCKS代理信息。两个比较重要的属性是:
-
NSStreamDataWrittenToMemoryStreamKey
:允许输出流查询写入到内存的数据 -
NSStreamFileCurrentOffsetKey
:允许操作基于文件的流的读写位置
可以给流对象指定一个代理对象。如果没有指定,则流对象作为自己的代理。流对象调用唯一的代理方法stream:handleEvent:
来处理流相关的事件:
- 对于输入流来说,是有可用的数据可读取事件。我们可以使用
read:maxLength:
方法从流中获取数据 - 对于输出流来说,是准备好写入的数据事件。我们可以使用
write:maxLength:
方法将数据写入流
Cocoa中的流对象与Core Foundation中的流对象是对应的。我们可以通过toll-free桥接方法来进行相互转换。NSStream
、NSInputStream
和NSOutputStream
分别对应CFStream
、CFReadStream
和CFWriteStream
。但这两者间不是完全一样的。Cocoa一般使用回调函数来处理数据。另外我们可以子类化 NSStream、NSInputStream和NSOutputStream
,来自定义一些属性和行为,而Core Foundation中的流对象则无法进行扩展。
上面主要介绍了iOS中流的一些基本概念,我们下面将介绍流的具体使用,首先看看如何从流中读取数据。
从输入流中读取数据
从一个NSInputStream
流中读取数据主要包括以下几个步骤:
- 1、 从数据源中创建和初始化一个NSInputStream实例
- 2、 将流对象放入一个run loop中并打开流
- 3、 处理流对象发送到其代理的事件
- 4、 当没有更多数据可读取时,关闭并销毁流对象。
要使用一个NSInputStream
,必须要有数据源。数据源可以是文件、NSData
对象和网络socket。创建好后,我们设置其代理对象,并将其放入到run loop中,然后打开流。
在流对象放入run loop且有流事件(有可读数据)发生时,流对象会向代理对象发送stream:handleEvent:消息。在打开流之前,我们需要调用流对象的scheduleInRunLoop:forMode:方法,这样做可以避免在没有数据可读时阻塞代理对象的操作。我们需要确保的是流对象被放入正确的run loop中,即放入流事件发生的那个线程的run loop中。
处理流事件
打开流后,我们可以使用streamStatus属性查看流的状态,用hasBytesAvailable属性检测是否有可读的数据,用streamError来查看流处理过程中产生的错误。
流一旦打开后,将会持续发送stream:handleEvent:消息给代理对象,直到流结束为止。这个消息接收一个NSStreamEvent常量作为参数,以标识事件的类型。对于NSInputStream对象,主要的事件类型包括NSStreamEventOpenCompleted、NSStreamEventHasBytesAvailable和NSStreamEventEndEncountered。通常我们会对NSStreamEventHasBytesAvailable更感兴趣。代理清单2演示了从流中获取数据的过程
代码如下:
- (void)setUpStreamForFile:(NSString *)fullPath {
// 1、 从数据源中创建和初始化一个NSInputStream实例
NSInputStream *inputStream = [[NSInputStream alloc] initWithFileAtPath:fullPath];
self.inputStream = inputStream;
// 2、 将流对象放入一个run loop中并打开流
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// 3、 处理流对象发送到其代理的事件
inputStream.delegate = self;
// 4、 打开流
[inputStream open];
}
#pragma mark -----------
#pragma mark NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
/*
NSStreamEventNone = 0,
NSStreamEventOpenCompleted = 1UL << 0,
NSStreamEventHasBytesAvailable = 1UL << 1,
NSStreamEventHasSpaceAvailable = 1UL << 2,
NSStreamEventErrorOccurred = 1UL << 3,
NSStreamEventEndEncountered = 1UL << 4
*/
switch (eventCode) {
case NSStreamEventOpenCompleted:
NSLog(@"打开流");
break;
case NSStreamEventHasBytesAvailable:
NSLog(@"读取到了数据");
uint8_t buf[1024] ={0};
NSInteger length = [self.inputStream read:buf maxLength:1024];
if (length > 0) {
[self.imageData appendBytes:buf length:length];
} else {
NSLog(@"读取到数据长度<0");
}
break;
case NSStreamEventHasSpaceAvailable:
NSLog(@"NSStreamEventHasSpaceAvailable");
break;
case NSStreamEventErrorOccurred:
NSLog(@"出错了");
break;
case NSStreamEventEndEncountered:
NSLog(@" 文件读取结束");
// 4、 当没有更多数据可读取时,关闭并销毁流对象。
// 关闭输入流
[self.inputStream close];
// 从运行循环中移除
[self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.inputStream = nil;
// 把读出的图片流数据显示出来
self.imageView.image = [UIImage imageWithData:self.imageData];
break;
default:
break;
}
}
写入数据到输出流
类似于从输入流读取数据,写入数据到输出流时,需要下面几个步骤:
- 1、 使用要写入的数据创建和初始化一个
NSOutputStream
实例,并设置代理对象 - 2、 将流对象放到run loop中并打开流
- 3、 处理流对象发送到代理对象中的事件
- 4、 如果流对象写入数据到内存,则通过请求
NSStreamDataWrittenToMemoryStreamKey
属性来获取数据 - 5 、当没有更多数据可供写入时,处理流对象
基本流程与输入流的读取差不多,我们主要介绍不同的地方
数据可写入的位置包括文件、C缓存、程序内存和网络socket。
-
hasSpaceAvailable
属性表示是否有空间来写入数据
在stream:handleEvent:
中主要处理NSStreamEventHasSpaceAvailable
事件,并调用流的write:maxLength
方法写数据。如果
NSOutputStream
对象的目标是应用的内存时,在NSStreamEventEndEncountered
事件中可能需要从内存中获取流中的数据。我们将调用NSOutputStream
对象的propertyForKey:
的属性,并指定key为NSStreamDataWrittenToMemoryStreamKey
来获取这些数据。
这里需要注意的是:当代理接收到NSStreamEventHasSpaceAvailable
事件而没有写入任何数据到流时,代理将不再从run loop中接收该事件,直到NSOutputStream
对象接收到更多数据,这时run loop
会重启NSStreamEventHasSpaceAvailable
事件。
#pragma mark -----------
#pragma mark NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
/*
NSStreamEventNone = 0,
NSStreamEventOpenCompleted = 1UL << 0,
NSStreamEventHasBytesAvailable = 1UL << 1,
NSStreamEventHasSpaceAvailable = 1UL << 2,
NSStreamEventErrorOccurred = 1UL << 3,
NSStreamEventEndEncountered = 1UL << 4
*/
switch (eventCode) {
case NSStreamEventOpenCompleted:
NSLog(@"打开流");
break;
case NSStreamEventHasBytesAvailable:
NSLog(@"读取到了数据");
uint8_t buf[1024] ={0};
NSInteger length = [self.inputStream read:buf maxLength:1024];
if (length > 0) {
[self.imageData appendBytes:buf length:length];
} else {
NSLog(@"读取到数据长度<0");
}
break;
case NSStreamEventHasSpaceAvailable:{
NSLog(@"是否有空间来写入数据"); // NSOutputStream 使用
uint8_t *readBytes = (uint8_t *)[self.imageData mutableBytes];
// 指针后移
readBytes += self.byteIndex;
NSInteger data_len = [self.imageData length];
NSInteger len = (data_len - self.byteIndex >= 1024) ? 1024 : (data_len - self.byteIndex);
uint8_t buf1[len];
(void)memcpy(buf1, readBytes, len);
NSOutputStream *outpuStream = (NSOutputStream *)aStream;
len = [outpuStream write:(const uint8_t *)buf1 maxLength:len];
self.byteIndex += len;
}
break;
case NSStreamEventErrorOccurred:
NSLog(@"出错了");
break;
case NSStreamEventEndEncountered:
NSLog(@" 文件读/写结束");
// 4、 当没有更多数据可读取时,关闭并销毁流对象。
// 关闭输入流
[aStream close];
// 从运行循环中移除
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.inputStream = nil;
self.outputStream = nil;
// 把读出的图片流数据显示出来
self.imageView.image = [UIImage imageWithData:self.imageData];
break;
default:
break;
}
}
流的轮循处理
在流的处理过程中,除了将流放入run loop来处理流事件外,还可以对流进行轮循处理。我们将流处理数据的过程放到一个循环中,并在循环中不断地去询问流是否有可用的数据供读取(hasBytesAvailable)或可用的空间供写入(hasSpaceAvailable)。当处理到流的结尾时,我们跳出循环结束流的操作。
具体的过程如代码清单5所示
- (void)createNewFile {
NSOutputStream *oStream = [[NSOutputStream alloc] initToMemory];
[oStream open];
uint8_t *readBytes = (uint8_t *)[data mutableBytes];
uint8_t buf[1024];
int len = 1024;
while (1) {
if (len == 0) break;
if ([oStream hasSpaceAvailable])
{
(void)strncpy(buf, readBytes, len);
readBytes += len;
if ([oStream write:(const uint8_t *)buf maxLength:len] == -1)
{
[self handleError:[oStream streamError]];
break;
}
[bytesWritten setIntValue:[bytesWritten intValue]+len];
len = (([data length] - [bytesWritten intValue] >= 1024) ? 1024 : [data length] - [bytesWritten intValue]);
}
}
NSData *newData = [oStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
if (!newData) {
NSLog(@"No data written to memory!");
} else {
[self processData:newData];
}
[oStream close];
[oStream release];
oStream = nil;
}
这种处理方法的问题在于它会阻塞当前线程,直到流处理结束为止,才继续进行后面的操作。而这种问题在处理网络socket流时尤为严重,我们必须等待服务端数据回来后才能继续操作。因此,通常情况下,建议使用run loop方式来处理流事件。
错误处理
当流出现错误时,会停止对流数据的处理。一个流对象在出现错误时,不能再用于读或写操作,虽然在关闭前可以查询它的状态。
NSStream
和NSOutputStream
类会以几种方式来告知错误的发生:
1. 如果流被放到run loop中,对象会发送一个NSStreamEventErrorOccurred事件到代理对象的stream:handleEvent:方法中
2. 任何时候,可以调用streamStatus属性来查看是否发生错误(返回NSStreamStatusError)
3. 如果在通过调用write:maxLength:写入数据到NSOutputStream对象时返回-1,则发生一个写错误。
一旦确定产生错误时,我们可以调用流对象的streamError属性来查看错误的详细信息。在此我们不再举例。
设置Socket流
在iOS中,NSStream类不支持连接到远程主机,幸运的是CFStream支持。前面已经说过这两者可以通过toll-free桥接来相互转换。使用CFStream时,我们可以调用CFStreamCreatePairWithSocketToHost函数并传递主机名和端口号,来获取一个CFReadStreamRef和一个CFWriteStreamRef来进行通信,然后我们可以将它们转换为NSInputStream和NSOutputStream对象来处理。