NSMutableArray removeAllObjects beyond bounds exce

2019-05-13 17:42发布

问题:

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?

回答1:

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:

  1. 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) {}

  2. 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];
      }
    }
}


回答2:

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..