美文网首页iOS知识栈
Aspects原理和使用

Aspects原理和使用

作者: Dolphii | 来源:发表于2019-08-24 19:03 被阅读0次

    前言

    在读这篇文章之前,需要对Runtime消息发送、消息转发有一定的了解。
    Runtime系列
    Objective-C消息发送与转发原理

    什么是Aspects

    Aspects是iOS一个高效简洁的用于AOP(面向切面编程)框架,它可以在类或者实例的方法中在前、在后添加代码,也可以替换这个方法。

    调用方法

    Aspects是NSObject的一个category,这样我们所有的需要修改方法类和实例都可以调用这两个方法

    @interface NSObject (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;
    
    @end
    

    selector需要修改的原方法,block是需要执行的代码块,error是hook失败的错误信息,options参数是修改的方法,参数如下:

    typedef NS_OPTIONS(NSUInteger, AspectOptions) {
        AspectPositionAfter   = 0,            /// Called after the original implementation (default)
        AspectPositionInstead = 1,            /// Will replace the original implementation.
        AspectPositionBefore  = 2,            /// Called before the original implementation.
        
        AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. 
    };
    

    AspectPositionAfter在原方法之后,AspectPositionInstead替换原方法,AspectPositionBefore在原方法之前执行,AspectOptionAutomaticRemoval是只hook一次就移除。

    实现原理

    上面的两个方法走的都是aspect_add方法,方法如下

    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;
    }
    

    1.首先需要判断参数的合法性

    我们看下aspect_isSelectorAllowedAndTrack判断方法的代码

    //检查hook
    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];
        });
    
        // Check against the blacklist.
        NSString *selectorName = NSStringFromSelector(selector);
        if ([disallowedSelectorList containsObject:selectorName]) {
            NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
            AspectError(AspectErrorSelectorBlacklisted, errorDescription);
            return NO;
        }
    
        // Additional checks. hook dealloc时只能使用AspectPositionBefore
        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;
        }
    
        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))) {
            Class klass = [self class];
            NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
            Class currentClass = [self class];
    
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if ([tracker subclassHasHookedSelectorName:selectorName]) {
                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;
            }
    
            do {
                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)));
    
            // Add the selector as being modified.
            currentClass = klass;
            AspectTracker *subclassTracker = nil;
            do {
                tracker = swizzledClassesDict[currentClass];
                if (!tracker) {
                    tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
                    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 {
            return YES;
        }
    
        return YES;
    }
    

    从上面可以看出类和实例对象必须可以响应传入的selector,另外有限制:

    • 不能hook retain、release、autorelease和forwardInvocation:。
    • hook dealloc方法只能使用AspectPositionBefore模式。
    • 类方法只能替换一次,是在整个类的继承树上校验,而不只是单单的一个类。

    2.获取容器保存hook内容

    获取(没有则创建)一个关联对象容器AspectsContainer,创建AspectIdentifier用来保存hook信息,然后把hook信息添加到容器中。

    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    [aspectContainer addAspect:identifier withOptions:options];
    
    
    // Loads or creates the aspect container.
    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;
    }
    
    + (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;
    }
    
    - (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;
        }
    }
    

    在创建AspectIdentifier前需要把block转换为方法签名,然后和原来的方法做比较,参数和返回类型都要匹配。这里面有个问题,如果多次执行hook方法,AspectIdentifier还是会持续加入,并没有判重复处理,所以执行hook方法可能会执行多次。

    3.hook类和方法

    前面准备工作做好了,接下来就要准备hook了。

    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)) {//判断被hook的方法本身是不是objc_msgForward
            // 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));
        }
    }
    

    首先需要hook类,然后再hook方法,我们接下来分开讲hook类和方法。

    3.1hook类

    我们看下static Class aspect_hookClass(NSObject *self, NSError **error)的源码

    static Class aspect_hookClass(NSObject *self, NSError **error) {
        NSCParameterAssert(self);
        //self是实例则返回它的Class,self是个Class则返回自身
        Class statedClass = self.class;
        //isa
        Class baseClass = object_getClass(self);
        NSString *className = NSStringFromClass(baseClass);
    
        // Already subclassed 已经子类化了
        if ([className hasSuffix:AspectsSubclassSuffix]) {
            return baseClass;
            //混写了一个class对象,而非一个单独的object
            // We swizzle a class object, not a single object.
        }else if (class_isMetaClass(baseClass)) {
            // baseClass 是元类,则 self 是 Class 或 MetaClass,混写 self
            return aspect_swizzleClassInPlace((Class)self);
            // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
            // 可能是一个 KVO'ed class。混写就位。也要混写 meta classes。
        }else if (statedClass != baseClass) {
            // 当 .class 和 isa 指向不同的情况,混写 baseClass
            return aspect_swizzleClassInPlace(baseClass);
        }
    
        // Default case. Create dynamic subclass.
        // 默认情况下,动态创建子类
        // 拼接子类后缀 AspectsSubclassSuffix
        const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
        Class subclass = objc_getClass(subclassName);
        
        // 找不到 isa,代表还没有动态创建过这个子类
        if (subclass == nil) {
            // 创建一个 class pair,baseClass 作为新类的 superClass,类名为 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:
            aspect_swizzleForwardInvocation(subclass);
            // subClass.class = statedClass
            aspect_hookedGetClass(subclass, statedClass);
            // subClass.isa.class = statedClass
            aspect_hookedGetClass(object_getClass(subclass), statedClass);
            // 注册新类
            objc_registerClassPair(subclass);
        }
        
        // 覆盖 isa
        object_setClass(self, subclass);
        return subclass;
    }
    

    从这里看出,对于类对象和实例对象其实是有区分。

    3.1.1类对象hook

    对于类对象直接走static Class aspect_swizzleClassInPlace(Class klass)方法,我们看下代码

    #pragma mark---*修改点[可以同时Hook类方法和实例方法]*
    static Class aspect_swizzleClassInPlace(Class klass) {
        NSCParameterAssert(klass);
        //    NSString *className = NSStringFromClass(klass); //这是apects原方法
        NSString *classIdentification = [NSString stringWithFormat:@"%@:%p",NSStringFromClass(klass),klass];
        
        _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
            if (![swizzledClasses containsObject:classIdentification]) {
                aspect_swizzleForwardInvocation(klass);
                [swizzledClasses addObject:classIdentification];
            }
        });
        return klass;
    }
    
    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));
    }
    

    这里把类的forwardInvocation的IMP指向了ASPECTS_ARE_BEING_CALLED。为什么要这样呢?因为后面会把原方法的IMP指向objc_forward,走的是消息转发,消息转发会走forwardInvocation方法,而IMP指向的ASPECTS_ARE_BEING_CALLED,最后走的就是ASPECTS_ARE_BEING_CALLED逻辑了。
    另外给类添加一个新方法__aspects_forwardInvocation:,这个方法的IMP指向的forwardInvocation的原有方法,为什么添加这个呢?这个方法ASPECTS_ARE_BEING_CALLED有用到,因为我们类可能添加了一些走消息转发的的方法,在forwardInvocation里面有对应的逻辑,这样没有hook的消息转发方法同样可以顺利执行,不会NotRecognize。
    aspects不可以同时Hook同一个类的类方法和实例方法, 这里做了修改,参考issues#145

    3.1.2实例对象hook

    我们先来看下实例对象的hook代码

        // 拼接子类后缀 AspectsSubclassSuffix
        const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
        Class subclass = objc_getClass(subclassName);
        
        // 找不到 isa,代表还没有动态创建过这个子类
        if (subclass == nil) {
            // 创建一个 class pair,baseClass 作为新类的 superClass,类名为 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: IMP
            aspect_swizzleForwardInvocation(subclass);
            // subclass和subclass的元类的class方法的IMP指向原来的类
            aspect_hookedGetClass(subclass, statedClass);
            aspect_hookedGetClass(object_getClass(subclass), statedClass);
            // 注册新类
            objc_registerClassPair(subclass);
        }
        
        // 覆盖 isa
        object_setClass(self, subclass);    
    

    这里创建了一个原类的子类,子类是原类名称后加上Aspects后缀,像类对象hook一样,子类的forwardInvocation的IMP指向ASPECTS_ARE_BEING_CALLED,添加__aspects_forwardInvocation方法,然后把子类的和子类的元类的class方法的IMP指向父类,然后实例对象的isa指针指向子类。

    3.2hook方法

    我们看下hook方法的代码

    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)) {//判断被hook的方法本身是不是objc_msgForward
            // 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));
        }
    }
    

    如果方法被hook过,给方法添加一个aspects_前缀的新方法,它的IMP是原方法的一样,然后把原方法的IMP指向_objc_msgForward或者_objc_msgForward_stret,整个hook完成。

    4.执行切面方法

    先看下切片方法代码

    // 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)];
    }
    

    顺序执行,先执行before数组的方法,然后偶执行替换方法或者原方法,再次执行after数组的方法。如果是没有hook的方法,如果没执行则走原类的消息转发的方法,如果没有则异常crash。

    总结

    简单总结Aspects就是先hook类(实例则生成一个子类),然后把forwardInvocation的IMP指向Aspects自定义的一个方法,并且添加一个新方法指向原来forwardInvocation的IMP,然后把原方法的IMP指向_objc_msgForward,从而我们调这个方法的时候走的是消息转发,最终执行的Aspects自定义的方法。

    不足

    • 不能hook retain、release、autorelease和forwardInvocation:
    • hook dealloc方法只能使用AspectPositionBefore模式
    • 如果目标方法中使用了super,不能使用使用AspectPositionInstead替代方法,只能用在前或者在后执行
    • 不能同时hook同一个类的类方法和实例方法(可修改源码解决issues#145
    • 同一个方法在整个类的继承树上只能被hook一次

    资料

    Runtime系列
    Objective-C消息发送与转发原理
    iOS 如何实现Aspect Oriented Programming

    相关文章

      网友评论

        本文标题:Aspects原理和使用

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