Потоковая передача NSXMLParser с помощью NSInputStream
Обновить:
Когда используешь NSXMLParser
метод класса initWithContentsOfURL
вместо синтаксического анализа при загрузке XML-канала он пытается загрузить весь XML-файл в память и только затем запустить процесс синтаксического анализа. Это проблематично, если подача XML велика (использование чрезмерного объема ОЗУ, по своей сути неэффективно, потому что вместо анализа параллельно с загрузкой, он запускается только после завершения загрузки и т. Д.).
Кто-нибудь обнаружил, как анализировать, как поток передается на устройство с помощью NSXMLParser
? Да, вы можете использовать LibXML2
(как обсуждено ниже), но кажется, что это должно быть возможно сделать с NSXMLParser
, Но это ускользает от меня.
Оригинальный вопрос:
Я боролся с использованием NSXMLParser
читать XML из веб-потока. Если вы используете initWithContentsOfURL
в то время как интерфейс может привести к выводу о том, что он будет передавать XML-данные из Интернета, он, похоже, не делает этого, а скорее пытается сначала загрузить весь XML-файл перед выполнением любого анализа. Для XML-файлов небольшого размера это хорошо, но для действительно больших это проблематично.
Я видел обсуждение использования NSXMLParser
в сочетании с initWithStream
с некоторыми настроенными NSInputStream
это потоковое из Интернета. Например, были ответы на это, которые предлагают использовать что-то вроде CFStreamCreateBoundPair
упоминается в следующем посте Cocoa Builder и обсуждении настройки потоков сокетов в Руководстве по программированию Apple Stream, но я не получил его на работу. Я даже пытался написать свой собственный подкласс NSInputStream
что использовал NSURLConnection
(что само по себе довольно хорошо для потоковой передачи), но я не смог заставить его работать вместе с NSXMLParser
,
В итоге я решил использовать LibXML2
скорее, чем NSXMLParser
, как показано в примере Apple XMLPerformance, но мне было интересно, если кому-то повезло, если бы потоковая передача из веб-источника работала с NSXMLParser
, Я видел множество ответов "теоретически ты мог бы сделать х ", предлагая все от CFStreamCreateBoundPair
чтобы схватить HTTPBodyStream
от NSURLRequest
, но я еще не сталкивался с рабочей демонстрацией потоковой передачи с NSXMLParser
,
Статья Рэя Вендерлиха " Как выбрать лучший парсер XML для вашего iPhone", кажется, подтверждает, что NSXMLParser
не очень подходит для больших файлов XML, но со всеми постами о возможном NSXMLParser
Обходные пути для потоковой передачи действительно больших XML-файлов, я удивлен, что мне еще не удалось найти рабочую демонстрацию этого. Кто-нибудь знает о функционировании NSXMLParser
реализация, которая течет из Интернета? Ясно, я могу просто придерживаться LibXML2
или какой-то другой эквивалентный синтаксический анализатор XML, но понятие потоковой передачи с NSXMLParser
кажется близлежащим.
2 ответа
-[NSXMLParser initWithStream:]
это единственный интерфейс для NSXMLParser
который в настоящее время выполняет потоковый анализ данных. Подключение к асинхронному NSURLConnection
что предоставление данных постепенно является громоздким, потому что NSXMLParser
принимает блокировку, "тянуть" на основе подхода к чтению из NSInputStream
, То есть, -[NSXMLParser parse]
делает что-то вроде следующего при работе с NSInputStream
:
while (1) {
NSInteger length = [stream read:buffer maxLength:maxLength];
if (!length)
break;
// Parse data …
}
Для того, чтобы постепенно предоставлять данные для этого парсера пользовательский NSInputStream
нужен подкласс, который направляет данные, полученные NSURLConnectionDelegate
вызовы в фоновой очереди или runloop к -read:maxLength:
называть это NSXMLParser
ждет
Внедрение для проверки концепции:
#include <Foundation/Foundation.h>
@interface ReceivedDataStream : NSInputStream <NSURLConnectionDelegate>
@property (retain) NSURLConnection *connection;
@property (retain) NSMutableArray *bufferedData;
@property (assign, getter=isFinished) BOOL finished;
@property (retain) dispatch_semaphore_t semaphore;
@end
@implementation ReceivedDataStream
- (id)initWithContentsOfURL:(NSURL *)url
{
if (!(self = [super init]))
return nil;
NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO] autorelease];
self.connection.delegateQueue = [[[NSOperationQueue alloc] init] autorelease];
self.bufferedData = [NSMutableArray array];
self.semaphore = dispatch_semaphore_create(0);
return self;
}
- (void)dealloc
{
self.connection = nil;
self.bufferedData = nil;
self.semaphore = nil;
[super dealloc];
}
- (BOOL)hasBufferedData
{
@synchronized (self) { return self.bufferedData.count > 0; }
}
#pragma mark - NSInputStream overrides
- (void)open
{
NSLog(@"open");
[self.connection start];
}
- (void)close
{
NSLog(@"close");
[self.connection cancel];
}
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)maxLength
{
NSLog(@"read:%p maxLength:%ld", buffer, maxLength);
if (self.isFinished && !self.hasBufferedData)
return 0;
if (!self.hasBufferedData)
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSAssert(self.isFinished || self.hasBufferedData, @"Was woken without new information");
if (self.isFinished && !self.hasBufferedData)
return 0;
NSData *data = nil;
@synchronized (self) {
data = [[self.bufferedData[0] retain] autorelease];
[self.bufferedData removeObjectAtIndex:0];
if (data.length > maxLength) {
NSData *remainingData = [NSData dataWithBytes:data.bytes + maxLength length:data.length - maxLength];
[self.bufferedData insertObject:remainingData atIndex:0];
}
}
NSUInteger copiedLength = MIN([data length], maxLength);
memcpy(buffer, [data bytes], copiedLength);
return copiedLength;
}
#pragma mark - NSURLConnetionDelegate methods
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(@"connection:%@ didReceiveData:…", connection);
@synchronized (self) {
[self.bufferedData addObject:data];
}
dispatch_semaphore_signal(self.semaphore);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"connectionDidFinishLoading:%@", connection);
self.finished = YES;
dispatch_semaphore_signal(self.semaphore);
}
@end
@interface ParserDelegate : NSObject <NSXMLParserDelegate>
@end
@implementation ParserDelegate
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict
{
NSLog(@"parser:%@ didStartElement:%@ namespaceURI:%@ qualifiedName:%@ attributes:%@", parser, elementName, namespaceURI, qualifiedName, attributeDict);
}
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
NSLog(@"parserDidEndDocument:%@", parser);
CFRunLoopStop(CFRunLoopGetCurrent());
}
@end
int main(int argc, char **argv)
{
@autoreleasepool {
NSURL *url = [NSURL URLWithString:@"http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml"];
ReceivedDataStream *stream = [[ReceivedDataStream alloc] initWithContentsOfURL:url];
NSXMLParser *parser = [[NSXMLParser alloc] initWithStream:stream];
parser.delegate = [[[ParserDelegate alloc] init] autorelease];
[parser performSelector:@selector(parse) withObject:nil afterDelay:0.0];
CFRunLoopRun();
}
return 0;
}
Я заметил, что в ответе bdash используется NSURLConnection. Но согласно документации NSURLConnection
Этот API считается устаревшим. Вместо этого используйте NSURLSession.
поэтому я заменил его на NSURLSessionDataTask.
#import <Foundation/Foundation.h>
#import <objc/objc-sync.h>
@interface RemoteInputStream : NSInputStream
+ (instancetype)new NS_UNAVAILABLE;
+ (instancetype)inputStreamWithData:(NSData *)data NS_UNAVAILABLE;
+ (instancetype)inputStreamWithFileAtPath:(NSString *)path NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithData:(NSData *)data NS_UNAVAILABLE;
- (instancetype)initWithFileAtPath:(NSString *)path NS_UNAVAILABLE;
+ (instancetype)inputStreamWithRequest:(NSURLRequest *)request;
- (instancetype)initWithRequest:(NSURLRequest *)request NS_DESIGNATED_INITIALIZER;
@end
@interface RemoteInputStream () <NSURLSessionDataDelegate>
@property (retain) NSURLSessionDataTask *sessionDataTask;
@property (retain) NSMutableArray<NSData *> *bufferData;
@property (retain, nullable) dispatch_semaphore_t semaphore;
@end
@implementation RemoteInputStream
+ (instancetype)inputStreamWithRequest:(NSURLRequest *)request {
return [[[self.class alloc] initWithRequest:request] autorelease];
}
- (instancetype)initWithURL:(NSURL *)url {
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
self = [self initWithRequest:request];
[request release];
return self;
}
- (instancetype)initWithRequest:(NSURLRequest *)request {
if (self = [super initWithURL:request.URL]) {
NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration];
NSURLSessionDataTask *sessionDataTask = [session dataTaskWithRequest:request];
self.sessionDataTask = sessionDataTask;
NSMutableArray<NSData *> *bufferData = [NSMutableArray<NSData *> new];
self.bufferData = bufferData;
[bufferData release];
}
return self;
}
- (void)dealloc {
[_sessionDataTask cancel];
[_sessionDataTask release];
[_bufferData release];
if (_semaphore) {
dispatch_release(_semaphore);
}
[super dealloc];
}
- (void)open {
self.sessionDataTask.delegate = self;
[self.sessionDataTask resume];
}
- (void)close {
[self.sessionDataTask suspend];
}
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
objc_sync_enter(self);
if (self.bufferData.count == 0) {
if (self.sessionDataTask.state == NSURLSessionTaskStateRunning) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
self.semaphore = semaphore;
objc_sync_exit(self);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
objc_sync_enter(self);
self.semaphore = nil;
dispatch_release(semaphore);
if (self.bufferData.count == 0) {
objc_sync_exit(self);
return 0;
}
} else {
objc_sync_exit(self);
return 0;
}
}
NSMutableData *result = [NSMutableData new];
NSUInteger remaining = len;
while (YES) {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
BOOL shouldBreak;
if (remaining < self.bufferData[0].length) {
NSData *data1 = [self.bufferData[0] subdataWithRange:NSMakeRange(0, remaining)];
NSData *data2 = [self.bufferData[0] subdataWithRange:NSMakeRange(remaining, self.bufferData[0].length - remaining)];
[result appendData:data1];
[self.bufferData replaceObjectAtIndex:0 withObject:data2];
remaining = 0;
shouldBreak = YES;
} else {
[result appendData:self.bufferData[0]];
remaining -= self.bufferData[0].length;
[self.bufferData removeObjectAtIndex:0];
if (self.bufferData.count == 0) {
shouldBreak = YES;
} else {
shouldBreak = NO;
}
}
[pool release];
if (remaining == 0) {
shouldBreak = YES;
}
if (shouldBreak) {
break;
}
}
objc_sync_exit(self);
NSUInteger length = result.length;
memcpy(buffer, result.bytes, length);
[result release];
return length;
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
objc_sync_enter(self);
[self.bufferData addObject:data];
if (self.semaphore) {
dispatch_semaphore_signal(self.semaphore);
}
objc_sync_exit(self);
}
@end
Пример кода модульного теста:
#import <XCTest/XCTestCase.h>
@interface RemoteInputStreamTests : XCTestCase
@end
@implementation RemoteInputStreamTests
- (void)test_read {
NSURL *testURL = [NSURL URLWithString:@"https://fastly.picsum.photos/id/11/2500/1667.jpg?hmac=xxjFJtAPgshYkysU_aqx2sZir-kIOjNR9vx0te7GycQ"];
NSData *normalData = [NSData dataWithContentsOfURL:testURL];
RemoteInputStream *inputStream = [RemoteInputStream inputStreamWithURL:testURL];
[inputStream open];
NSUInteger maxLength = 16;
uint8_t *buffer = malloc(sizeof(uint8_t) * maxLength);
NSUInteger len = [inputStream read:buffer maxLength:maxLength];
NSMutableData *streamingData = [NSMutableData new];
while (len) {
[streamingData appendBytes:buffer length:len];
len = [inputStream read:buffer maxLength:maxLength];
}
free(buffer);
XCTAssertTrue([normalData isEqualToData:streamingData]);
[streamingData release];
}
@end