NSScanner scanUpToString leaking while using ARC

1.7k views Asked by At

To parse part of the querystring of an URL I use this method :

NSScanner *scanner = [[NSScanner alloc] initWithString:query];
        [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"&?"]];

        NSString *parameterString = [NSString new];
        while ([scanner scanUpToString:ampersand intoString:&parameterString]) 
        {
            NSScanner *parameterScanner = [[NSScanner alloc] initWithString:parameterString];

            NSString *name = [NSString new];
            [parameterScanner scanUpToString:isEqual intoString:&name];

            NSString *value = [parameterString substringFromIndex:([name length] + 1)];
            [parameters setObject:value forKey:name];


        }

In this project I am using ARC, but still the method is leaking at this line:

[parameterScanner scanUpToString:isEqual intoString:&name];

What exactly is leaking and how do I solve this?

4

There are 4 answers

1
rckoenes On

There is no reason to initialize the name variable

    NSScanner *scanner = [[NSScanner alloc] initWithString:query];
    [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"&?"]];
    NSString *parameterString = [NSString new];
    while ([scanner scanUpToString:ampersand intoString:&parameterString]) 
    {
        NSScanner *parameterScanner = [[NSScanner alloc] initWithString:parameterString];

        NSString *name = nil;
        [parameterScanner scanUpToString:isEqual intoString:&name];

        NSString *value = [parameterString substringFromIndex:([name length] + 1)];
        [parameters setObject:value forKey:name];


    }
2
extremeboredom On

I suspect that name is not actually leaking, it is simply not being released when you think it is. Under ARC, I believe that scanUpToString:intoString: would be defined similarly to methods using NSError. In other words, it takes NSString * __autoreleasing *. Therefore, whatever value is passed to it is actually autoreleased, and won't be released until the current autorelease pool is drained. Assuming you don't have any others dotted around, that will be when the run loop goes around again. If that memory usage is a problem to you, it would be possible to place an explicit autorelease pool around the loop, so the objects go away immediately:

NSScanner *scanner = [[NSScanner alloc] initWithString:query];
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"&?"]];

@autoreleasepool
{
    NSString *parameterString = [NSString new];
    while ([scanner scanUpToString:ampersand intoString:&parameterString]) 
    {
        NSScanner *parameterScanner = [[NSScanner alloc] initWithString:parameterString];

        NSString *name = [NSString new];
        [parameterScanner scanUpToString:isEqual intoString:&name];

        NSString *value = [parameterString substringFromIndex:([name length] + 1)];
        [parameters setObject:value forKey:name];


    }
}

That is probably unnecessary though, and the run loop will clear up the objects anyway.

That said, there is still a small issue that means the compiler is creating an additional temporary variable for you. Your name variable is implicitly __strong, so the compiler inserts a temporary variable that is __autoreleasing and copies the values around for you. You can avoid this by explicitly declaring the NSString as autoreleasing. You also do not need to init it, as rckoeness said, because the scanUpToString:intoString: is doing that for you (which is why it has to be __autoreleasing in the first place). (See http://developer.apple.com/library/mac/ipad/#releasenotes/ObjectiveC/RN-TransitioningToARC/_index.html for more info).

So, overall I think you actually want your code to look like this:

NSScanner *scanner = [[NSScanner alloc] initWithString:query];
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"&?"]];

NSString __autoreleasing *parameterString = nil;
while ([scanner scanUpToString:ampersand intoString:&parameterString]) 
{
    NSScanner *parameterScanner = [[NSScanner alloc] initWithString:parameterString];

    NSString __autoreleasing *name = nil;
    [parameterScanner scanUpToString:isEqual intoString:&name];

    NSString *value = [parameterString substringFromIndex:([name length] + 1)];
    [parameters setObject:value forKey:name];
}

Hope that helps!


I've had another thought, perhaps name is simply a red herring. Leaks will show your where the allocation happened, but name continues to exist beyond this loop, when it is added to parameters. I presume that it is an NSMutableDictionary or similar, based on the selector. If I were you I would confirm that the name instances are not being leaked because that dictionary (or something that later reads those keys from the dictionary) is being leaked.

1
AudioBubble On

what about using autoreleased initializer?

// [NSScanner scannerWithString:]
// and
// [NSString string]

NSScanner *scanner = [NSScanner scannerWithString:query];
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"&?"]];

NSString *parameterString = [NSString string];
while ([scanner scanUpToString:ampersand intoString:&parameterString]) 
{
    NSScanner *parameterScanner = [NSScanner scannerWithString:parameterString];

    NSString *name = [NSString string];
    [parameterScanner scanUpToString:isEqual intoString:&name];

    NSString *value = [parameterString substringFromIndex:([name length] + 1)];
    [parameters setObject:value forKey:name];
}
0
Kevin Lewis On

I've just been dealing with the exact same issue. My solution was to reset the NSScanner Object based on the number of iterations applied to it. In other words, every time my test for completeness of the scanner ran, I incremented a value and then based on that value recreated the scanner object and applied the current locations from the previous scanner to the new scanner. I also have an @autoreleasepool mark placed every time I create a new version of the scanner.

The reason I approached it this way, was because NSScanner is just a memory hog and won't release until it's done in a loop. I only verified this via activity viewer and not any tools. (I tested in a Mac OS X Application Setting)

Enjoy!

NSUInteger currentLocation = 0;
while (currentLocation < [dehyphenatedText length])
{
@autoreleasepool
{
    NSUInteger iterations = 0;
    NSScanner * scanner = [NSScanner scannerWithString:dehyphenatedText];
    [scanner setCharactersToBeSkipped: nil];
    [scanner setScanLocation: currentLocation];
    while (([scanner scanLocation] < [dehyphenatedText length]) && (iterations < 15000))
    {
    NSString * found=nil;
    [scanner scanCharactersFromSet:inverted intoString:&found];
    if ((found != nil) && ([found length] > 0))
    {
               // Some code to process the results
            }
        found = nil;
        if ([scanner scanLocation] < [dehyphenatedText length])
        {
           [scanner scanCharactersFromSet: whiteSpaceAndMore intoString:nil];
        }

        iterations ++;
    }
    }
currentLocation = [scanner scanLocation];
 }