We’ve all got little libraries of code or scripts that help us with debugging. Often these are for logging information in a particular way, or wrapping logs/tests such that they’re only invoked in Debug builds but not in production. Or they clean up your IDE’s brainfarts.
Having created these debug libraries, how are you going to get your production code to use them? Are you really going to sprinkle MyCompanyDebugLog(fmt,…) messages throughout your app?
Introducing step one on the road to sanity: the debug proxy. This is useful when you want to find out how a particular class gets used, e.g. when it provides callbacks that will be invoked by a framework. You can intercept all the messages to the object, and inspect them as you see fit. Here’s the code (written with the assumption that ARC is enabled):
FZADebugProxy.h #import <Foundation/Foundation.h> @interface FZADebugProxy : NSProxy - (id)initWithTarget: (NSObject *)aTarget; @end FZADebugProxy.m #import "FZADebugProxy.h" @implementation FZADebugProxy { NSObject *target; } - (id)initWithTarget:(NSObject *)aTarget { target = aTarget; return self; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { NSMethodSignature *signature = [target methodSignatureForSelector: sel]; if (signature == nil) { signature = [super methodSignatureForSelector: sel]; } return signature; } - (BOOL)respondsToSelector:(SEL)aSelector { return [target respondsToSelector: aSelector] ? YES : [super respondsToSelector: aSelector]; } - (void)forwardInvocation:(NSInvocation *)invocation { invocation.target = target; SEL aSelector = [invocation selector]; (void)aSelector; [invocation invoke]; } @end
And no, there isn’t a bug in the -initWithTarget: method. The slightly clumsy extraction of the selector in -forwardInvocation: is done to avoid a common problem with using Objective-C inside the debugger where it decides it doesn’t know the return type of objc_msgSend() and refuses to call the method.
You would use it like this. Here, I’ve modified the app delegate from BrowseOverflow to use a proxy object for the object configuration – a sort of domain-specific IoC container.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { BrowseOverflowViewController *firstViewController = [[BrowseOverflowViewController alloc] initWithNibName: nil bundle: nil]; firstViewController.objectConfiguration = (BrowseOverflowObjectConfiguration *)[[FZADebugProxy alloc] initWithTarget: [[BrowseOverflowObjectConfiguration alloc] init]]; TopicTableDataSource *dataSource = [[TopicTableDataSource alloc] init]; [dataSource setTopics: [self topics]]; firstViewController.dataSource = dataSource; self.navigationController.viewControllers = [NSArray arrayWithObject: firstViewController]; self.window.rootViewController = self.navigationController; [self.window makeKeyAndVisible]; return YES; }
The bold line is the important change. The cast silences the compiler’s strict type-checking when it comes to property assignment, because it doesn’t believe that NSProxy is of the correct type. Remember this is only debug code that you’re not going to commit: you could switch to a plain old setter, suppress the warning using diagnostic pragmas or do whatever you want here.
At this point, it’s worth running the unit tests and using the app to convince yourself that the behaviour hasn’t changed at all.
So, how do you use it? Shouldn’t there be an NSLog() or something in the proxy class so you can see when the target’s messaged?
No.
Step two on the road to sanity is to avoid printf()-based debugging in all of its forms. What you want to do here is to use Xcode’s debugger actions so that you don’t hard-code your debugging inspection capabilities into your source code.
Set a breakpoint in -[FZADebugProxy forwardInvocation:]. This breakpoint will be met whenever the target object is messaged. Now right-click on the breakpoint marker in the Xcode source editor’s gutter and choose “Edit Breakpoint…” to bring up this popover.
In this case, I’ve set the breakpoint to log the selector that was invoked, and crucially to continue after evaluation so that my app doesn’t stop in the debugger every time the target object is messaged. After a bit of a play with the app in the simulator, the debug log looks like this:
GNU gdb 6.3.50-20050815 (Apple version gdb-1752) (Sat Jan 28 03:02:46 UTC 2012) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "x86_64-apple-darwin".sharedlibrary apply-load-rules all Attaching to process 3898. Pending breakpoint 1 - ""FZADebugProxy.m":37" resolved Current language: auto; currently objective-c 0x10271: "stackOverflowManager" 0x102a0: "avatarStore" 0x10271: "stackOverflowManager" 0x10271: "stackOverflowManager" 0x102a0: "avatarStore"
Pretty nifty, yes? You can do a lot with Xcode’s breakpoint actions: running a shell script or an AppleScript are interesting options (you could have Xcode send you an iMessage every time it send your target an Objective-C message). Speaking out the names of selectors is fun for a very short while, but not overly useful.
Xcode’s breakpoint actions give you a much more powerful debugging capability than NSLog(). By using breakpoint actions on an Objective-C proxy object, you can create highly customisable aspect-oriented techniques for debugging your code.
Thanks for this idea! I’ve adopted it for our own use.
A couple of questions though: (1) Wouldn’t [super methodSignatureForSelector: sel] raise an exception, because that’s the default NSProxy implementation? (2) What exactly is the purpose of the -respondsToSelector: override?
(1) yes.
(2) The use case for this class is when you want to find out how another object interacts with your object, e.g. in what order delegate, datasource or other callback methods are invoked. The far object will call -respondsToSelector: to find out whether its delegate implements the optional methods, so that needs to be handled correctly in the proxy.
It seems to be handled correctly by default, though. If you just call [super respondsToSelector: aSelector], or don’t override it at all, then NSProxy will call methodSignatureForSelector: and then forwardInvocation:, which is exactly what you want, right?
That’s not what the documentation says:
All the documentation says is that if -respondsToSelector returns NO, the object might still forward the message. The documentation doesn’t say that if the object forwards the message, -respondsToSelector must return NO. The latter is definitely not true for NSProxy. You can verify this in the debugger.
Sure, but if I’m going to _depend_ on it working in some particular way, I can provide that behaviour instead of relying on undocumented magic.
The documentation also says this: “You cannot test whether an object inherits a method from its superclass by sending respondsToSelector: to the object using the super keyword. This method will still be testing the object as a whole, not just the superclass’s implementation. Therefore, sending respondsToSelector: to super is equivalent to sending it to self.”
Correct. Luckily, I don’t need to do that.
Hmm? You do call [super respondsToSelector: aSelector]
Yes, to find out what my class responds to, not the superclass.
But it doesn’t work. If you don’t believe me, add a method -foo to FZADebugProxy, call [myProxy respondsToSelector:@selector(foo)], and see what the return value is.
When you call [super respondsToSelector: aSelector], NSProxy’s implementation of -respondsToSelector: actually forwards the call of -respondsToSelector:, i.e., it calls methodSignatureForSelector: with the argument @selector(respondsToSelector:) in order to build an invocation, and then it calls -forwardInvocation:. So Apple’s docs could probably be better worded, but the point is that calling -respondsToSelector: on super is completely useless.
Look at Apple’s sample code, it doesn’t call super for either -respondsToSelector: or methodSignatureForSelector:
https://developer.apple.com/library/mac/#samplecode/ForwardInvocation/Listings/main_m.html