美文网首页
[iOS] Aspects原理分析

[iOS] Aspects原理分析

作者: 木小易Ying | 来源:发表于2019-11-30 22:59 被阅读0次

又到了愉快的双休周末了~ 在准备和妈妈出门买喜欢了两天的一双超级超级好看的MK运动鞋以前,偷偷开始看下Aspects库是怎么实现方法hook的~


Aspects是用于方法实现AOP的一个库,它的方法hook只要是通过类似酱紫的~ 当然你也可以改成用类来调用hook。

[self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
    NSLog(@"hook_viewWillAppear");
}

首先它能够直接让对象/类调用是因为这个库是以category的方式实现的,并且有类方法以及对象方法,也就是上面的对类/对象都可以hook:

@implementation NSObject (Aspects)

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Public Aspects API

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
}

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

我们看到里面都是通过aspect_add实现的,那么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(^{
        // 检查这个selector是不是允许被hook,例如retain之类的method是不允许的
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {

            // 获取一个aspectContainer,它会保存一个对象的before/instead/after的方法hook identifier
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);

            // AspectIdentifier类似一个model记录了是什么对象的哪个方法的样子
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {

                // 把identifier加入到aspectContainer的数组
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                // 真正的hook啦
                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);
}

不知道为啥木有换成别的lock,毕竟自旋锁是有可能因为忙等+线程优先级死锁的。。

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

    // retain,release,autorelease,forwardInvocation不允许hook
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
        AspectError(AspectErrorSelectorBlacklisted, errorDescription);
        return NO;
    }

    // dealloc只允许before hook
    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的selector也不行
    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
    // 如果当前要修改的是class而非对象,那么就会按下面的去看class层级问题
    if (class_isMetaClass(object_getClass(self))) {
        Class klass = [self class];

        // 这里其实dispatch_once建了一个mutableDict
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];

        // swizzledClassesDict以class为key保存了它的AspectTracker
        AspectTracker *tracker = swizzledClassesDict[currentClass];

        // 检查这个类的子类们有没有hook过这个selector
        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;
        }

        // 检查这个类的父类们有没有hook过这个selector
        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)));

        // 记录一下这个selector被hook了,告诉它的父类,你们的有个subclass hook了XXX,不断向上找直到superclass为空
        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;
    }

    return YES;
}

上面这一段其实就做了两件事情,一件事情是检查是不是要hook的方法有问题,包括autorelease、release、除了before的dealloc、不能响应的selector;另一个就是如果hook的是class,那么就要检查它是不是已经被子类/父类hook过了,如果没有就要标记一下这次会hook这个类的这个selector,并告诉它的父类们。

比较优秀的一点是,我们其实很容易找到自己的父类们,但是不能找到自己的子类们,所以这里是用了AspectTracker来记录了自己的子类被hook了的方法,方便查找子类是不是已经被hook过了。

@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName;
- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName;
@end

它其实也类似一个model记录了自己被hook过的selector,以及用selectorNamesToSubclassTrackers记录了subclass的tracker,形成了一个类似树的结构,可以逐层下查。

如果我们希望记录类的层级结构,可以用Set/Array/Dict记录它的直属子类,逐级下询即可遍历。

③ AspectsContainer

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

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

static NSString *const AspectsMessagePrefix = @"aspects_";

aspect_getContainerForObject通过aliasSelector为key查找对应的container,这个dictionary作为关联对象保存给了当前对象。

那么Container包含什么呢?

@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

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

可以看到其实container就是三个数组,分别记录了self.beforeAspects、self.insteadAspects、self.afterAspects三个切入点的AspectIdentifier。

④ AspectIdentifier

它其实就是一个记录了哪个类的哪个selector被怎么切入了什么样的block……

@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

⑤ aspect_prepareClassAndHookSelector真正的hook~

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);

    // hook class
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);

    // targetMethodIMP不是_objc_msgForward
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);

        // 形成一个新的方法aliasSelector
        SEL aliasSelector = aspect_aliasForSelector(selector);

        // 如果aliasSelector还没加过
        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.
        // 用_objc_msgForward替代原有selector对应的IMP
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

关于什么是_objc_msgForward可以参考https://www.jianshu.com/p/be4f4c6aa6a4,其实就是开始消息转发的IMP,之后会按照消息转发机制逐步尝试让object resolve消息。

这一段其实就是首先将原有selector对应的IMP,作为aliasSelector对应的IMP给object先加上去,然后将selector对应的IMP用_objc_msgForward替代,这样当object的selector被调用的时候,其实真正被执行的是消息转发_objc_msgForward。


  • 这里aspect_hookClass做了什么呢?
