Aspects 源码分析

Aspects 源码分析

Aspects是一个非常知名的用于 AOP 的 Objective-C 库,可以对类方法或者实例方法进行 Hook,虽然它的作者不推荐在生产环境下使用这个组件,但是了解 Aspects 的原理,对于我们更好地掌握 Objective-C 这门语言还是很有好处的,因此本文就来简要地分析下 Aspects 的实现细节。

NSObject 分类

这个库里的代码不多,只有 Aspects.hAspects.m 两个文件。头文件里定义了一个 NSObject 的分类,给所有 NSObject 的子类添加了两个方法,分别用于对类方法和对类实例方法进行 Hook:

erlang 复制代码
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

options 指定了 AOP 切面执行的时机:

ini 复制代码
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 在原始方法执行后生效(默认)        
    AspectPositionInstead = 1,            /// 替换原始方法
    AspectPositionBefore  = 2,            /// 在原始方法执行前生效
    AspectOptionAutomaticRemoval = 1 << 3 /// 只执行一次
};

进入

erlang 复制代码
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}

它调用了 aspect_add 这个静态函数:

scss 复制代码
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    NSCParameterAssert(self);
    NSCParameterAssert(selector);
    NSCParameterAssert(block);

    __block AspectIdentifier *identifier = nil;
    
    // 1.
    aspect_performLocked(^{
    
        // 2. 
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 3
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 4. 
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // 5. 
                [aspectContainer addAspect:identifier withOptions:options];

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

aspect_performLocked

aspect_performLocked 这个方法在执行作为参数的 block 前会进行加锁操作,而在 block 执行完后进行解锁:

scss 复制代码
static void aspect_performLocked(dispatch_block_t block) {
    static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&aspect_lock);
    block();
    OSSpinLockUnlock(&aspect_lock);
}

aspect_isSelectorAllowedAndTrack

调用 aspect_isSelectorAllowedAndTrack(self, selector, options, error) 这个方法:

ini 复制代码
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {

    // `retain`,`release`,`autorelease`和`forwardInvocation`都是不允许被 hook ,它们会被加入黑名单中
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    });

    // 如果要 Hook 的方法是以上黑名单中的四个方法中的一个,会打印出错误信息并且返回 NO
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
        AspectError(AspectErrorSelectorBlacklisted, errorDescription);
        return NO;
    }

    // 如果 Hook 的是 dealloc 方法,并且 Hook 时机不是在 dealloc 方法执行之前,也会打印出错误信息,并且返回 NO
    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;
    }
 
 // 如果被 Hook 的类本身不能响应被 Hook 的方法,那么这里也会报错并且返回 NO
    if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
        NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
        AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
        return NO;
    }

    // Search for the current class and the class hierarchy IF we are modifying a class object
    
    if (class_isMetaClass(object_getClass(self))) {
        // 如果 self 所属的类是一个元类
    
        Class klass = [self class];
        
        // aspect_getSwizzledClassesDict会返回一个全局唯一的`swizzledClassesDict`,以 Class 为键,以`AspectTracker`实例为值,下文中会介绍这个 dictionary 内的键值对是怎么添加的
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];

        AspectTracker *tracker = swizzledClassesDict[currentClass];
        if ([tracker subclassHasHookedSelectorName:selectorName]) {
            // 如果self 所属类的子类已经 Hook 了 selectorName 代表的 SEL,那么在这里就会打印出错误信息并且返回 NO
            NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName];
            NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"];
            NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames];
            AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
            return NO;
        }

        // 从 self 所属类的继承链上进行查找
        do {
            tracker = swizzledClassesDict[currentClass];
            // 如果继承链上的某个类的被交换的方法列表中有当前这个 SEL
            if ([tracker.selectorNames containsObject:selectorName]) {
            
                // 继承链上的这个类就是被 Hook 的那个类,说明之前已经 Hook 过它了,因此这里直接返回 YES
                if (klass == currentClass) {
                    // Already modified and topmost!
                    return YES;
                }
                
                // 继承链上的这个类不是被 Hook 的那个类,而是它的父类,那么这个方法已经被 Hook 过了,我们就不能再次进行 Hook 了,所以这里需要打印出一条错误信息,并且返回 NO
                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)));

        // Add the selector as being modified.
        currentClass = klass;
        AspectTracker *subclassTracker = nil;
        do {
            tracker = swizzledClassesDict[currentClass];
            if (!tracker) {
                // 为 self 所属的这个类创建对应的 AspectTracker
                tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
                
                // 以 self 所属的类为键,以tracker 为值,添加到swizzledClassesDict中
                swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
            }
            if (subclassTracker) {
                [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
            } else {
                [tracker.selectorNames addObject:selectorName];
            }

            // All superclasses get marked as having a subclass that is modified.
            subclassTracker = tracker;
        }while ((currentClass = class_getSuperclass(currentClass)));
	} else {
	
	   // 如果 self 所属的类不是元类,那么直接返回 YES
		return YES;
	}

    return YES;
}

