What are Aspects?

Aspects is a lightweight AOP library on iOS. It uses the Method Swizzling technique to add extra code to existing class or instance methods, It is part of the well-known PSPDFKit (an iOS PDF framework that ships with apps like Dropbox or Evernote).

How to use Aspects

/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

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

Copy the code

Aspects provides two AOP methods, one for classes and one for instances. After determining the method of a hook, Aspects allow us to choose whether the hook is timed before or after the method is executed, or even directly replace the method implementation. Common usage scenarios for Aspects are business-neutral operations such as logs and dot-count. Like the Hook ViewController viewWillLayoutSubviews method.

[aspectsController aspect_hookSelector:@selector(viewWillLayoutSubviews) withOptions:0 usingBlock:^{
            NSLog(@"Controller is layouting!");
        } error:NULL];
Copy the code

Technologies used by Aspects

Before reading the source code for Aspects, I need some knowledge of the Runtime. Check out some of my own blogs.

  1. Objective-c simple implementation of objc_msgSend
  2. Objective-c method signature and invocation
  3. Dynamic implementation of Objective-C
  4. Objective-c message forwarding
  5. Mixing up objective-C methods

Aspects of the code

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

The following source interpretation is mainly to analyze the execution process of the instance method of Aspects, as well as the design idea of Aspects. As for the execution flow and thinking of the class methods in Aspects, they are much the same and no longer cumbersome.

/// @return A token which allows to later deregister the aspect.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}

Copy the code

This method returns an AspectToken object, which is primarily the aspect’s unique identifier. Static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) This method is used to add an aspect to an instance.

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {

   / /... Omit code
    __block AspectIdentifier *identifier = nil;
    // Lock the block
    aspect_performLocked(^{
        // Check whether selector can be hooked
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // Create an AspectsContainer object and associate it with the instance object using selector
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // Create an AspectIdentifier object,
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // Add the AspectIdentifier object to the AspectsContainer object
                [aspectContainer addAspect:identifier withOptions:options];
                
                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error); }}});return identifier;
}
Copy the code
  1. To block the lock
  2. Determine whether a selector conforms to the rules of a hook
  3. Create an AspectsContainer object and associate it with the instance object using selector. All aspects of a method used to manage an object or class
  4. Create an AspectIdentifier object and place it in an AspectsContainer object to manage it. The AspectIdentifier object represents the content of an aspect

Scrutinize aspect_isSelectorAllowedAndTrack method content, look at how to determine a selector that conform to rules of hook

// Check whether selector can be hooked
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    // Set of methods that cannot be hooked
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        // These methods cannot be hooked
        disallowedSelectorList = [NSSet setWithObjects:@"retain".@"release".@"autorelease".@"forwardInvocation:".nil];
    });
    
    // Check against the blacklist.
    / /... Omit code
    
    // Additional checks.
    AspectOptions position = options&AspectPositionFilter;
    // The dealloc method is not allowed to be hooked after execution because the object will be destroyed
    if ([selectorName isEqualToString:@"dealloc"] && position ! = AspectPositionBefore) {/ /... Omit code
    }
    // The method being hooked does not exist in the class
    if(! [selfrespondsToSelector:selector] && ! [self.class instancesRespondToSelector:selector]) {
       / /... Omit code
    }
    
    // Search for the current class and the class hierarchy IF we are modifying a class object
    if (class_isMetaClass(object_getClass(self))) {
        Class klass = [self class];
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];
        
        AspectTracker *tracker = swizzledClassesDict[currentClass];
        // Check whether subclasses have already hooked the method
        if ([tracker subclassHasHookedSelectorName:selectorName]) {
            / /... Omit code
        }
        
        do {
            // Check whether the method has been hooked
            tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) {
                if (klass == currentClass) {
                    // Already modified and topmost!
                    return YES;
                }
                NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)];
                AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                return NO; }}while ((currentClass = class_getSuperclass(currentClass)));
        
        / /... Omit code
    return YES;
}

Copy the code

Selector A rule that is not allowed to be hooked

  1. @”retain”, @”release”, @”autorelease”, @”forwardInvocation:” these methods are not allowed to be hooked
  2. The dealloc method is not allowed to be hooked after execution
  3. The hook method does not exist in the class
  4. A method can only be hooked once

Then look at the static void aspect_prepareClassAndHookSelector (NSObject * self, SEL selector, NSError * * error) method.

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);// 1 swizzling forwardInvocation
    // Selector by hook
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if(! aspect_isMsgForwardIMP(targetMethodIMP)) {//2 swizzling method
        // use an aliasSelector to point to the implementation of the original selector method
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if(! [klass instancesRespondToSelector:aliasSelector]) { __unusedBOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
            NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@".NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
        }
        
        // We use forwardInvocation to hook in.
        // point selector to the _objc_msgForward function
        // replace the selector IMP with the _objc_msgForward pointer, and then execute the IMP
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); }}Copy the code
  1. Swizzling forwardInvocation.
  2. Take the implementation of the original selector method, reproduce it as an aliasSelector that points to the implementation of the original selector method
  3. You point selector to _objc_msgForward and replace the IMP of selector with the pointer to _objc_msgForward, so that when performing a selector, the _objc_msgForward function is executed.

