流式读写数据,所谓流式就是像小溪流一样细水长流避免峰值影响。它可以让你避免一次性将数据加载到内存,从而导致内存暴涨。比如播放一个本地视频文件,你不需要先将整个视频都加载到内存再播放,而是读取一段播放一段。
系统提供了两个子类NSInputStream和NSOutputStream用于流式处理。NSInputStream用于流式读取数据,NSOutputStream用于流式写入数据。
输入流,按次序从仓库读取数据。这个仓库可以是一个文件,一个NSData对象或者是一个网络socket。注意:网络socket输入流和前面两种输入流的初始化步骤是不一样的。
因为是读数据,所以仓库必须存在,比如如果仓库是一个文件,则该文件必须存在,否则会打开失败提升:
1 读错误:Error Domain =NSPOSIXErrorDomain Code =2 "No such file or directory" UserInfo={_kCFStreamErrorCodeKey =2, _kCFStreamErrorDomainKey =1}
read:maxLength:
1 2 - (NSInteger )read:(uint8_t *)buffer maxLength:(NSUInteger )len;
返回值说明:
1 2 3 4 A number indicating the outcome of the operation: A positive number indicates the number of bytes read . 0 indicates that the end of the buffer was reached .-1 means that the operation failed; more information about the error can be obtained with streamError.
一个正数表示读取了多少字节。0表示待读取的数据已经到了结尾,没有待读取的数据了。-1表示读取出错。
NSOutputStream 输出流,按次序写入数据到一个仓库。这个仓库可以是一个文件,一个C buffer,应用的内存,或者一个网络socket。注意:网络socket输出流和前面三种输出流的初始化步骤是不一样的。
如果仓库是一个文件,打开流后系统会自动创建该文件。
write:maxLength:
1 2 - (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;
返回值说明:
1 2 3 4 A number indicating the outcome of the operation: A positive number indicates the number of bytes written. 0 indicates that a fixed-length stream and has reached its capacity.-1 means that the operation failed; more information about the error can be obtained with streamError.
一个正数表示写入了多少字节。0表示待写入的数据已经到了结尾,没有待写入的数据了。-1表示写入出错。
流的事件 1 2 3 4 5 6 7 8 typedef NS_OPTIONS (NSUInteger , NSStreamEvent ) { NSStreamEventNone = 0, NSStreamEventOpenCompleted = 1UL << 0, NSStreamEventHasBytesAvailable = 1UL << 1, NSStreamEventHasSpaceAvailable = 1UL << 2, NSStreamEventErrorOccurred = 1UL << 3, NSStreamEventEndEncountered = 1UL << 4 };
NSStreamEventOpenCompleted:流打开完成。
NSStreamEventHasBytesAvailable:有数据可读取。
NSStreamEventHasSpaceAvailable:有空间可写入。
NSStreamEventErrorOccurred:读取流出现错误。
NSStreamEventEndEncountered:读取流结束。
流发生错误或者流已经结束或者流已经关闭,这个流就不能继续使用了,不能再读或再写,只能重新创建。
polling轮询 VS runloop调度 有两种方式获取流事件发生,一种是轮询,一种是事件监听。比如流式读取数据,可能这个数据的产生是随机的,你不知道什么时候会有数据可读,怎么办呢?最简单的办法就是不断地查询是否有数据,这种方式虽然有效但无疑是对CPU的极大浪费。另一种办法就是事件监听,将流绑定到一个线程的runloop,这样当有流事件发生时,代理方法就会被回调,没有事件发生时runloop会进入睡眠。
ps:虽然轮询看起来好像不太行的样子,但是如果流是确定的,使用轮询也未尝不可。
轮询示例:将数据流式写入到内存,再读取出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 - (void )test_outputStream_to_memory { NSString *filePath = [[NSBundle mainBundle] pathForResource:@"share_qq_icon" ofType:@"png" ]; NSData *imgData = [NSData dataWithContentsOfFile:filePath]; NSOutputStream *outputStream = [[NSOutputStream alloc] initToMemory]; [outputStream open]; uint8_t *buffer = (uint8_t *)[imgData bytes]; NSInteger totalLength = imgData.length; NSInteger totalWrite = 0 ; NSInteger currentWrite = 0 ; while (1 ) { if (totalWrite >= totalLength) { break ; } if ([outputStream hasSpaceAvailable]) { NSInteger writeStep = MIN(totalLength - totalWrite, 256 ); currentWrite = [outputStream write:&buffer[totalWrite] maxLength:writeStep]; if (currentWrite == -1 ) { NSLog (@"写入出错:%@" , outputStream.streamError); break ; } else { NSLog (@"写入数据长度:%ld" , currentWrite); totalWrite += currentWrite; } } } NSData *pdata = [outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey ]; [outputStream close]; UIImage *img = [UIImage imageWithData:pdata]; NSLog (@"img = %@" , img); UIImageView *imgView = [self .view viewWithTag:1002 ]; if (imgView == nil ) { imgView = [[UIImageView alloc] initWithImage:img]; imgView.tag = 1002 ; imgView.frame = CGRectMake (100 , 100 , 100 , 100 * img.size.height / img.size.width); [self .view addSubview:imgView]; } }
这里需要注意的地方有两个:
writeStep不能是一个固定值比如512,否则只要stream还有空间就会一直写,超出buffer范围后还会一直往后读出来,超出的都是垃圾数据了。writeStep到后面肯定是0。
写完一段后需要移动位置,否则每次写的都是buffer的第一段。
采用runloop调度也很简单就不展示了。将流安排到runloop里,runloop监听流事件并回调代理方法。
1 2 - (void )scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSRunLoopMode )mode; - (void )removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSRunLoopMode )mode;
早期版本AF的使用 初始化outputStream 这里初始化了一个输出流,它是将数据写入到应用的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (NSOutputStream *)outputStream { if (!_outputStream) { self .outputStream = [NSOutputStream outputStreamToMemory]; } return _outputStream; } - (void )setOutputStream:(NSOutputStream *)outputStream { [self .lock lock]; if (outputStream != _outputStream) { if (_outputStream) { [_outputStream close]; } _outputStream = outputStream; } [self .lock unlock]; }
绑定到网络线程的runloop 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - (void )operationDidStart { [self .lock lock]; if (![self isCancelled]) { self .connection = [[NSURLConnection alloc] initWithRequest:self .request delegate:self startImmediately:NO ]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; for (NSString *runLoopMode in self .runLoopModes) { [self .connection scheduleInRunLoop:runLoop forMode:runLoopMode]; [self .outputStream scheduleInRunLoop:runLoop forMode:runLoopMode]; } [self .outputStream open]; [self .connection start]; } [self .lock unlock]; dispatch_async (dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self ]; }); }
写入数据 流事件可以使用事件监听也可以直接轮询,这里是轮询hasSpaceAvailable。实际上由于每次写入的数据很少,所以下面的代码是一次就写入内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 - (void )connection:(NSURLConnection __unused *)connection didReceiveData:(NSData *)data { NSUInteger length = [data length]; while (YES ) { NSInteger totalNumberOfBytesWritten = 0 ; if ([self .outputStream hasSpaceAvailable]) { const uint8_t *dataBuffer = (uint8_t *)[data bytes]; NSInteger numberOfBytesWritten = 0 ; while (totalNumberOfBytesWritten < (NSInteger )length) { numberOfBytesWritten = [self .outputStream write:&dataBuffer[(NSUInteger )totalNumberOfBytesWritten] maxLength:(length - (NSUInteger )totalNumberOfBytesWritten)]; if (numberOfBytesWritten == -1 ) { break ; } totalNumberOfBytesWritten += numberOfBytesWritten; } break ; } else { [self .connection cancel]; if (self .outputStream.streamError) { [self performSelector:@selector (connection:didFailWithError:) withObject:self .connection withObject:self .outputStream.streamError]; } return ; } } dispatch_async (dispatch_get_main_queue(), ^{ self .totalBytesRead += (long long )length; if (self .downloadProgress) { self .downloadProgress(length, self .totalBytesRead, self .response.expectedContentLength); } }); }
读取 由于数据是写入到内存,我们最终需要将其读取出来,对于outputStreamToMemory的输出流来说,可以使用NSStreamDataWrittenToMemoryStreamKey获取。
1 2 3 4 5 6 7 8 9 10 11 12 - (void )connectionDidFinishLoading:(NSURLConnection __unused *)connection { self .responseData = [self .outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey ]; [self .outputStream close]; if (self .responseData) { self .outputStream = nil ; } self .connection = nil ; [self finish]; }
最后不要忘了关闭流。
参考 File System Programming Guide
Stream Programming Guide
Writing To Output Streams