NSInputStream和NSOutputStream的分析

NSStream

流提供了一种简单的方式在不同和介质中交换数据,这种交换方式是与设备无关的。流是在通信路径中串行传输的连续的比特位序列。从编码的角度来看,流是单向的,因此流可以是输入流或输出流。除了基于文件的流外,其它形式的流都是不可查找的,这些流的数据一旦消耗完后,就无法从流对象中再次获取。

在Cocoa中包含三个与流相关的类:NSStream、NSInputStream和NSOutputStream。NSStream是一个抽象基类,定义了所有流对象的基础接口和属性。NSInputStream和NSOutputStream继承自NSStream,实现了输入流和输出流的默认行为。从下图中可以看出,NSInputStream可以从文件、socket和NSData对象中获取数据;NSOutputStream可以将数据写入文件、socket、内存缓存和NSData对象中

NSInputStream和NSOutputStream的分析_第1张图片
官方图片.png

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对象,主要的事件类型包括NSStreamEventOpenCompletedNSStreamEventHasBytesAvailableNSStreamEventEndEncountered


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;

注意:输出流主要的事件类型包括NSStreamEventOpenCompletedNSStreamEventHasSpaceAvailableNSStreamEventEndEncountered


Polling Versus Run-Loop Scheduling

流处理的潜在问题是阻塞。正在写入或读取流的线程可能无限期地等待,直到流上有空间将字节或字节放在可以读取的流上。实际上,线程受流的支配,这可能会给应用程序带来麻烦。阻塞特别是socket流的问题,因为它们依赖于来自远程主机的响应。

在Cocoa中,有两种方法来处理流事件:

  • Run-loop:将流对象安排在一个运行循环中,以便只有在不太可能发生阻塞时,委托才接收到报告与流相关的事件的消息。对于读写操作,相关的NSStreamEvent常量是NSStreamHasBytesAvailableNSStreamHasSpaceAvailable

  • 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.... */
        ...
    }
}

你可能感兴趣的:(NSInputStream和NSOutputStream的分析)