Reputation: 509
I have an NSMutableArray object called logBuffer, which holds log information and dumps it in a file every N lines. When that happens, I remove all its entries, with
[logBuffer removeAllObjects]
Sometimes this throws an exception:
[__NSArrayM removeObjectAtIndex:]: index 7 beyond bounds [0 .. 6]
I guess removeAllObjects internally iterates through all objects in the array, but I am not sure how it can go beyond its bounds. My only though is that there is another thread that manipulates the array while the objects are being removed, but I am not sure at all.
Any thoughts?
EDIT: Here's some additional code:
- (void) addToLog:(NSString*)str {
[logBuffer addObject:s];
if ([logBuffer count] >= kBufferSize) {
[self writeLogOnFile];
}
}
- (void) writeLogOnFile {
NSArray *bufferCopy = [NSArray arrayWithArray:logBuffer]; // create a clone, so that logBuffer doesn't change while dumping data and we have a conflict
NSString *multiline = [bufferCopy componentsJoinedByString:@"\r\n"];
multiline = [NSString stringWithFormat:@"%@\n", multiline];
NSData *data = [multiline dataUsingEncoding:NSUTF8StringEncoding];
NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
[outputFileHandle seekToEndOfFile];
[outputFileHandle writeData:data];
[outputFileHandle closeFile];
[logBuffer removeAllObjects]; // This is where the exception is thrown
}
[CrashManager addToLog:] is called by dozens of classes, not always on the main thread.
Here's the backtrace:
"0 AClockworkBrain 0x0008058f -[SWCrashManager backtrace] + 79",
"1 AClockworkBrain 0x0007fab6 uncaughtExceptionHandler + 310",
"2 CoreFoundation 0x041fe318 __handleUncaughtException + 728",
"3 libobjc.A.dylib 0x03c010b9 _ZL15_objc_terminatev + 86",
"4 libc++abi.dylib 0x044c9a65 _ZL19safe_handler_callerPFvvE + 13",
"5 libc++abi.dylib 0x044c9acd __cxa_bad_typeid + 0",
"6 libc++abi.dylib 0x044cabc2 _ZL23__gxx_exception_cleanup19_Unwind_Reason_CodeP17_Unwind_Exception + 0",
"7 libobjc.A.dylib 0x03c00f89 _ZL26_objc_exception_destructorPv + 0",
"8 CoreFoundation 0x041171c4 -[__NSArrayM removeObjectAtIndex:] + 212",
"9 CoreFoundation 0x04153f70 -[NSMutableArray removeAllObjects] + 96",
"10 AClockworkBrain 0x000817c3 -[SWCrashManager writeLogOnFile] + 691",
"11 AClockworkBrain 0x0008141d -[SWCrashManager addToLog:] + 429",
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM removeObjectAtIndex:]: index 7 beyond bounds [0 .. 6]'
EDIT #2
After reading the suggestion about @synchronize
, I modified it to:
- (void) addToLog:(NSString*)str {
[self performSelectorOnMainThread:@selector(doAddToLog:) withObject:str waitUntilDone:YES];
}
- (void) doAddToLog:(NSString*)str {
// Do the real stuff
}
- (void) writeLogOnFile {
[self performSelectorOnMainThread:@selector(doWriteLogOnFile) withObject:nil waitUntilDone:YES];
}
- (void) doWriteLogOnFile {
// Do the real stuff
}
I tested the code for a few hours and it hasn't thrown an exception. It used to crash about 1-2 times per hour, so I assume that the issue is fixed. Can someone explain how this approach differs from the @synchronize
suggestion?
Also, is it wise to use waitUntilDone:YES or perhaps NO would be better in this case?
Upvotes: 3
Views: 1522
Reputation: 3372
Use @synchronized(logBuffer)
:
- (void) addToLog:(NSString*)str {
@synchronized(logBuffer) {
[logBuffer addObject:s];
}
if ([logBuffer count] >= kBufferSize) {
[self writeLogOnFile];
}
}
- (void) writeLogOnFile {
@synchronized(logBuffer) {
NSString *multiline = [logBuffer componentsJoinedByString:@"\r\n"];
multiline = [NSString stringWithFormat:@"%@\n", multiline];
NSData *data = [multiline dataUsingEncoding:NSUTF8StringEncoding];
NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
[outputFileHandle seekToEndOfFile];
[outputFileHandle writeData:data];
[outputFileHandle closeFile];
[logBuffer removeAllObjects]; // This is where the exception is thrown
}
}
Edit: Since I'm using @synchronized
, we can get rid of the buffer copy and just synch.
In continuation, considering the comments and edited question:
If you only call writeLogOnFile
from addToLog
, then I would do one of two things:
Merge the writeLogOnFile
code into addToLog
, since it's 1-to-1 anyway. This ensures that nothing will ever directly call writeLogOnFile
. In this case, wrap addToLog
completely within @synchronized(logBuffer) {}
If you want to keep writeLogOnFile
separate for whatever reason, then make this method private to the class. In this case, you can get rid of @synchronized(logBuffer)
within writeLogOnFile
since in theory you know what you are doing within the class, but you should also wrap addToLog
completely within @synchronized(logBuffer) {}
As you can see, in both cases you should absolutely make addToLog
completely single-threaded through @synchronized
(or keep the original answer). It's very simple, keeps your code clean, and gets rid of all the threading issues that your edited question is trying to work around. The @synchronized
pattern was created specifically to avoid writing all the wrapper code that you wrote to solve your problem, namely forcing everything through the main thread (or a specific thread).
For completeness, here's the complete code that I would write:
- (void) addToLog:(NSString*)str {
@synchronized(logBuffer) {
[logBuffer addObject:s];
if ([logBuffer count] >= kBufferSize) { // write log to file
NSString *multiline = [logBuffer componentsJoinedByString:@"\r\n"];
multiline = [NSString stringWithFormat:@"%@\n", multiline];
NSData *data = [multiline dataUsingEncoding:NSUTF8StringEncoding];
NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
[outputFileHandle seekToEndOfFile];
[outputFileHandle writeData:data];
[outputFileHandle closeFile];
[logBuffer removeAllObjects];
}
}
}
Upvotes: 4
Reputation:
Though information You have provided it is difficult to tell what's going wrong but according to me the problem could be that you might be modifying the array while iterating through it using main thread or other thread..
Upvotes: 2