Static Class aspect_hookClass(NSObject *self, NSError **error


static Class aspect_hookClass(NSObject *self.NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);
    // Whether there is an _Aspects_ suffix
    // Already subclassed
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;
        // We swizzle a class object, not a single object.
    }else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
    }else if(statedClass ! = baseClass) {return aspect_swizzleClassInPlace(baseClass);
    }
    
    // dynamically generate a subclass of the current object and associate the current object with the subclass, then replace the forwardInvocation method of the subclass
    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);
    
    if (subclass == nil) {
        // Generate a subclass of baseClass
        subclass = objc_allocateClassPair(baseClass, subclassName, 0);
        if (subclass == nil) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }
        // replace the forwardInvocation method for the subclass
        aspect_swizzleForwardInvocation(subclass);
        // Modify subclass and its subclass metaclass class method to return the class of the current object.
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
    // The current isa pointer points to subclass
    // Set the current self to a subclass, which is really just changing the ISA pointer to self
    object_setClass(self, subclass);
    return subclass;
}

Copy the code

The purpose of this method is to dynamically generate a subclass of the current object, associate the current object with the subclass, and then replace the forwardInvocation method of the subclass without needing to change the instance of the object itself. The static void aspect_swizzleForwardInvocation(Class klass) method mixes the forwardInvocation: method of the subclass;

Static void aspect_swizzleForwardInvocation(Class klass

/ / swizzling forwardinvation method
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    // If there is no method, replace will act like class_addMethod.
    // Replace the subclass's forwardInvocation method implementation with __ASPECTS_ARE_BEING_CALLED__
    // Since the subclass does not implement the forwardInvocation itself,
    / / so the returned originalImplementation will null values, so the subclass will not generate AspectsForwardInvocationSelectorName this method
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        NSLog(@"class_addMethod");
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.".NSStringFromClass(klass));
}
Copy the code

The key implementation is in this code, which replaces the forwardInvocation: implementation with an __ASPECTS_ARE_BEING_CALLED__ implementation

IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
Copy the code

If you hook a method, you’re going to execute ASPECTS_ARE_BEING_CALLED, and you’re basically at the end of the code. Let’s look at this method implementation

// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    / /... Omit code

    // Before hooks. Execute Before cutting
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);
    
    // Instead hooks
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        // Revert to the function pointed to by the original selector
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break; }}while(! respondsToAlias && (klass = class_getSuperclass(klass))); }// After hooks. Execute After the cut
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
    
    // If no hooks are installed, call original implementation (usually to throw an exception)
    // If no implementation of the aliasSelector method is found, that is, the original implementation of the hook selector is not found
    if(! respondsToAlias) { invocation.selector = originalSelector; SEL originalForwardInvocationSEL =NSSelectorFromString(AspectsForwardInvocationSelectorName);
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else{[selfdoesNotRecognizeSelector:invocation.selector]; }}// Remove any hooks that are queued for deregistration.
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

Copy the code
  1. Execute the corresponding code according to the timing point of the slice
  2. After executing the extra code, see if you need to go back to the original code, and execute the original code if you do
  3. If no implementation of the original method by the hook’s selector is found, the message is forwarded
  4. Aspects do corresponding cleanup work

Aspects of the idea

  1. Find the originalSelector method implemented by the hook

  2. Create a new aliasSelector that points to the originalSelector method implementation

  3. Dynamically create a subclass of the originalSelector instance, hook the forwardInvocation: method and replace the method’s implementation with the ASPECTS_ARE_BEING_CALLED method

  4. OriginalSelector points to the _objc_msgForward method implementation

  5. The originalSelector method of an instance actually points to objc_msgForward, whose method implementation is replaced with an ASPECTS_ARE_BEING_CALLED method implementation, That is, after the originalSelector method is executed, the __ASPECTS_ARE_BEING_CALLED method implementation is actually executed. The purpose of aliasSelector is to save the method implementation of originalSelector, and when the hook code is executed, it can go back to the original method implementation of originalSelector.

reference

  1. https://github.com/steipete/Aspects
  2. https://wereadteam.github.io/2016/06/30/Aspects/
  3. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.ht ml
  4. Objective-c simple implementation of objc_msgSend
  5. Objective-c method signature and invocation
  6. Dynamic implementation of Objective-C
  7. Objective-c message forwarding
  8. Mixing up objective-C methods
  9. Code comments https://github.com/junbinchencn/Aspects