static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
    // 已经是被hook创建过subclass的了,直接return当前class即可
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;

        // We swizzle a class object, not a single object.
    // 如果是hook一个类对象,而非实例对象,这段就是把这个class的forwardInvocation替换为ASPECTS_ARE_BEING_CALLED
    }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);
    }

    // Default case. Create dynamic subclass.
    // 创建一个subclass,父类是当前对象的类
    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;
        }

        // 把新创建的class的forwardInvocation的IMP替换为__ASPECTS_ARE_BEING_CALLED__
        aspect_swizzleForwardInvocation(subclass);
        
        // 让subclass的@selector(class)返回statedClass,也就是当你调用class方法时会返回statedClass
        aspect_hookedGetClass(subclass, statedClass);
        
        // 让subclass的metaclass的@selector(class)返回statedClass,也就是当你调用class方法时会返回statedClass
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }

    // 让self的isa指向subclass
    object_setClass(self, subclass);
    return subclass;
}

objc_allocateClassPair可以用于创建子类哦~ self.class和object_getClass之类的区别我准备放小知识点的文章里面啦,其实上面这段就是给当前的class加一个subclass,然后让当前对象的isa指针指向新建的subclass,和KVO的实现很像。

并且将当前subclass的forwardInvocation方法替换为ASPECTS_ARE_BEING_CALLED

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

  • ASPECTS_ARE_BEING_CALLED做了什么呢?
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    NSCParameterAssert(self);
    NSCParameterAssert(invocation);
    SEL originalSelector = invocation.selector;
    
    // 之前如果hook了的method都把真实的IMP换给aliasSelector了
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    
    // 获取实例对象的AspectsContainer以及class的AspectsContainer
    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.
    // 调用beforeAspects里面的aspect们
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        // 调用insteadAspects里面的aspect们
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        // 不断向上找superclass,直到可以响应aliasSelector,调用后break
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks.
    // 调用afterAspects里面的aspect们
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);

    // If no hooks are installed, call original implementation (usually to throw an exception)
    // 如果之前没有找到可响应aliasSelector的,则调用原有的消息转发机制AspectsForwardInvocationSelectorName
    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.
    // 有些aspects调用一次以后就自动remove,这个是option为AspectOptionAutomaticRemoval的
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

这一段比较简洁,就是分别在before、instead以及after的地方切入aspect,并且invoke,然后最后如果始终没响应这个selector,就调用之前存起来的原有的消息转发把他抛出去。


  • 如何invoke的aspect?
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
    [aspect invokeWithInfo:info];\
    if (aspect.options & AspectOptionAutomaticRemoval) { \
        aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
    } \
}

这里可以看出来如果要是aspect.options为AspectOptionAutomaticRemoval那么只要invoke一次以后就被放入了aspectsToRemove,之后就会被移出啦~

对应定义:

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

invoke的部分是酱紫的(偷懒抄别人的了。。)

- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
     #根据block签名生成block的invocation
    NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
     #这个是原方法的invocation
    NSInvocation *originalInvocation = info.originalInvocation;
    NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
    #检测两个invocation的参数必须匹配
    // 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;
    }
    #把id<AspectInfo> info先作为第二个参数,第一个参数为self,block的方法没有_cmd
    // The `self` of the block will be the AspectInfo. Optional.
    if (numberOfArguments > 1) {
        [blockInvocation setArgument:&info atIndex:1];
    }
    
    void *argBuf = NULL;
      #这个地方是从2开始,因为我们平时的方法其实会有两个隐形的参数self和_cmd,这了两个参数占了0和1的位置
    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;
        }
                #把原方法的参数传递给block
        [originalInvocation getArgument:argBuf atIndex:idx];
        [blockInvocation setArgument:argBuf atIndex:idx];
    }
    #执行block
    #因为block的本质就是一个对象,内部保存着函数指针,
    #所以它执行的target就是自己,不需要通过selector 
   找到方法名称。
    [blockInvocation invokeWithTarget:self.block];
    
    if (argBuf != NULL) {
        free(argBuf);
    }
    return YES;
}

这里注意的是,hook的selector需要和你block的参数保持一致哦~ 另外一个小tip~

block的第一个参数默认是self;消息的第一个参数是self,第二是_cmd~


总的而言这个库利用了消息转发+各种runtime来实现了AOP,步骤如下:

  1. 给需要hook的类加subclass,并将当前object的isa指向新的subclass
  2. 交换需要hook的selector的IMP给alisaSelector,并且将_objc_msgForward消息转发的IMP替换给hook的selector
  3. 用ASPECTS_ARE_BEING_CALLED替换subclass的forwardInvocation
  4. 当需要被hook的selector调用的时候,会直接进行消息转发,依次切入before/instead/after的aspect

有些例如hook的不是对象是class的话会有些许差别,但总的其实就是利用消息转发~ 我比较喜欢的一点是它的subclass记录方式,在将来有一些下询需求的时候可以借鉴~

想了解更多可以参考感觉写的比我好多了hhh:
https://www.jianshu.com/p/a9bca76d1635 或者 https://www.jianshu.com/p/51690cdf1914

相关文章

网友评论

      本文标题:[iOS] Aspects原理分析

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