aspect_getContainerForObject

以 self 和 selector 为参数,调用 aspect_getContainerForObject 函数,创建并返回一个 AspectsContainer

ini 复制代码
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
    NSCParameterAssert(self);
    SEL aliasSelector = aspect_aliasForSelector(selector);
    AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
    if (!aspectContainer) {
        aspectContainer = [AspectsContainer new];
        objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
    }
    return aspectContainer;
}

AspectsContainer 和 selector 是通过关联对象关联在一起的,因此这个方法里就是通过 selector 取出对应的 AspectsContainer 实例。

identifierWithSelector:object:options:block:error:

创建出一个 AspectsIdentifier 实例:

ini 复制代码
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {
    NSCParameterAssert(block);
    NSCParameterAssert(selector);
    NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }

    AspectIdentifier *identifier = nil;
    if (blockSignature) {
        identifier = [AspectIdentifier new];
        identifier.selector = selector;
        identifier.block = block;
        identifier.blockSignature = blockSignature;
        identifier.options = options;
        identifier.object = object; // weak
    }
    return identifier;
}

addAspect:withOptions:

将上一步中创建的 AspectIdentifier 实例添加到第3步创建的 AspectsContainer 中:

objectivec 复制代码
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
    NSParameterAssert(aspect);
    NSUInteger position = options&AspectPositionFilter;
    
    // 根据options的设置,AspectIdentifier实例会被添加到AspectsContainer对应的数组里
    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;
    }
}

aspect_prepareClassAndHookSelector

第6步是整个方法 Hook 的核心:

scss 复制代码
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    
    // 6-1 
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    
    // 6-2 
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        
        // 6-3 
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            // 如果第6-1步中创建出来的类不能响应第6-3步中新创建的 SEL,那么就通过class_addMethod 来给新创建出来的类添加一个方法,这个方法的 SEL 就是新创建的 SEL,IMP 和类型编码则是要被替换的方法的 IMP 和类型编码
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
            NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
        }

        // 6-4 
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

这个方法也比较复杂,因此我们将它分成四个小步骤来分析。

第6-1步,调用 aspect_hookClass

objectivec 复制代码
static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    
   // 这里是比较容易混淆的地方
   // .class 方法,当 self 是一个 instance 的时候,返回 self 的类对象;
   // 当 self 是一个类对象的时候,返回它自身
   // object_getClass 方法则是获取 self 的 isa 指针指向的对象,如果self 是一个
   // instance,那么返回一个类对象;如果 self 是一个类对象,则返回一个元类

    
	Class statedClass = self.class;
	Class baseClass = object_getClass(self);
	NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
    
	if ([className hasSuffix:AspectsSubclassSuffix]) {
	   // AspectsSubclassSuffix是个 static 字符串常量,也就是_Aspects_
		return baseClass;

	}else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // 在原来的类名后加上_Aspects_后缀
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
	   // 创建出一个新的 Class 出来,基本步骤有下面几个:
	   // 1. 通过objc_allocateClassPair来创建一个新的子类
	   // 2. 通过class_addMethod, class_addIvar来向新的子类添加方法和实例变量
	   // 3. 通过objc_registerClassPair来向运行时系统注册这个新类
	   // 完成以上3步后就可以使用这个新建的类了
	   
	   // subclass是 baseClass 的子类,类名是subclassName
		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;
        }
    
      // 将新创建出来的类的forwardInvocation:方法替换成__ASPECTS_ARE_BEING_CALLED__方法,详见下面的注释
		aspect_swizzleForwardInvocation(subclass);
		// 替换subclass 的class 方法
		aspect_hookedGetClass(subclass, statedClass);
		// 替换subclass 的元类的class 方法
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		// 注册新创建出来的子类,现在可以使用了
		objc_registerClassPair(subclass);
	}

    // 将 self 所属类的类型改为我们刚刚创建出来的带有_Aspects_后缀的类
    // 这里可以通过在 Xcode 中打断点来进行验证
	object_setClass(self, subclass);
	// 返回这个子类
	return subclass;
}

static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    
    // class_replaceMethod的第一个参数是要进行方法替换的 Class,第二个参数是被替换方法的 SEL,第三个参数是进行替换的方法的 IMP,第四个参数是类型编码,返回值是被替换的方法原来的 IMP。
    // 如果 Class 中没有要被替换的SEL,那么这个方法和class_addMethod 就是一样的
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        // 向 Class 中添加一个新的方法AspectsForwardInvocationSelectorName,也就是__aspects_forwardInvocation:,而它的IMP就是原来的forwardInvocation:的 IMP
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}

