preface

In everyday iOS development, it is common to hook certain methods. The most common is the method swizzle in + Load using categories, which is class-specific and changes the behavior of all instances of that class. But sometimes we only want to hook for a single instance, and this approach becomes weak. The Aspects framework takes care of this. It supports single instance hooks by dynamically subclassing the Runtime, pointing the isa pointer to the newly created subclass, and then processing the hook methods in the subclass. The Aspects framework supports hooks to classes and instances. The API is easy to use and allows you to hook easily from anywhere. It is thread safe. But the Aspects framework also has some flaws that can fall into place if you’re not careful, as I’ll explain through source code analysis.

The source code parsing

I mainly use diagrams to illustrate the source code of Aspects. It is recommended to refer to the source code. Understanding these topics requires some knowledge of ISA Pointers, message forwarding mechanisms, and runtime, which I won’t cover in this article because each of these topics would require a separate article.

Main process analysis

  1. Its first flow is add using associated objectsContainerIn this process, some preconditions will be judged, such as whether the method can be supported by hook, etc. If the condition is verified, the hook information will be saved and used when the method is called.
  2. The second process is to create subclasses dynamically, which you would not do if you were targeting a hook of a class.
  3. The third step is to replace the classforwardInvocation:Methods for__ASPECTS_ARE_BEING_CALLED__Internally, the method looks up the Container created earlier and makes the actual call based on the logic in the Container.
  4. The fourth step is the original methodIMPInstead of_objc_msgForwardWill be called when the original method is called_objc_msgForward, thus triggeringforwardInvocation:Methods.

I have made a simplified diagram of its flow, marked with the serial number of each flow, each flow will be analyzed later. The process is as follows:

The retrieved object type in the figure refers to the type of the object that calls hook. If it is an instance object, then the classpath is used. If it is a class object, go through the metaclass path. If the actual type is inconsistent, such as KVO, go to another subclass path.

① Process for adding containers

In this process, the logic of the hook is encapsulated into a Container and stored in an associated object. In this process, it will judge whether the hook method is supported, judge the inheritance relationship of the hook class, verify the correctness of the callback block and other operations. The specific picture is as follows:

The key codes are as follows:

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    ...
    aspect_performLocked(^{ / / lock
        // hook preconditions
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // Get the Container object by associating it with the selector key.
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // Internally checks if a block matches a hook's selector, and returns nil if it doesn't.
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // Add identifier that contains the type and callback of hook.
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error); }}});return identifier;
}

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        disallowedSelectorList = [NSSet setWithObjects:@"retain".@"release".@"autorelease".@"forwardInvocation:".nil];
    });

    // Filter methods that do not support hook
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
        AspectError(AspectErrorSelectorBlacklisted, errorDescription);
        return NO;
    }

    // Dealloc supports only AspectPositionBefore calls
    AspectOptions position = options&AspectPositionFilter;
    if ([selectorName isEqualToString:@"dealloc"] && position ! = AspectPositionBefore) {NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
        AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
        return NO;
    }

    // Check whether the method exists
    if(! [selfrespondsToSelector:selector] && ! [self.class instancesRespondToSelector:selector]) {
        NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].".NSStringFromClass(self.class), selectorName];
        AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
        return NO;
    }

    // It is forbidden to use the same method as the class hook
    if (class_isMetaClass(object_getClass(self))) {... }return YES;
}

/// The implementation of AspectIdentifier is added inside AspectsContainer.
// we can see that multiple hooks of the same method will be called, and the previous hook will not be overwritten.
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
    NSParameterAssert(aspect);
    NSUInteger position = options&AspectPositionFilter;
    switch (position) {
        case AspectPositionBefore:  self.beforeAspects  = [(self.beforeAspects ? :@[]) arrayByAddingObject:aspect];break;
        case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects? :@[]) arrayByAddingObject:aspect];break;
        case AspectPositionAfter:   self.afterAspects   = [(self.afterAspects ? :@[]) arrayByAddingObject:aspect];break; }}Copy the code
  1. As can be seen from the source code, hook methods are not supported[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];. Among themretain.release.autoreleaseIs disabled under ARC, the frame itself ishooktheforwardInvocation:Implement, so its hook is not supported.
  2. deallocOnly supportAspectPositionBeforeType, usingAspectPositionInsteadCauses the system to defaultdeallocThe operation was replaced and could not be executed.AspectPositionAfterThe object may have already been released when called, raising a wild pointer error.
  3. AspectsTo disallow the same method of an inherited class hook, see one of themissue, it reports that doing so leads to an infinite loop, which I’ll explain later in this article.
  4. AspectsuseblockThe call to hook involves passing method parameters and returning values, so the block will be checked.

