Here is a situation: Hockeyapp and testflight every now and then complain about me
"attempting to insert nil object"
in mutable dictionaries/arrays. I know the right thing is to check for nil all the time, and I do when it makes sense.. Our testers can not catch those crashes, but AppStore users obviously can.
My guess is that sometimes server returns NSNulls when it should not.
So not to insert checks for nil everywhere in the huge project my idea was to create a separate target for the testers and use method swizzling for collection classes.
Say, I'll replace insertObject:atIndex
with my swizzled_insertObject:atIndex
, where if the object is actually nil I log/show a descriptive report before it crashes.
The thing is I can not use swizzling for __NSPlaceholderDictionary
or __NSArrayM
(just because I can not make a category on private classes) and that makes me sad.
So basically I'm asking for advice on how to catch those nasty rare crashes.
One solution I have in mind is using try-catch blocks, I know they are expensive in Objective-c, so I'd not use them in production, just for testers. But methods surrounded by try-catche
-s surrounded by #ifdef
-#endif
-s will erase all the readableness of the code. So I'm searching for a more elegant solution.
Thanks.
Update: the stack traces are unfortunaely not very descriptive, here is what I get
Exception Type: SIGABRT
Exception Codes: #0 at 0x3a378350
Crashed Thread: 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[2]'
Last Exception Backtrace:
0 CoreFoundation 0x321522a3 <redacted> + 163
1 libobjc.A.dylib 0x39e7a97f _objc_exception_throw + 31
2 CoreFoundation 0x320a355f <redacted> + 135
3 CoreFoundation 0x320da0d3 <redacted> + 51
....
You don't need to add a category to do method swizzling. I was able to isolate a crash like this by method swizzling initWithObjects:forKeys:count: and putting a try/catch around the original method call. Finally I added a breakpoint in the catch section. This allowed me to break and go back up the stack to where the nil value was being used. This code is added at the top of my AppDelegate.m:
#import <objc/runtime.h>
#import <objc/message.h>
static id safe_initWithObjects(id self, SEL _cmd, const id objects[], const id <NSCopying> keys[], NSUInteger count) {
id orignialResult = nil;
@try {
orignialResult = objc_msgSend(self, @selector(safe_initWithObjects:forKeys:count:), objects, keys, count);
}
@catch (NSException *exception) {
NSLog(@"BUSTED!"); // put breakpoint here
}
return orignialResult;
}
And then in my app did finish launching method:
Class target = NSClassFromString(@"__NSPlaceholderDictionary");
class_addMethod(target, @selector(safe_initWithObjects:forKeys:count:), (IMP)&safe_initWithObjects, "@@:**L");
Method m1 = class_getInstanceMethod(target, @selector(safe_initWithObjects:forKeys:count:));
Method m2 = class_getInstanceMethod(target, @selector(initWithObjects:forKeys:count:));
method_exchangeImplementations(m1, m2);