Declarative logging in Objective-C

Posted by on Jan 12, 2011 in code | One Comment

google_analytics.png

I’m currently working on a project with a requirement to log various events using Google Analytics. I created a UsageLogger service to encapsulate all the GA logic and make it easy to turn it off or replace it with another analytics solution.

But I didn’t want to have logging methods sprinkled throughout the code, and don’t want to couple my classes with an analytics package, or even with my custom logger class, so I looked into ways to set up declarative logging.

The approach I landed on uses method swizzling to keep all the analytics logic in my UsageLogger class. One cool aspect of Objective-C’s dynamic runtime is that you can replace the implementation of a method on an individual instance of an object, or on all instances, at runtime. Among other things, this is useful for unit testing.

My approach starts with the following function, adapted from Kevin Ballard’s post on swizzling cocoa methods:

void SwapMethods(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
    if (aClass != nil) {
        Method orig_method = nil, alt_method = nil;
        if (forInstance) {
            orig_method = class_getInstanceMethod(aClass, orig_sel);
            alt_method = class_getInstanceMethod(aClass, alt_sel);
        } else {
            orig_method = class_getClassMethod(aClass, orig_sel);
            alt_method = class_getClassMethod(aClass, alt_sel);
        }
        // If both are found, swizzle them
        if ((orig_method != nil) && (alt_method != nil)) {
            method_exchangeImplementations(orig_method, alt_method);
        } else {
            NSLog(@"SwapMethods Error: Original %@, Alternate %@",
                  (orig_method == nil)?@" not found":@" found",
                  (alt_method == nil)?@" not found":@" found");
        }
    } else {
        NSLog(@"SwapMethods Error: Class not found");
    }
}

The effect of this function is that the pointers to the underlying method implementations are swapped within the class definition. When an object receives a message matching the original selector, the replacement method is invoked instead. And since the implementations are swapped, the replacement selector can be called to invoke the original method.

With this in place, I do the following to intercept the method calls to be logged:

  1. Create a category method matching the signature of the method to be logged.
  2. In the category method, invoke the category method. Once the implementations are swapped, this will cause the original method to be invoked.
  3. In the category method, send a message to the UsageLogger with the information to be captured.
  4. In the UsageLogger’s initialization, call SwapMethods to swap the pointers of the original and category methods.

The code

First, the category method. To keep things clean, I put this in my UsageLogger class:

@implementation TextEntryController (UsageLogger)

-(void)logTextEntered:(NSString *)text {
    // call original (pointer to that method has been swapped with this one)
    [self logTextEntered:text];
    // log the event
    [[UsageLogger sharedLogger] logEvent:@"text-entered" withValue:text];
}

@end

This category adds the logTextEntered: method to the TextEntryController class.

Next, swap the method implementations:

+(void)setUp {
    SwapMethods([TextEntryController class],
                 @selector(textEntered:),
                 @selector(logTextEntered:), YES);
}

After calling [UsageLogger setUp], when an instance of TextEntryController is sent a textEntered: message, the logTextEntered: method is invoked instead. Likewise, when logTextEntered: calls logTextEntered:, the textEntered: method is invoked.

1 Comment

  1. Zak
    August 9, 2013

    I’ve been looking for a solution to the same problem — I don’t want to see analytics code sprinkled everywhere. I’m curious how this solution scaled. I have about 80-100 events being tracked so it seems like the SwapMethods approach would get hard to manage.

    Reply

Leave a Reply