② Create a subclass of runtime

In the iOSKVOIt is throughruntimeCreate a subclass dynamically and override the corresponding in the subclasssetterMethod to implement,AspectsThe hook principle that supports a single instance is somewhat similar. The illustration is as follows:For details, please see the comments in the source code

/ / the hooks
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    // For instance types, subclasses are created dynamically through Runtime. Class types hook directly.
    Class klass = aspect_hookClass(self, error); . }static Class aspect_hookClass(NSObject *self.NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
	Class baseClass = object_getClass(self);
	NSString *className = NSStringFromClass(baseClass);

    // The class that has been hooked is returned directly
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;

    // is a MetaClass, which means to hook the class. (Not a single instance)
	}else if (class_isMetaClass(baseClass)) {
        // Internally the class's forwardInvocation: method is replaced with __ASPECTS_ARE_BEING_CALLED__
        return aspect_swizzleClassInPlace((Class)self);
    // It could be a KVO object, etc., passing in the actual type to hook.
    }else if(statedClass ! = baseClass) {return aspect_swizzleClassInPlace(baseClass);
    }

    // In the case of a single instance, dynamically create subclasses to hook.
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
		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;
        }
        // Internally the class's forwardInvocation: method is replaced with __ASPECTS_ARE_BEING_CALLED__
		aspect_swizzleForwardInvocation(subclass);
        // Override the class method to return the previous type instead of the newly created subclass. Avoid type judgment problems after hook.
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	}

	object_setClass(self, subclass);
	return subclass;
}

Copy the code

(3) replace forwardInvocation:

This part is to replace the old forwardInvocation with a custom implementation: __ASPECTS_ARE_BEING_CALLED__. The source code is as follows:

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.
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.".NSStringFromClass(klass));
}
Copy the code

The corresponding relationship after replacement is shown as follows:

Hook method to exchange IMP:

The illustration is as follows:

The IMP of the hook invocation should be replaced with _objc_msgForward, which is based on the iOS message transfer mechanism. The IMP of the hook invocation should be replaced with _objc_msgForward. It is important to note that some frameworks, such as JSPatch, also use the iOS messaging mechanism to do some operations, which should be used carefully to avoid conflicts.

The process of being called by a hook method

When a hook method is called after hook injection, the call flow changes. The illustration is as follows:

From the above analysis, we can seeAspectsThis frame is cleverly designed to show you a lotruntimeApplication of knowledge. But the authors do not recommend using it in real projects:

Because Apsects makes changes to the bottom of the class, these changes are fundamental and need to take into account various scenarios and boundary issues, and if one aspect is not considered properly, some unknown problems can arise. In addition, this framework is flawed and has not been updated for a long time. I have summarized its known problem points and explained them below. If there is no summary in place, welcome to add.

Trouble spots

Class-based hooking, all classes in the same inheritance chain, a method can only be hooked once, after the hook is invalid.

In the past, such behavior would appear an infinite loop, but the author modified it later, prohibited this behavior and added an error message. See this issue for details.

@interface A : NSObject
- (void)foo;
@end

@implementation A
- (void)foo {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}
@end

@interface B : A @end

@implementation B
- (void)foo {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [super foo]; // Code that causes an infinite loop
}
@end

int main(int argc, char *argv[]) {
    [B aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
        NSLog(@"before -[B foo]");
    }];
    [A aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
        NSLog(@"before -[A foo]");
    }];

    B *b = [[B alloc] init];
    [b foo]; // Call after an infinite loop
}
Copy the code

As we all know, super looks for methods from its parent class and passes in self to call. The IMP of foo has been replaced by _objc_msgForward, and self is called. Because it is passed in self, it actually calls its own forwardInvocation:, resulting in an infinite loop.

For a single instance of hook, there is no problem using KVO after hook, but there will be problems using KVO after hook.

To illustrate this, use the Animal object as an example:

@interface Animal : NSObject
@property(strong.nonatomic) NSString * name;
@end

