I need to download and parse 40mb of json. Right now I'm using AFJSONRequestOperation, which on older devices causes memory warning and crash. How it should be done? I think the only way to do it correctly, is to stream json but I've got no idea how to do it or which library will be best. Please provide examples. Thanks a lot!
For anybody who has the same problem, here's how I solved it:
1. Download JSON file to local storage using AFHTTPRequestOperation's output stream.
2. Parse little chunks of NSData using YAJLParser.
Result: I was testing it on 50mb json on iPad (1), without any memory warnings (memory around 10mb).
Example:
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:path
options:NSDataReadingMappedAlways | NSDataReadingUncached
error:&error];
YAJLParser *parser = [[YAJLParser alloc] initWithParserOptions:YAJLParserOptionsAllowComments];
parser.delegate = self;
[parser parse:data];
parser.delegate = nil;
parser = nil;
YAJLParser delegate:
// first declare in header file NSMutableArray *stack and NSString *mapKey
- (void)parserDidStartDictionary:(YAJLParser *)parser
{
NSString *dictName = mapKey;
if (mapKey == nil)
{
dictName = (stack.count == 0) ? #"" : [stack lastObject];
}
[stack addObject:(dictName)];
}
- (void)parserDidEndDictionary:(YAJLParser *)parser
{
mapKey = nil;
[stack removeLastObject];
}
- (void)parserDidStartArray:(YAJLParser *)parser
{
NSString *arrayName = mapKey;
if (mapKey == nil)
{
arrayName = stack.count == 0 ? #"" : [stack lastObject];
}
[stack addObject:(arrayName)];
if([mapKey isEqualToString:#"something"])
{
// do something
}
}
- (void)parserDidEndArray:(YAJLParser *)parser
{
if([mapKey isEqualToString:#"some1"])
{
// do something
}
mapKey = nil;
[stack removeLastObject];
}
- (void)parser:(YAJLParser *)parser didMapKey:(NSString *)key
{
mapKey = key;
}
- (void)parser:(YAJLParser *)parser didAdd:(id)value
{
if([mapKey isEqualToString:#"id"])
{
// do something
}
}
Write your data to a file, then use NSData's dataWithContentsOfFile:options:error: and specify the NSDataReadingMappedAlways and NSDataReadingUncached flags. This will tell the system to use mmap() to reduce the memory footprint, and not to burden the file system cache with blocks of memory (that makes it slower, but much less of a burden to iOS).
You can find the answer here
iPad - Parsing an extremely huge json - File (between 50 and 100 mb)
Related
We want to import a huge XML-file (13MB) to Core Data. At the moment, the XML-File includes around 64000 entries, but this number will increase in future.
XML-Structure:
<entry name='...' doctype='' last-modified='...' [some more attributes] />
After a lot of research which included the XMLSchema Sample Project, Ray Wenderlich XML Tutorial and some stackoverflow entries, we didn't found a solution yet.
We first download the XML-File, and afterwards start parsing and insert the data to CoreData
Here is our implementation:
- (void)importXMLFile:(NSString*)fileName {
NSInputStream* theStream = [[NSInputStream alloc] initWithFileAtPath:fileName];
_theParser = [[NSXMLParser alloc] initWithStream:theStream];
_theParser.delegate = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[_theParser parse];
});
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
if ([elementName isEqualToString:#"entry"]) {
Importer* __weak weakSelf = self;
NSManagedObjectContext* theContext = self.importContext;
[theContext performBlock:^{
CustomObject* mo;
// Create ManagedObject
// Read values from parsed XML element
dispatch_async(dispatch_get_main_queue(), ^{
// Call a handler, just for information "added object"
});
NSError *error = nil;
if ([theContext hasChanges] && ![theContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
} else {
DLOGError(error);
}
}];
}
}
Using this methods, memory usage explodes leading to a crash. The XML file seems to be parsed completely before even one block is being processed by Core Data. So the question is:
Is it possible to process parts of the XML file (f.e. 30 entries a time), than save to CoreData and after that continue parsing?
Or more commonly asked: How can memory usage be optimized?
You want to use a stream based parser so you don't need to load the whole XML to memory at the same time. Perhaps this or something from github.
You should also batch your save operation. Don't save every individual object, save groups of perhaps 100 objects. If this is inside a tight loop you should have an autorelease pool.
guess our memory problem occurred with a line we didn't publish, while creating our ManagedObject. We had to free the xmlChar
Instead of
xmlChar *xmlString = xmlTextReaderGetAttribute(reader, (xmlChar*)"someAttribute");
NSString *someAttributeToString = [NSString stringWithUTF8String:(const char *)xmlString];
we used
xmlChar * nameString = xmlTextReaderGetAttribute(reader, (xmlChar*)"someAttribute");
if (attributeString)
{
[elementDict setValue:[NSString stringWithUTF8String:(const char*)attributeString] forKey:#"someAttribute"];
xmlFree(nameString);
}
And we pause our parser after parsing 100elements and wait, till those elements are written to CoreData. After that, we parse the next 100 bundle
Parser
// Start the data parse
- (void) parse {
_dictionaryQeue = [NSMutableArray new];
xmlTextReaderPtr reader = xmlReaderForMemory([data bytes], [data length], NULL, NULL,
(XML_PARSE_NOBLANKS | XML_PARSE_NOCDATA | XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
if (!reader) {
NSLog(#"Failed to create xmlTextReader");
return;
}
while (xmlTextReaderRead(reader)) {
#autoreleasepool {
while (_isPaused) {
//[NSThread sleepForTimeInterval:0.1];
}
switch (xmlTextReaderNodeType(reader)) {
case XML_READER_TYPE_ELEMENT: {
NSMutableDictionary* elementDict = [NSMutableDictionary new];
//Create Object
xmlChar * nameString = xmlTextReaderGetAttribute(reader, (xmlChar*)"name");
if (nameString)
{
[elementDict setValue:[NSString stringWithUTF8String:(const char*)nameString] forKey:#"name"];
xmlFree(nameString);
}
//...
if (self.collectDictionaries) {
[_dictionaryQeue addObject:elementDict];
NSArray* dictArray = [NSArray arrayWithArray:_dictionaryQeue];
if ([dictArray count] == self.maxCollectedDictionaries) {
dispatch_async(dispatch_get_main_queue(), ^{
if (saxDelegate && [(NSObject*)saxDelegate respondsToSelector:#selector(SAXDictionaryElements:finished:)]) {
[saxDelegate SAXDictionaryElements:dictArray finished:FALSE];
}
});
[_dictionaryQeue removeAllObjects];
_isPaused = TRUE;
}
}
elementDict = nil;
}
break;
case XML_READER_TYPE_END_ELEMENT: {
DLOGcomment(#"XML_READER_TYPE_END_ELEMENT");
if (self.collectDictionaries) {
NSArray* dictArray = [NSArray arrayWithArray:_dictionaryQeue];
if ([dictArray count] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
if (saxDelegate && [(NSObject*)saxDelegate respondsToSelector:#selector(SAXDictionaryElements:finished:)]) {
[saxDelegate SAXDictionaryElements:dictArray finished:TRUE];
}
});
data = nil;
[_dictionaryQeue removeAllObjects];
_dictionaryQeue = nil;
}
}
}
break;
}
}
}
xmlTextReaderClose(reader);
xmlFreeTextReader(reader);
reader = NULL;
}
DOM based parsers are quite convenient (TBXML, TouchXML, KissXML, TinyXML, GDataXML, RaptureXML, etc) especially those with XPATH support. But, memory becomes an issue as a DOM is created.
I am phasing the same memory constrains, so I started looking at wrappers for the Libxml2 XmlTextReader and so far I only found one IGXMLReader
IGXMLReader parses an XML document similar to the way a cursor would
move. The Reader is given an XML document, and return a node (an
IGXMLReader object) to each calls to nextObject.
Example,
IGXMLReader* reader = [[IGXMLReader alloc] initWithXMLString:#"<x xmlns:edi='http://ecommerce.example.org/schema'>\
<edi:foo>hello</edi:foo>\
</x>"];
for (IGXMLReader* node in reader) {
NSLog(#"node name: %#", node.name);
}
This is a different approach to that of the NSXMLParser.
How to detect total available/free disk space on the iPhone/iPad device?
I assume that clicking that "done" button would invoke cancelling the picker; in which case you can try did you try implementing the delegate method:
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[self dismissViewControllerAnimated:YES completion:^(void) {
NSLog(#"Cancelled pick");
};
}
Also it looks like you're calling release -- any reason you're not using ARC?
If I had to guess, I'd say perhaps the alert dismiss is in fact calling something like
[imagePicker.delegate imagePickerControllerDidCancel:imagePicker];
but it's already released so you're seeing this "lock-up" issue. Maybe step through in the debugger and make sure those objects still exist.
Edit:
While it doesn't solve your original issue, you could do a check for available space prior to launching the image picker, possibly with something like this so post suggests:
- (uint64_t)freeDiskspace
{
uint64_t totalSpace = 0;
uint64_t totalFreeSpace = 0;
__autoreleasing NSError *error = nil;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error: &error];
if (dictionary) {
NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize];
NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize];
totalSpace = [fileSystemSizeInBytes unsignedLongLongValue];
totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue];
NSLog(#"Memory Capacity of %llu MiB with %llu MiB Free memory available.", ((totalSpace/1024ll)/1024ll), ((totalFreeSpace/1024ll)/1024ll));
} else {
NSLog(#"Error Obtaining System Memory Info: Domain = %#, Code = %d", [error domain], [error code]);
}
return totalFreeSpace;
}
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
NSMutableArray addObject not working
I'm making an iPhone app, and so far, I'm receiving data from my server, creating objects using that data and filling an array with those objects.
My data is in XML format, and its saved into a string, which is transformed into a NSData object, like this:
NSMutableURLRequest *myRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"https://my.URL/data.php"]];
[myRequest setHTTPMethod:#"POST"];
NSURLResponse *response;
NSError *error = NULL;
NSData *myReturn = [NSURLConnection sendSynchronousRequest:myRequest returningResponse:&response error:&error];
NSString *returnString = [[NSString alloc] initWithData:myReturn encoding:NSASCIIStringEncoding];
NSData *tempData = [return dataUsingEncoding:NSUTF8StringEncoding];
After that, I do the standard objective-C XML event parsing, but I don't create anything until the following method:
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if ([elementName isEqual:#"id"])
{
hailID = currentValue;
return;
}
if ([elementName isEqual:#"timestamp"])
{
timeStamp = currentValue;
return;
}
if ([elementName isEqual: #"lat"])
{
hailLat = currentValue;
return;
}
if ([elementName isEqual:#"lng"])
{
hailLng = currentValue;
return;
}
if ([elementName isEqual:#"address"])
{
address = currentValue;
return;
}
if ([elementName isEqual:#"serviceType"])
{
serviceType = currentValue;
return;
}
if ([elementName isEqual: #"hail"])
{
Hail *newHail = [[Hail alloc]init];
newHail.hailID = hailID;
newHail.hailLat = hailLat;
newHail.hailLng = hailLng;
newHail.address = address;
newHail.timeStamp = timeStamp;
newHail.serviceType = serviceType;
[hails addObject:newHail];
NSLog(#"%u", [hails count]);
return;
}
}
hails is declared in the header file, and it's just a NSMutableArray with a capacity of 10000 items. The Hail object is a separate class. The NSLog always returns 0, even though I know that the XML code itself is valid, and the hail object exists.
Any ideas on why the hails array is always zero?
Edit: Sorry, I forgot to mention that the hails array is initialized in the -(void)viewDidLoad method as this:
hails = [[NSMutableArray alloc]initWithCapacity:10000];
Your hail is always nil because you never allocated + initialized it. You don't even mention its initialization into your code
I'm trying to extract the weather information from here using Xpath on the iPhone. As of now it parses all the data but I'm stuck on how to extract the content and display it in a table.
This is what I have so far:
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[ #"http://aviationweather.gov/adds/metars/?station_ids=1234&std_trans=translated&chk_metars=on&hoursStr=most+recent+only&submitmet=Submit"stringByReplacingOccurrencesOfString:#"1234" withString:self.title]]];
TFHpple * doc = [[TFHpple alloc] initWithHTMLData:data];
NSArray * elements = [doc searchWithXPathQuery:#"//table[1]//tr"];
NSLog(#"%#", elements);
TFHppleElement * element = [elements objectAtIndex:0];
[element content]; // Tag's innerHTML
[element tagName]; // "a"
[element attributes]; // NSDictionary of href, class, id, etc.
[element objectForKey:#"href"]; // Easy access to single attribute
If anybody needs to see what its outputting so far, let me know.
Thanks,
Andrew
I had the same issue I got to the point your at and didn't no where to go but I end up implementing this code. Hope it helps there is still little bits need to make it work correctly but do to the nature of the app I have developed this is all I can give you. its not much more its just the actual implementation into your code that you need really.
#import "XPathQuery.h"
NSMutableArray *weatherArray = [[NSMutableArray arrayWithArray:0]retain]; // Initilize the NSMutableArray can also be done with just an NSArray but you will have to change the populateArray method.
NSString *xPathLookupQuery = #"//table[1]//tr"; // Path in xml
nodes = PerformXMLXPathQuery(data, xPathLookupQuery); // Pass the data in that you need to search through
[self populateArray:weatherArray fromNodes:nodes]; // To populate multiple values into array.
session = [[self fetchContent:nodes] retain]; // To populate a single value and returns value.
- (void)populateArray:(NSMutableArray *)array fromNodes:(NSArray *)nodes
{
for (NSDictionary *node in nodes) {
for (id key in node) {
if ([key isEqualToString:#"nodeContent"]) {
[array addObject:[node objectForKey:key]];
}
}
}
}
You only need either the above code or below code unless you want both.
- (NSString *)fetchContent:(NSArray *)nodes
{
NSString *result = #"";
for (NSDictionary *node in nodes) {
for (id key in node) {
if([key isEqualToString:#"nodeContent"]) {
result = [node objectForKey:key];
}
}
}
return result;
}
I am playing around with Dave DeLong's excellent CHCSVParser for Objective-C with an extremely long .CSV file and am running into some trouble using it. I would use the arrayWithContentsOfCSVFile method, but I'm running the code on an iPhone and parsing the whole file into memory would take more memory than is available.
In my code below, the parser opens the document and calls the delegate methods perfectly, but where in the delegate do I stop after each line and access the data (to create and save a Core Data object to the data store)? I assume that would be in - (void) parser:(CHCSVParser *)parser didEndLine:(NSUInteger)lineNumber, but how do I get an NSArray (or whatever) of the data from the parser when it's done with the line?
Here is my code so far:
//
// The code from a method in my view controller:
//
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSFileManager *manager = [NSFileManager defaultManager];
NSError *err = nil;
NSArray *fileList = [manager contentsOfDirectoryAtPath:documentsDirectory error:&err];
NSString *fileName = [fileList objectAtIndex:1];
NSURL *inputFileURL = [NSURL fileURLWithPath: [documentsDirectory stringByAppendingPathComponent:fileName]];
NSStringEncoding encoding = 0;
CHCSVParser *p = [[CHCSVParser alloc] initWithContentsOfCSVFile:[inputFileURL path] usedEncoding:&encoding error:nil];
[p setParserDelegate:self];
[p parse];
[p release];
...
#pragma mark -
#pragma mark CHCSVParserDelegate methods
- (void) parser:(CHCSVParser *)parser didStartDocument:(NSString *)csvFile {
NSLog(#"Parser started!");
}
- (void) parser:(CHCSVParser *)parser didStartLine:(NSUInteger)lineNumber {
//NSLog(#"Parser started line: %i", lineNumber);
}
- (void) parser:(CHCSVParser *)parser didEndLine:(NSUInteger)lineNumber {
NSLog(#"Parser ended line: %i", lineNumber);
}
- (void) parser:(CHCSVParser *)parser didReadField:(NSString *)field {
//NSLog(#"Parser didReadField: %#", field);
}
- (void) parser:(CHCSVParser *)parser didEndDocument:(NSString *)csvFile {
NSLog(#"Parser ended document: %#", csvFile);
}
- (void) parser:(CHCSVParser *)parser didFailWithError:(NSError *)error {
NSLog(#"Parser failed with error: %# %#", [error localizedDescription], [error userInfo]);
}
Thanks!
I'm glad to see that my code is proving useful! :)
CHCSVParser is similar in behavior to an NSXMLParser, in that every time it finds something interesting, it's going to let you know via one of the delegate callbacks. However, if you choose to ignore the data that it gives you in the callback, then it's gone. These parsers (CHCSVParser and NSXMLParser) are pretty stupid. They just know the format of the stuff they're trying to parse, but don't really do much beyond that.
So the answer, in a nutshell, is "you have to save it yourself". If you look at the code for the NSArray category, you'll see in the .m file that it's using a simple NSObject subclass as the parser delegate, and that subclass is what's aggregating the fields into an array, and then adding that array to the overall array. You'll need to do something similar.
Example delegate:
#interface CSVParserDelegate : NSObject <CHCSVParserDelegate> {
NSMutableArray * currentRow;
}
#end
#implementation CSVParserDelegate
- (void) parser:(CHCSVParser *)parser didStartLine:(NSUInteger)lineNumber {
currentRow = [[NSMutableArray alloc] init];
}
- (void) parser:(CHCSVParser *)parser didReadField:(NSString *)field {
[currentRow addObject:field];
}
- (void) parser:(CHCSVParser *)parser didEndLine:(NSUInteger)lineNumber {
NSLog(#"finished line! %#", currentRow);
[self doSomethingWithLine:currentRow];
[currentRow release], currentRow = nil;
}
#end
However, I could be convinced to modify the behavior of the parser to aggregate the row itself, but if I go down that route, why not just have the parser aggregate the entire file? (Answer: it shouldn't)
I tried using this today, based on #DaveDeLong's excellent answer and code, but I think the software has been revised since his (2010) answer. At the time of writing, I found I had to use this:
#interface CSVParserDelegate : NSObject <CHCSVParserDelegate> {
NSMutableArray * currentRow;
}
#end
#implementation CSVParserDelegate
- (void) parser:(CHCSVParser *)parser didBeginLine:(NSUInteger)lineNumber {
currentRow = [[NSMutableArray alloc] init];
}
- (void) parser:(CHCSVParser *)parser didReadField:(NSString *)field atIndex:(NSInteger)fieldIndex {
[currentRow addObject:field];
}
- (void) parser:(CHCSVParser *)parser didEndLine:(NSUInteger)lineNumber {
NSLog(#"finished line! %#", currentRow);
[self doSomethingWithLine:currentRow];
[currentRow release], currentRow = nil;
}
#end
i.e., parser:didStartLine:lineNumber: has become parser:didBeginLine:lineNumber: and parser:didReadField: has become parser:didReadField:atIndex:.
To use CHCSVParser with Swift you can use a swift wrapper for basic needs