Making an NSMutableString transformation without leaking memory? - memory-management

I have this function within an iPhone project Objective C class.
While it's correct in terms of the desired functionality, after a few calls, it crashes into the debugger.
So I think it's a case of bad memory management, but I'm not sure where.
- (NSString *)stripHtml:(NSString *)originalText {
// remove all html tags (<.*>) from the originalText string
NSMutableString *strippedText = [[NSMutableString alloc] init];
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSString *current = [originalText substringWithRange:NSMakeRange(i, 1)];
if( [current isEqualTo:#"<"] )
appendFlag = NO;
if( appendFlag )
[strippedText appendString:current];
if( [current isEqualTo:#">"] )
appendFlag = YES;
}
NSString *newText = [NSString stringWithString:strippedText];
[strippedText release];
return newText;
}

Every time you iterate over your for loop, you're allocating a new NSString. While these NSStrings are autoreleased, they won't actually be released until after all the processing of your last input is finished. In the meantime, you'll allocate a potentially infinite amount of memory. The solution is to create your own autorelease pool and drain it every trip through the for loop. It'll look something like this:
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
// rest of for loop body
[pool drain];
}
That'll free up the memory used by your current pointer right away.

Related

How to read all remaining output of readInBackgroundAndNotify after NSTask has ended?

I'm invoking various command line tools via NSTask. The tools may run for several seconds, and output text constantly to stdout. Eventually, the tool will terminate on its own. My app reads its output asynchronously with readInBackgroundAndNotify.
If I stop processing the async output as soon as the tool has exited, I will often lose some of its output that hasn't been delivered by then.
Which means I have to wait a little longer, allowing the RunLoop to process pending read notifications. How do I tell when I've read everything the tool has written to the pipe?
This problem can be verified in the code below by removing the line with the runMode: call - then the program will print that zero lines were processed. So it appears that at the time the process has exited, there's already a notification in the queue that is waiting to be delivered, and that delivery happens thru the runMode: call.
Now, it might appear that simply calling runMode: once after the tool's exit may be enough, but my testing shows that it isn't - sometimes (with larger amounts of output data), this will still only process parts of the remaining data.
Note: A work-around such as making the invoked tool outout some end-of-text marker is not a solution I seek. I believe there must be some proper way to do this, whereby the end of the pipe stream is signalled somehow, and that's what I'm looking for in an answer.
Sample Code
The code below can be pasted into a new Xcode project's AppDelegate.m file.
When run, it invokes a tool that generates some longer output and then waits for the termination of the tool with waitUntilExit. If it would then immediately remove the outputFileHandleReadCompletionObserver, most of the tool's output would be missed. By adding the runMode: invocation for the duration of a second, all output from the tool is received - Of course, this timed loop is less than optimal.
And I would like to keep the runModal function synchronous, i.e. it shall not return before it has received all output from the tool. It does run in its own tread in my actual program, if that matters (I saw a comment from Peter Hosey warning that waitUntilExit would block the UI, but that would not be an issue in my case).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[self runTool];
}
- (void)runTool
{
// Retrieve 200 lines of text by invoking `head -n 200 /usr/share/dict/words`
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", #"200", #"/usr/share/dict/words"];
__block int lineCount = 0;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
id <NSObject> outputFileHandleReadCompletionObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait one more second so that we can process any remaining output from the tool
NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1];
while ([NSDate.date compare:endDate] == NSOrderedAscending) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
[[NSNotificationCenter defaultCenter] removeObserver:outputFileHandleReadCompletionObserver];
NSLog(#"Lines processed: %d", lineCount);
}
It's quite simple. In the observer block when data.length is 0 remove the observer and call terminate.
The code will continue after the waitUntilExit line.
- (void)runTool
{
// Retrieve 20000 lines of text by invoking `head -n 20000 /usr/share/dict/words`
const int expected = 20000;
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", [#(expected) stringValue], #"/usr/share/dict/words"];
__block int lineCount = 0;
__block bool finished = false;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadCompletionNotification object:nil];
[theTask terminate];
finished = true;
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait until all data from the pipe has been received
while (!finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(#"Lines processed: %d (should be: %d)", lineCount, expected);
}
The problem with waitUntilExit is that it doesn't always behave the way one might think. The following is mentioned in the documenation:
waitUntilExit does not guarantee that the terminationHandler
block has been fully executed before waitUntilExit returns.
It appears this is precisely the problem you are having; it's a race condition. The waitUntilExit is not waiting long enough and the lineCount variable is reached before the NSTask completes. The solution would likely be to use a semaphore or dispatch_group, although it's unclear if you want to go that route — this is not an easy problem to resolve it seems.
*I experienced a similar issue from months back that still isn't resolved unfortunately.

Using NSGlyph and memory allocation

in a method to track line breaks frequently, for a NSTextView visibleRect, i am allocating memory for NSGlyph to use NSLayoutManager getGlyphs:range:.
should/can i find out how much memory this should be since i have a reference for the range (without affecting layout), and also, what kind of cleanup should happen -- running with ARC ?
the code (which runs on a main queue) :
NSLayoutManager *lm = [self.textView layoutManager];
NSTextContainer *tc = [self.textView textContainer];
NSRect vRect = [self.textView visibleRect];
NSRange visibleRange = [lm glyphRangeForBoundingRectWithoutAdditionalLayout:vRect inTextContainer:tc];
NSUInteger vRangeLoc = visibleRange.location;
NSUInteger numberOfLines;
NSUInteger index;
NSGlyph glyphArray[5000]; // <--- memory assigned here
NSUInteger numberOfGlyphs = [lm getGlyphs:glyphArray range:visibleRange];
NSRange lineRange;
NSMutableIndexSet *idxset = [NSMutableIndexSet indexSet];
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++) {
(void)[lm lineFragmentRectForGlyphAtIndex:index effectiveRange:&lineRange withoutAdditionalLayout:YES];
[idxset addIndex:lineRange.location + vRangeLoc];
index = NSMaxRange(lineRange);
}
self.currentLinesIndexSet = idxset;
With the NSGlyph glyphs[5000] notation, you're allocating the memory on the stack. But instead of 5000 glyphs it only has to hold visibleRange.length + 1 glyphs:
glyphArray
On output, the displayable glyphs from glyphRange,
null-terminated. Does not include in the result any NSNullGlyph or
other glyphs that are not shown. The memory passed in should be large
enough for at least glyphRange.length+1 elements.
And because it is on the stack, you don't have to worry about freeing the memory—because never malloced memory; it is freed automatically when leaving the function—even without ARC
So it should work, if you write it like this:
NSLayoutManager *lm = ...
NSRange glyphRange = ...
NSGlyph glyphArray[glyphRange.length + 1];
NSUInteger numberOfGlyphs = [lm getGlyphs:glyphArray range:glyphRange];
// do something with your glyphs

Problem with NSNumber and plotting a graph with Core-Plot

I try to plot a Bar Chart with Core-Plot with an Array (content are NSIntegers) given one view before.
After transfering the Array in an NSInteger, i must convert it into a NSDecimalNumber, and in this process, my NSInteger (for example 45) becomes "60900224"...
Here's the code extract:
-(NSNumber *)numberForPlot:(CPPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index
{ NSInteger *values = [Werte objectAtIndex:index];
NSDecimalNumber *num = nil;
if ( [plot isKindOfClass:[CPBarPlot class]] ) {
switch ( fieldEnum ) {
case CPBarPlotFieldBarLocation:
num = (NSDecimalNumber *)[NSDecimalNumber numberWithUnsignedInteger:index];
break;
case CPBarPlotFieldBarLength:
//num = (NSDecimalNumber *)[NSDecimalNumber numberWithUnsignedInteger:(index+1)*(index+1)];
num = [NSNumber numberWithInt:values];
if ( [plot.identifier isEqual:#"Bar Plot 2"] )
num = [num decimalNumberBySubtracting:[NSDecimalNumber decimalNumberWithString:#"10"]];
break;
}
}
return num;
}
Thanks for help!!
NSInteger is not an object type and can't be stored in an NSArray (which your Werte appears to be). You seem to be implicitly converting from a pointer to an integer.
Instead, you should always put NSNumber objects into the array, and then get NSInteger values out of those via integerValue:
NSInteger value = [[Werte objectAtIndex:index] integerValue];

NSArray filled with bool objects

I have an NSArray filled with bools (expressed as a number), and I need to test to see if any object within the array is equal to 1. How can I do it?
BOOLs are not objects. Assuming you mean some object representing a boolean like NSNumber that implements a proper isEqual:, you could just do something like [array containsObject:[NSNumber numberWithBool:YES]].
As Chuck says, use -[NSArray containsObject:[NSNumber numberWithBool:YES]]. As a thought experiment, here are some other ways to accomplish the goal...
You can do this using an NSPredicate or using the new blocks API:
NSArray *myArr //decleared, initialized and filled
BOOL anyTrue = [myArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"boolValue == 1"]].count > 0;
or
BOOL anyTrue = [myArray indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop) {
if([obj boolValue]) {
*stop = YES;
}
return [obj boolValue];
}].count > 0;
You can also use Key-Value coding, though I'm not sure of its relative efficiency:
[[myArray valueForKeyPath:#"#sum.boolValue"] integerValue] > 0;

cocoa: NSString not removing all the characters

I have an int and for some reason it isn't working after 16 or so. Here's my code:
NSArray *sortedArray;
sortedArray = [doesntContainAnother sortedArrayUsingFunction:firstNumSort context:NULL];
int count2 = [sortedArray count];
//NSLog(#"%d", count2);
int z = 0;
while (z < count2) {
NSString *myString = [sortedArray objectAtIndex:z];
NSString *intstring = [NSString stringWithFormat:#"%d", z];
NSString *stringWithoutSpaces;
stringWithoutSpaces = [[myString stringByReplacingOccurrencesOfString:intstring
withString:#""] mutableCopy];
[hopefulfinal addObject:stringWithoutSpaces];
NSLog(#"%#", [hopefulfinal objectAtIndex:z]);
z++;
}
Edit: It's not the int, it's the stringWithoutSpaces line... I can't figure out what's causing it.
So it (the NSLog, see above the z++) looks like this:
"Here"
"whatever"
"17 whatevere"
"18 this"
etc.
I'm guessing this is related to your earlier question Sort NSArray’s by an int contained in the array, and that you're trying to strip the leading number and whitespace from an array that looks like the one you had in that question:
"0 Here is an object"
"1 What the heck, here's another!"
"2 Let's put 2 here too!"
"3 Let's put this one right here"
"4 Here's another object"
Without know the full input, I'd guess that your code is likely failing because the leading numbers and the value of z are getting out of sync. Since you don't seem to actually care what the leading number is and just want to vamoose it, I'd recommend a different approach that scans for leading digits and extracts the substring from the position where those digits end:
NSArray *array = [NSArray arrayWithObjects:#"1 One",
#"2 Two",
#"5 Five",
#"17 Seventeen",
nil];
NSMutableArray *results = [NSMutableArray array];
NSScanner *scanner;
NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet];
for (NSString *item in array) {
scanner = [NSScanner scannerWithString:item];
[scanner scanInteger:NULL]; // throwing away the BOOL return value...
// if string does not start with a number,
// the scanLocation will be 0, which is good.
[results addObject:[[item substringFromIndex:[scanner scanLocation]]
stringByTrimmingCharactersInSet:whitespace]];
}
NSLog(#"Resulting array is: %#", results);
// Resulting array is: (
// One,
// Two,
// Five,
// Seventeen
// )
)

Resources