static void aspect_hookedGetClass(Class class, Class statedClass) {
    NSCParameterAssert(class);
    NSCParameterAssert(statedClass);
    
   // 获得class 原来的 class 方法
	Method method = class_getInstanceMethod(class, @selector(class));
	// 创建一个新的 IMP
	IMP newIMP = imp_implementationWithBlock(^(id self) {
		return statedClass;
	});
	// 将class 的实现用我们新创建的 IMP 来替换
	class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method));
}

第6-2步,调用了 aspect_isMsgForwardIMP 来判断我们要替换的 SEL 的 IMP 是不是 _objc_msgForward,如果不是才会进行第6-3、6-4步的操作

ini 复制代码
static BOOL aspect_isMsgForwardIMP(IMP impl) {
    return impl == _objc_msgForward
#if !defined(__arm64__)
    || impl == (IMP)_objc_msgForward_stret
#endif
    ;
}

第6-3步,创建出一个新的 selector,这个新的 selector 是由被替换的 selector 的字符串加上一个特定前缀字符串生成的,然后将它添加到上面新创建的类中:

scss 复制代码
static SEL aspect_aliasForSelector(SEL selector) {
    NSCParameterAssert(selector);
	return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
}

第6-4步,将 selector 对应的实现 IMP 替换为 _objc_msgForward

scss 复制代码
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id.
    // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html
    // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783
    // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4)
    Method method = class_getInstanceMethod(self.class, selector);
    const char *encoding = method_getTypeEncoding(method);
    BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
    if (methodReturnsStructValue) {
        @try {
            NSUInteger valueSize = 0;
            NSGetSizeAndAlignment(encoding, &valueSize, NULL);

            if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
                methodReturnsStructValue = NO;
            }
        } @catch (__unused NSException *e) {}
    }
    if (methodReturnsStructValue) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
#endif
    return msgForwardIMP;
}

所以我们可以总结下第6步这个方法做了什么事情:

  1. 创建了一个新的类,然后将这个新类的 forwardInvocation: 实现替换为了 __ASPECTS_ARE_BEING_CALLED__,并且将这个新类的类对象和元类的 class 修改为原来的类,最后将 self 所属的类修改为新类。

  2. 将 selector 对应的 IMP 实现替换为 _objc_msgForward_objc_msgForward_stret

以上介绍的1~6步,完成了 Hook 的动作。

ASPECTS_ARE_BEING_CALLED

在被 Hook 的对象执行被替换的方法时,就会转而执行 __ASPECTS_ARE_BEING_CALLED__ 这个方法。

ini 复制代码
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    NSCParameterAssert(self);
    NSCParameterAssert(invocation);
    SEL originalSelector = invocation.selector;
    // 给invocation.selector 的 name 添加了aspects_前缀
	SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
	
    invocation.selector = aliasSelector;
    
    // AspectsContainer 和 SEL 是在第3步中通过关联对象技术关联到一起的
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // 在第5步中,根据 options 的设置,AspectIdentifier 会被加入到container 的 beforeAspects、insteadAspects或afterAspects中
    
    // 先执行 Hook 时机为调用原方法之前的方法
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
    
        //如果 option 为替代原方法,那么这里执行替代方法,原方法不执行
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
    
        // 如果 option 不是替代原方法,那么在这里去执行原来的方法
        Class klass = object_getClass(invocation.target);
        do {
            // 我们在第6-3步中向新创建出的类通过class_addMethod添加了aliasSelector
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // 原方法已经执行完了,在这里执行 Hook 时机为调用原方法之后的方法
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);

    // 如果没有安装任何钩子函数(也就是没有进行方法 Hook),那么就调用原来的方法
    if (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

总结

总结下 Aspects Hook 方法的基本原理:

  1. 将 selector 对应的 IMP 修改为 _objc_msgForward_objc_msgForward_stret。这样在调用被 Hook 的 selector 时,会直接进入消息转发流程。

  2. forwardInvocation 对应的 IMP 修改为 __ASPECTS_ARE_BEING_CALLED__

  3. __ASPECTS_ARE_BEING_CALLED__ 中,调用原始实现并根据设置的 Hook 时机来执行额外的逻辑。

相关推荐
Magnetic_h13 小时前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...15 小时前
「iOS」——单例模式
ios·单例模式·cocoa
yanling202317 小时前
黑神话悟空mac可以玩吗
macos·ios·crossove·crossove24
归辞...19 小时前
「iOS」viewController的生命周期
ios·cocoa·xcode
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403551 天前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan1 天前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin2 天前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
名字不要太长 像我这样就好2 天前
【iOS】push和pop、present和dismiss
学习·macos·ios·objective-c·cocoa