0%

iOS输入输出流

流式读写数据,所谓流式就是像小溪流一样细水长流避免峰值影响。它可以让你避免一次性将数据加载到内存,从而导致内存暴涨。比如播放一个本地视频文件,你不需要先将整个视频都加载到内存再播放,而是读取一段播放一段。

系统提供了两个子类NSInputStream和NSOutputStream用于流式处理。NSInputStream用于流式读取数据,NSOutputStream用于流式写入数据。

NSInputStream

输入流,按次序从仓库读取数据。这个仓库可以是一个文件,一个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;
// reads up to length bytes into the supplied buffer, which must be at least of size len. Returns the actual number of bytes read.

返回值说明:

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;
// writes the bytes from the specified buffer to the stream up to len bytes. Returns the number of bytes actually written.

返回值说明:

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]) {
//一次性写完
// currWriteLen = [outputStream write:&buffer[totalWrite] maxLength:totalLen - totalWrite];
//writeStep不能是一个固定值比如512,否则只要stream还有空间就会一直写,超出buffer范围后还会一直往后读出来,超出的都是垃圾数据了。
NSInteger writeStep = MIN(totalLength - totalWrite, 256);
// currWriteLen = [outputStream write:buffer maxLength:writeStep]; //错误。每次写的都是buffer的第一段
//写完一段后需要移动位置,否则每次写的都是buffer的第一段。
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];
}
}

这里需要注意的地方有两个:

  1. writeStep不能是一个固定值比如512,否则只要stream还有空间就会一直写,超出buffer范围后还会一直往后读出来,超出的都是垃圾数据了。writeStep到后面肯定是0。
  2. 写完一段后需要移动位置,否则每次写的都是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

觉得文章有帮助可以打赏一下哦!