NSStream
流提供了一种简单的方式在不同和介质中交换数据,这种交换方式是与设备无关的。流是在通信路径中串行传输的连续的比特位序列。从编码的角度来看,流是单向的,因此流可以是输入流或输出流。除了基于文件的流外,其它形式的流都是不可查找的,这些流的数据一旦消耗完后,就无法从流对象中再次获取。
在Cocoa中包含三个与流相关的类:NSStream、NSInputStream和NSOutputStream。NSStream是一个抽象基类,定义了所有流对象的基础接口和属性。NSInputStream和NSOutputStream继承自NSStream,实现了输入流和输出流的默认行为。从下图中可以看出,NSInputStream可以从文件、socket和NSData对象中获取数据;NSOutputStream可以将数据写入文件、socket、内存缓存和NSData对象中
NSInputStream
//从流中读取数据到 buffer 中,buffer 的长度不应少于 len,
//该接口返回实际读取的数据长度(该长度最大为 len)
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len;
//获取当前流中的数据以及大小,注意 buffer 只在下一个流操作之前有效。
- (BOOL)getBuffer:(uint8_t * _Nullable * _Nonnull)buffer length:(NSUInteger *)len;
//检查流中是否还有数据。
@property (readonly) BOOL hasBytesAvailable;
在Cocoa中,从NSInputStream实例读取包含几个步骤:
- 从数据源中创建和初始化一个NSInputStream实例
- 将流对象放入一个run loop中并打开流
- 处理流对象发送到其代理的事件
- 当没有更多数据可读取时,关闭并销毁流对象。
创建和初始化NSInputStream对象
// inputSteam是NSInputStream实例变量
NSString *path = [[NSBundle mainBundle] pathForResource:@"abc" ofType:@"pcm"];
inputSteam = [[NSInputStream alloc] initWithFileAtPath:path];
inputSteam.delegate = self;
[inputSteam scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[inputSteam open];
处理代理事件
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventNone:{
NSLog(@"NSStreamEventNone");
}
break;
case NSStreamEventOpenCompleted:{
NSLog(@"NSStreamEventOpenCompleted");
}
break;
case NSStreamEventHasBytesAvailable:
{
if (!data) {
data = [NSMutableData data];
}
uint8_t buf[1024];
NSInteger len = 0;
len = [(NSInputStream *)aStream read:buf maxLength:1024];// 读取数据
if (len) {
[data appendBytes:(const void *)buf length:len];
}
NSLog(@"数据长度:%lu",data.length);
}
break;
case NSStreamEventHasSpaceAvailable:{
NSLog(@"NSStreamEventHasSpaceAvailable");
}
break;
//此处处理错误事件
case NSStreamEventErrorOccurred:
{
NSError * error = [aStream streamError];
NSString * errorInfo = [NSString stringWithFormat:@"Failed while reading stream; error '%@' (code %ld)", error.localizedDescription, error.code];
NSLog(@"%@",errorInfo);
break;
}
case NSStreamEventEndEncountered:
{
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
aStream = nil;
}
break;
default:
break;
}
NSLog(@"查看inputSteam状态值:%lu",inputSteam.streamStatus);
}
注意:一旦打开了流对象,它就会不断stream:handleEvent:向其委托发送消息(只要代理继续在流上放置字节(操作了读写操作)),直到它遇到流的末尾,这个消息接收一个NSStreamEvent常量作为参数,以标识事件的类型。对于NSInputStream对象,主要的事件类型包括
NSStreamEventOpenCompleted
、NSStreamEventHasBytesAvailable
和NSStreamEventEndEncountered
。
NSOutputStream
//将 buffer 中的数据写入流中,返回实际写入的字节数。
- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;
//检查流中是否还有可供写入的空间。
@property (readonly) BOOL hasSpaceAvailable;
在Cocoa中 使用NSOutputStream实例写入输出流需要几个步骤:
- 使用要写入的数据创建和初始化一个NSOutputStream实例,设置代理对象
- 在运行循环上计划流对象并打开流。
- 处理流对象报告给其委托的事件。
- 如果流对象已将数据写入内存,请通过请求
NSStreamDataWrittenToMemoryStreamKey
属性获取数据。 - 当没有更多数据要写入时,处理流对象。
创建和初始化NSOutputStream对象
//oStream是一个实例变量
//(举个例子,输出到文件)
//NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]stringByAppendingPathComponent:@"111.pcm"];
//oStream = [[NSOutputStream alloc] initToFileAtPath:path append:YES];
oStream = [[NSOutputStream alloc] initToMemory];
oStream.delegate = self;
[oStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[oStream open];
处理代理事件
case NSStreamEventHasSpaceAvailable:
{
if (!data) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"abc" ofType:@"pcm"];
data = [NSMutableData dataWithContentsOfFile:path];
}
uint8_t *readBytes = (uint8_t *)[data mutableBytes];
readBytes += byteIndex; // instance variable to move pointer
NSInteger data_len = [data length];
NSInteger len = ((data_len - byteIndex >= 1024) ?
1024 : (data_len-byteIndex));
uint8_t buf[len];
(void)memcpy(buf, readBytes, len);
len = [oStream write:(const uint8_t *)buf maxLength:len];
byteIndex += len;
break;
}
//关闭并释放NSOutputStream对象
case NSStreamEventEndEncountered:
{
NSData *newData = [oStream propertyForKey:
NSStreamDataWrittenToMemoryStreamKey];
if (!newData) {
NSLog(@"No data written to memory!");
} else {
NSLog(@"数据总长度%ld",newData.length);
}
[oStream close];
[oStream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
oStream = nil; // oStream is instance variable
}
break;
注意:输出流主要的事件类型包括
NSStreamEventOpenCompleted
、NSStreamEventHasSpaceAvailable
、NSStreamEventEndEncountered
Polling Versus Run-Loop Scheduling
流处理的潜在问题是阻塞。正在写入或读取流的线程可能无限期地等待,直到流上有空间将字节或字节放在可以读取的流上。实际上,线程受流的支配,这可能会给应用程序带来麻烦。阻塞特别是socket流的问题,因为它们依赖于来自远程主机的响应。
在Cocoa中,有两种方法来处理流事件:
Run-loop:将流对象安排在一个运行循环中,以便只有在不太可能发生阻塞时,委托才接收到报告与流相关的事件的消息。对于读写操作,相关的
NSStreamEvent
常量是NSStreamHasBytesAvailable
和NSStreamHasSpaceAvailable
。Polling。仅在流的末尾或错误时被破坏的闭环中,一直在询问流对象是否有(对于读流)可读的字节或(对于写流)可写的空间。相关的方法有hasBytesAvailable (NSInputStream)和hasSpaceAvailable (NSOutputStream)。
注意:Run-loop几乎总是优于Polling,这就是为什么“ 从输入流读取”和“ 写入输出流”中的代码示例中仅显示Run-loop的使用。通过Polling,程序将被锁定在一个紧凑的循环中,等待可能会或可能不会即将发生的流事件。通过Run-loop,程序可以启动并执行其他操作,因为它知道在有流事件需要处理时会通知它。此外,Run-loop使您不必管理状态,并且比Polling更有效。Polling也是CPU密集型的; 你可以用你的处理时间做其他事情。
下面这个是官方的代码事例,其实就是while循环一直在操作数据,然后在内部判断何时中断。这种处理方法的问题在于它会阻塞当前线程,直到流处理结束为止,才继续进行后面的操作。而这种问题在处理网络socket流时尤为严重,我们必须等待服务端数据回来后才能继续操作。因此,通常情况下,建议使用run loop方式来处理流事件。
- (void)createNewFile {
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流
在iOS中,NSStream类不支持连接到远程主机,幸运的是CFStream支持。前面已经说过这两者可以通过toll-free桥接来相互转换。使用CFStream时,我们可以调用CFStreamCreatePairWithSocketToHost函数并传递主机名和端口号,来获取一个CFReadStreamRef和一个CFWriteStreamRef来进行通信,然后我们可以将它们转换为NSInputStream和NSOutputStream对象来处理。
- (IBAction)searchForSite:(id)sender
{
NSString *urlStr = [sender stringValue];
if (![urlStr isEqualToString:@""]) {
NSURL *website = [NSURL URLWithString:urlStr];
if (!website) {
NSLog(@"%@ is not a valid URL");
return;
}
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)[website host], 80, &readStream, &writeStream);
NSInputStream *inputStream = (__bridge_transfer NSInputStream *)readStream;
NSOutputStream *outputStream = (__bridge_transfer NSOutputStream *)writeStream;
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
/* Store a reference to the input and output streams so that
they don't go away.... */
...
}
}