Skip to content

Delightful, simple library for aspect oriented programming.

License

Notifications You must be signed in to change notification settings

JsonLeecoder/Aspects

Repository files navigation

Aspects v1.3.1 Build Status

Delightful, simple library for aspect oriented programming by @steipete.

Think of Aspects as method swizzling on steroids. It allows you to add code to existing methods per class or per instance, whilst thinking of the insertion point e.g. before/instead/after. Aspects automatically deals with calling super and is easier to use than regular method swizzling.

This is stable and used in hundreds of apps since it's part of PSPDFKit, an iOS PDF framework that ships with apps like Dropbox or Evernote, and now I finally made it open source.

Aspects extends NSObject with the following methods:

/// Adds a block of code before/instead/after the current `selector` for a specific class.
/// If you choose `AspectPositionInstead`, the `arguments` array will contain the original invocation as last argument.
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<Aspect>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(void (^)(id instance, NSArray *args))block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<Aspect>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(void (^)(id instance, NSArray *args))block
                            error:(NSError **)error;

/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<Aspect> aspect = ...;
[aspect remove];

Adding aspects returns an opaque token which can be used to deregister again. All calls are thread-safe.

Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called 1000 times per second.

Aspects collects all arguments in the arguments array. Primitive values will be boxed.

When to use Aspects

Aspects can be used to dynamically add logging for debug builds only:

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id object, NSArray *arguments) {
    NSLog(@"View Controller %@ will appear animated: %@", object, arguments.firstObject);
} error:NULL];

It can be used to greatly simplify your analytics setup: orta/ARAnalytics#77


You can check if methods are really being called in your test cases:

- (void)testExample {
    TestClass *testClass = [TestClass new];
    TestClass *testClass2 = [TestClass new];

    __block BOOL testCallCalled = NO;
    [testClass aspect_hookSelector:@selector(testCall) withOptions:AspectPositionAfter usingBlock:^(id object, NSArray *arguments) {
        testCallCalled = YES;
    } error:NULL];

    [testClass2 testCallAndExecuteBlock:^{
        [testClass testCall];
    } error:NULL];
    XCTAssertTrue(testCallCalled, @"Calling testCallAndExecuteBlock must call testCall");
}

It can be really useful for debugging. Here I was curious when exactly the tap gesture changed state:

[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(__unsafe_unretained id object, NSArray *arguments) {
    NSLog(@"%@: %@", object, arguments);
} error:NULL];

Another convenient use case is adding handlers for classes that you don't own. I've written it for use in PSPDFKit, where we require notifications when a view controller is being dismissed modally. This includes UIKit view controllers like MFMailComposeViewController or UIImagePickerController. We could have created subclasses for each of these controllers, but this would be quite a lot of unnecessary code. Aspects gives you a simpler solution for this problem:

@implementation UIViewController (DismissActionHook)

// Will add a dismiss action once the controller gets dismissed.
- (void)pspdf_addWillDismissAction:(void (^)(void))action {
    PSPDFAssert(action != NULL);

    __weak __typeof(self)weakSelf = self;
    [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id object, NSArray *arguments) {
        if (weakSelf.isBeingDismissed) {
            action();
        }
    } error:NULL];
}

@end

Debugging

Aspects identifies itself nicely in the stack trace, so it's easy to see if a method has been hooked:

Using Aspects with non-void return types

When you're using Aspects with AspectPositionInstead, the last argument of the arguments array will be the NSInvocation of the original implementation. You can use this invocation to customize the return value:

    [PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id object, NSArray *arguments) {
        // Call original implementation.
        BOOL processTouches;
        NSInvocation *invocation = arguments.lastObject;
        [invocation invoke];
        [invocation getReturnValue:&processTouches];

        if (processTouches) {
            processTouches = pspdf_stylusShouldProcessTouches(arguments[0], arguments[1]);
            [invocation setReturnValue:&processTouches];
        }
    } error:NULL];

Installation

The simplest option is to use pod "Aspects".

You can also add the two files Aspects.h/m. There are no further requirements.

Compatibility and Limitations

Aspects uses quite some runtime trickery to achieve what it does. You can mostly mix this with regular method swizzling.

An important limitation is that for class-based hooking, a method can only be hooked once within the subclass hierarchy. See #2 This does not apply for objects that are hooked. Aspects creates a dynamic subclass here and has full control.

KVO works if observers are created after your calls aspect_hookSelector: It most likely will crash the other way around. Still looking for workarounds here - any help apprechiated.

Because of ugly implementation details on the ObjC runtime, methods that return unions that also contain structs might not work correctly unless this code runs on the arm64 runtime.

Credits

The idea to use _objc_msgForward and parts of the NSInvocation argument selection is from the excellent ReactiveCocoa from the GitHub guys. This article explains how it works under the hood.

Supported iOS & SDK Versions

  • Aspects requires ARC.
  • Aspects is tested with iOS 6+ and OS X 10.7 or higher.

License

MIT licensed, Copyright (c) 2014 Peter Steinberger, steipete@gmail.com, @steipete

Release Notes

Version 1.3.1

  • Add support for OS X 10.7 or higher. (thanks to @ashfurrow)

Version 1.3.0

  • Add automatic deregistration.
  • Checks if the selector exists before trying to hook.
  • Improved dealloc hooking. (no more unsafe_unretained needed)
  • Better examples.
  • Always log errors.

Version 1.2.0

  • Adds error parameter.
  • Improvements in subclassing registration tracking.

Version 1.1.0

  • Renamed the files from NSObject+Aspects.m/h to just Aspects.m/h.
  • Removing now works via calling remove on the aspect token.
  • Allow hooking dealloc.
  • Fixes infinite loop if the same method is hooked for multiple classes. Hooking will only work for one class in the hierarchy.
  • Additional checks to prevent things like hooking retain/release/autorelease or forwardInvocation:
  • The original implementation of forwardInvocation is now correctly preserved.
  • Classes are properly cleaned up and restored to the original state after the last hook is deregistered.
  • Lots and lots of new test cases!

Version 1.0.1

  • Minor tweaks and documentation improvements.

Version 1.0.0

  • Initial release

About

Delightful, simple library for aspect oriented programming.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Objective-C 99.2%
  • Ruby 0.8%