美文网首页
Aspects源码解析

Aspects源码解析

作者: ADreamClusive | 来源:发表于2020-03-29 00:04 被阅读0次

    1. 简介

    Aspects是一个令人愉悦的、简单的面向切面编程的库。

    可以把 Aspects 看成是一个 method swizzling 。 可以为一个已经存在的类或实例方法添加 code ,我们只需要考虑 code 的插入点即可: before、 instead、 after。Aspects 可以自动为我们调用 super 的方法(也即是原来的方法)。

    通过类方法调用不会产生新的子类、通过当前类的对象调用注册aspect的方法会产生当前类的子类,并将当前对象所属类设置为子类。

    Aspects 像 KVO 一样通过创建动态子类来实现上述功能。 这种方式仍然存在一些已知问题,并不建议在生产代码中使用 Aspects

    Aspects 使用了 _objc_msgForward,这会导致使用消息转发的其他代码出现问题。

    由于 Aspects 使用消息转发实现 hook,这将产生一些开销。 建议不要在调用很频繁的方法上增加 aspects。 Aspects 主要用于 View/Controller 的每秒调用次数少于 1000 次的代码。

    2. 从入口处看 Aspects

    /// Adds a block of code before/instead/after the current `selector` for a specific class.
    ///
    /// @param block Aspects replicates the type signature of the method being hooked.
    /// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
    /// These parameters are optional and will be filled to match the block signature.
    /// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
    ///
    /// @note Hooking static methods is not supported.
    /// @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;
    
    /// 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;
    

    整个 Aspects 提供两个调用入口, 一个是供类调用/一个是供类的实例调用;但 Aspects 并不能 hook 类方法;这两个方法都会返回一个 AspectToken , 通过这个 token 可以在适当的位置取消注册的 aspect, 所有的调用都是线程安全的。

    这两个方法的 block 是可以传递任何参数的,也可以为空,也可以带多个参数,第一个参数类型必须是 id<AspectInfo> ,后面的每个参数与被 hook 的参数顺序一致, 后面的参数个数不能多余被 hook 方法的参数个数。

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

    3. 使用注意

    • 具有层级关系的两个类的同一个方法,只能 hook 一个;

    比如: A、B两个类, A 是 B 的父类, 他们都有一个方法: - (void)add:(int a, int b);, 只能 hook 父类 或 子类的- (void)add:(int a, int b); 方法。

    • 如果 KVO 是在 aspect_hookSelector 调用之后调用的,可以正常工作; 如果 KVO 是在 hook 方法之前调用, 很可能会崩溃。

    • 由于ObjC运行时的实现细节,返回类型为包含结构体的联合体的方法可能无法正常工作,除非此代码在arm64运行时上运行。

    4. 内部细节窥探

    0. 注册 aspect 所进行的检查及类的层级追踪

    • Aspects 默认不允许注册 aspect 的列表为: disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];

    • 在新设置 aspect 时, 首先会检查是否在黑名单中。

    • dealloc 只允许 hook 的位置为 before;

    • 如果当前对象或当前类的对象不能响应该方法,不允许hook;

    • 如果当前调用者为class

      搜索当前类的子类是否Hook过该方法
          
      依次检查当前类的父类是否hook过该方法
          
      设置所有的父类tracker, 让其知晓有一个子类hook了某个selector
      

    此外,还会对block和method的方法签名进行校验, 不符合要求会注册 aspect 失败。

    1. 注册 aspect

    static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
        NSCParameterAssert(self);
        NSCParameterAssert(selector);
        NSCParameterAssert(block);
    
        __block AspectIdentifier *identifier = nil;
        aspect_performLocked(^{
            if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
                AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
                identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
                if (identifier) {
                    [aspectContainer addAspect:identifier withOptions:options];
    
                    // Modify the class to allow message interception.
                    aspect_prepareClassAndHookSelector(self, selector, error);
                }
            }
        });
        return identifier;
    }
    
    

    aspect_performLocked 通过加锁的方式保证内部代码的执行是线程安全的,其内部使用的 OSSpinLock 实现 (这是一个被爆料不安全的锁)。

    static void aspect_performLocked(dispatch_block_t block) {
        static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
        OSSpinLockLock(&aspect_lock);
        block();
        OSSpinLockUnlock(&aspect_lock);
    }
    
    1. 通过 aspect_isSelectorAllowedAndTrack 判断 Selector 是否允许 Track ;
    2. 调用 aspect_getContainerForObject 获得一个和当前对象绑定的 AspectsContainer 容器;
    3. 使用 objcect/selector/block/options/error 所有参数生成一个AspectIdentifier
    4. AspectIdentifier 根据 options 添加到 AspectsContainer 中;
    5. 修改 class 实现消息拦截;(aspect_prepareClassAndHookSelector(self, selector, error)

    2. 接下来重点看一下 aspect_prepareClassAndHookSelector 做了什么

    static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
        NSCParameterAssert(selector);
        Class klass = aspect_hookClass(self, error);
        Method targetMethod = class_getInstanceMethod(klass, selector);
        IMP targetMethodIMP = method_getImplementation(targetMethod);
        if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
            // 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]) {
                __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);
            }
    
            // We use forwardInvocation to hook in.
            class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
            AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
        }
    }
    
    1. 获取 hook 后的子类;

    2. 在 子类 中获取 method 及对应 methodIMP

    3. 判断 当前 IMP 是否为消息转发 IMP _objc_msgForward, 如果不是,执行 hook;

    4. 使用 forwardInvocation 来 hook 当前 Selector

      这一步之后,每次调用 selector 就会直接触发 消息转发 函数, 跳转到 Aspects hook的 消息转发处理函数 : __ASPECTS_ARE_BEING_CALLED__

    3. __ASPECTS_ARE_BEING_CALLED__ 函数做了什么?

    // This is the swizzled forwardInvocation: method.
    static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
        NSCParameterAssert(self);
        NSCParameterAssert(invocation);
        SEL originalSelector = invocation.selector;
        SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
        invocation.selector = aliasSelector;
        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;
    
        // Before hooks.
        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 {
            Class klass = object_getClass(invocation.target);
            do {
                if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                    [invocation invoke];
                    break;
                }
            }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
        }
    
        // After hooks.
        aspect_invoke(classContainer.afterAspects, info);
        aspect_invoke(objectContainer.afterAspects, info);
    
        // If no hooks are installed, call original implementation (usually to throw an exception)
        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];
            }
        }
    
        // Remove any hooks that are queued for deregistration.
        [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
    }
    
    • 获取与当前 调用者 或 其所属类 绑定的 AspectsContainer
    • 分别遍历 classContainer.beforeAspects/classContainer.insteadAspects/classContainer.afterAspects 中的 AspectIdentifier 执行 invokeWithInfo:
    • 判断当前对象是否响应方法,如果不能响应,则会触发系统默认的消息转发;
    • 最后,移除 AspectOptions 为 AspectOptionAutomaticRemoval 类型的 aspect

    4. invokeWithInfo: 调用注册 aspect 时设置的 block

    - (BOOL)invokeWithInfo:(id<AspectInfo>)info {
        NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
        NSInvocation *originalInvocation = info.originalInvocation;
        NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
    
        // Be extra paranoid. We already check that on hook registration.
        if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
            AspectLogError(@"Block has too many arguments. Not calling %@", info);
            return NO;
        }
    
        // The `self` of the block will be the AspectInfo. Optional.
        if (numberOfArguments > 1) {
            [blockInvocation setArgument:&info atIndex:1];
        }
        
        void *argBuf = NULL;
        for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
            const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
            NSUInteger argSize;
            NSGetSizeAndAlignment(type, &argSize, NULL);
            
            if (!(argBuf = reallocf(argBuf, argSize))) {
                AspectLogError(@"Failed to allocate memory for block invocation.");
                return NO;
            }
            
            [originalInvocation getArgument:argBuf atIndex:idx];
            [blockInvocation setArgument:argBuf atIndex:idx];
        }
        
        [blockInvocation invokeWithTarget:self.block];
        
        if (argBuf != NULL) {
            free(argBuf);
        }
        return YES;
    }
    
    • 通过将 block 转为 NSInvocation 来调用

      blockInvocation invokeWithTarget:self.block

    5. aspect 的移除操作

    // Will undo the runtime changes made.
    static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) {
        NSCParameterAssert(self);
        NSCParameterAssert(selector);
    
        Class klass = object_getClass(self);
        BOOL isMetaClass = class_isMetaClass(klass);
        if (isMetaClass) {
            klass = (Class)self;
        }
    
        // Check if the method is marked as forwarded and undo that.
        Method targetMethod = class_getInstanceMethod(klass, selector);
        IMP targetMethodIMP = method_getImplementation(targetMethod);
        if (aspect_isMsgForwardIMP(targetMethodIMP)) {
            // Restore the original method implementation.
            const char *typeEncoding = method_getTypeEncoding(targetMethod);
            SEL aliasSelector = aspect_aliasForSelector(selector);
            Method originalMethod = class_getInstanceMethod(klass, aliasSelector);
            IMP originalIMP = method_getImplementation(originalMethod);
            NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
    
            class_replaceMethod(klass, selector, originalIMP, typeEncoding);
            AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
        }
    
        // Deregister global tracked selector
        aspect_deregisterTrackedSelector(self, selector);
    
        // Get the aspect container and check if there are any hooks remaining. Clean up if there are not.
        AspectsContainer *container = aspect_getContainerForObject(self, selector);
        if (!container.hasAspects) {
            // Destroy the container
            aspect_destroyContainerForObject(self, selector);
    
            // Figure out how the class was modified to undo the changes.
            NSString *className = NSStringFromClass(klass);
            if ([className hasSuffix:AspectsSubclassSuffix]) {
                Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]);
                NSCAssert(originalClass != nil, @"Original class must exist");
                object_setClass(self, originalClass);
                AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass));
    
                // We can only dispose the class pair if we can ensure that no instances exist using our subclass.
                // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around.
                //objc_disposeClassPair(object.class);
            }else {
                // Class is most likely swizzled in place. Undo that.
                if (isMetaClass) {
                    aspect_undoSwizzleClassInPlace((Class)self);
                }else if (self.class != klass) {
                    aspect_undoSwizzleClassInPlace(klass);
                }
            }
        }
    }
    
    • aspect 的 remove 操作 核心函数为 aspect_cleanupHookedClassAndSelector
    • 恢复系统默认的消息转发函数
    • 取消对selector的全局追踪
    • 获取 AspectsContainer ,并销毁 当前 selector 对应的container
    • 将当前对象所属类设置为原来的类 object_setClass(self, originalClass);; 或者在全局 swizzledClasses 中移除当前类

    大概流程就是这样!!!有一些细节没有讲到可以查看Aspects源码!

    相关文章

      网友评论

          本文标题:Aspects源码解析

          本文链接:https://www.haomeiwen.com/subject/zkoeuhtx.html