@implementation Animal
- (void)testKVO {
    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.name = @"Animal";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    NSLog(@"observeValueForKeyPath keypath:%@ name:%@", keyPath, self.name);
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"name"];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [animal testKVO];
        NSKVONotifying_Animal (NSKVONotifying_Animal (NSKVONotifying_Animal)
        [animal aspect_hookSelector:@selector(setName:) 
                        withOptions:AspectPositionAfter 
                         usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
            NSLog(@"aspects hook setName");
        } error:nil];
        // There will be a crash
        animal.name = @"ChangedAnimalName"; }}Copy the code

The abnormal cause analysis is shown as follows:

Above is the inheritance chain and method call flow chart, it can be seen that were _NSSetObjectValueAndNotify aspects__setName: call, _NSSetObjectValueAndNotify internal implementation logic is calling it the selector, The parent class looks for the aspects__setName: method, which the Animal object does not have, causing the crash.

Coexistence with categories

Hook using Aspects first and then category will lead to crash. Otherwise, there is no problem. The sample code is as follows:

@interface Animal : NSObject
@property(strong.nonatomic) NSString * name;
@end

@implementation Animal
- (void)setName:(NSString *)name {
    NSLog(@"%s", __func__);
    _name = name;
}
@end

@interface Animal(hook)
+ (void)categoryHook;
@end

@implementation Animal(hook)
+ (void)categoryHook {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [super class];
        SEL originalSelector = @selector(setName:);
        SEL swizzledSelector = @selector(lx_setName:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)lx_setName:(NSString *)name {
    NSLog(@"%s", __func__);
    [self lx_setName:name];
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [Animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
            NSLog(@"aspects hook setName");
        } error:nil];
        
        [Animal categoryHook];
        // Crash: [Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0
        animal.name = @"ChangedAnimalName"; }}Copy the code

This with__ASPECTS_ARE_BEING_CALLED__Is related to the internal logic of theaspect__Make a call to call the originalIMP, butcategoryHook breaks this process. The illustration is as follows:

Aspects__lx_setName = aspects__lx_setName = aspects__lx_setName

Class-based hook, if the same class hook method and instance method, then the post-hook method will crash. The sample code is as follows:

@interface Animal : NSObject
- (void)testInstanceMethod;
+ (void)testClassMethod;
@end

@implementation Animal
- (void)testInstanceMethod {
    NSLog(@"%s", __func__);
}
+ (void)testClassMethod {
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [Animal aspect_hookSelector:@selector(testInstanceMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
            NSLog(@"aspects hook testInstanceMethod");
        } error:nil];
        
        [object_getClass([Animal class]) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
            NSLog(@"aspects hook testClassMethod");
        } error:nil];
        
        [animal testInstanceMethod];
        // crash: "+[Animal testClassMethod]: unrecognized selector sent to class 0x1000114a0"[Animal testClassMethod]; }}Copy the code

Such a call is perfectly normal in everyday development, but it can cause a crash. It is caused by a logical flaw in the aspect_swizzleClassInPlace method.

static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    // Animal and Animal metaclass get the same string.
    NSString *className = NSStringFromClass(klass);
    NSLog(@"aspect_swizzleClassInPlace %@ %p", klass, object_getClass(klass));
    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        // The class object and the metaclass object get the same className.
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}
Copy the code

From the code above you can see, it’s to heavy logic judging simply string, take the Animal’s metaclass Animal and the name of the class to get the same string, lead to add after filtered, after the call after the hook method, execution _objc_msgForward, The post hook aspect_swizzleForwardInvocation was filtered and not executed, so the forwardInvocation IMP could not be found, resulting in a crash.

_objc_msgForward conflicts

Internally, the message forwarding mechanism is used with care to avoid conflicts with other frameworks that use _objc_msgForward or related logic.

Performance issues

Hook after the method, through the original message mechanism to find IMP, will not be directly called. Instead, the message will be forwarded to the __ASPECTS_ARE_BEING_CALLED__ method, and then the corresponding Coantiner will be called through the key. Compared with before hook, the call cost will be increased. Frequently invoked methods and heavy use in projects are not recommended.

Threading issues

Internal framework in order to ensure thread safety, there are locks, but the use of a spin lock OSSpinLock, there is a thread inversion problem, in iOS10 has been marked as deprecated.

To hook a class method, use object_getClass to get a metaclass object to hook it

This is not a framework problem, but some students do not know how to hook class methods.

@interface Animal : NSObject
+ (void)testClassMethod;
@end

// Use object_getClass to get the metaclass object to hook
[object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod)     
                                 withOptions:AspectPositionAfter 
                                  usingBlock:^(id<AspectInfo> aspectInfo){
    NSLog(@"aspects hook setName");
} error:null];
Copy the code