Aspects源码分析

Posted FarmGuo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Aspects源码分析相关的知识,希望对你有一定的参考价值。

Aspects是一个用来切片编程的开源框架,提供了丰富接口,可以Hook类和单个对象的方法,并提供了原实现前Hook,替换原实现,原实现后Hook等选项。

1 补充知识点

Objective-C里面的方法调用,我们看到的第1个参数,实际上是第3个参数,前2个参数是隐形参数,第1个是self(encode后是@),即实例对象或者类对象,第2个是Selector(encode后是#),即方法名。
Block有些类似,我们看到的第1个参数,实际上是第2个参数,前1个参数是隐形参数,即Block自身,encode后是@?。

2 简单流程

不管是Hook类的还是Hook某个对象的,基本流程都是将原SEL指向objc_msgForward,使其走消息转发的流程,然后将原实现保存在aliseSEL中。然后将forwardInnovation的实现设为自定义的函数,如果原来有forwardInnovation的实现,则将其保存在AspectsForwardInvocationSelectorName这个SEL中。

Aspects做了很多其他的操作来保证安全性和稳定性。

1 先判断方法是否能被Hook

这部分位于aspect_isSelectorAllowedAndTrack中

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.
    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];
        do 
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) 

                // Find the topmost class for the log.
                if (tracker.parentEntry) 
                    AspectTracker *topmostEntry = tracker.parentEntry;
                    while (topmostEntry.parentEntry) 
                        topmostEntry = topmostEntry.parentEntry;
                    
                    NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
                    AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                    return NO;
                else if (klass == currentClass) 
                    // Already modified and topmost!
                    return YES;
                
            
        while ((currentClass = class_getSuperclass(currentClass)));

        // Add the selector as being modified.
        currentClass = klass;
        AspectTracker *parentTracker = nil;
        do 
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if (!tracker) 
                tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
                swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
            
            [tracker.selectorNames addObject:selectorName];
            // All superclasses get marked as having a subclass that is modified.
            parentTracker = tracker;
        while ((currentClass = class_getSuperclass(currentClass)));
    

    return YES;

首先就是判断retain、release、autorelease等等这些禁止hook的方法。
对于dealloc则只能替换或后Hook。
对于不响应的方法不Hook。
最后是判断是否已经Hook过了该方法。但个人认为这里有点瑕疵,会产生一个问题,当2个分别继承自NSObject的类有着相同的方法名,Hook了其中一个类的方法后,另一个类的就不能Hook了。

2 替换后的Block和原实现的参数是否匹配

这部分判断位于aspect_isCompatibleBlockSignature中,
首先Block是一种特殊的结构体,首个变量是和其他对象一样都是isa,这使得Block也可以响应方法的调用。在Block结构体的最后保存着Block的签名,这个签名和方法的签名是一样的。参数的匹配就是比较2个签名的参数个数和参数类型的匹配。

static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) 
    BOOL signaturesMatch = YES;
    NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
    if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) 
        signaturesMatch = NO;
    else 
        if (blockSignature.numberOfArguments > 1) 
            const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
            if (blockType[0] != '@') 
                signaturesMatch = NO;
            
        
        // Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
        // The block can have less arguments than the method, that's ok.
        if (signaturesMatch) 
            for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) 
                const char *methodType = [methodSignature getArgumentTypeAtIndex:idx];
                const char *blockType = [blockSignature getArgumentTypeAtIndex:idx];
                // Only compare parameter, not the optional type data.
                if (!methodType || !blockType || methodType[0] != blockType[0]) 
                    signaturesMatch = NO; break;
                
            
        
    

    if (!signaturesMatch) 
        NSString *description = [NSString stringWithFormat:@"Blog signature %@ doesn't match %@.", blockSignature, methodSignature];
        AspectError(AspectErrorIncompatibleBlockSignature, description);
        return NO;
    
    return YES;


3 进行Hook

对于类的Hook走的是简单流程,但对于对象的Hook,因为为了不影响其他的对象,处理流程上有些不同。
以要Hook的实例对象的类为父类,新生成一个子类,然后将这个子类的forwardInvocation实现改为自定义的函数,同时替换这个子类的-class和+class方法,让其都返回要Hook的实例对象的类。然后修改将要Hook的实例对象的isa,将其isa指向这个子类。然后再将这个子类的原方法的实现改为objc_msgSend,并将原实现保存在aliseSEL中。总结起来就是Hook类和Hook某个对象的方法类似,不同的是,Hook对象需要额外生成一个子类,而后所有的修改操作都在这个子类上进行。

以上是关于Aspects源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Aspects源码分析

全面谈谈Aspects和JSPatch兼容问题

全面谈谈Aspects和JSPatch兼容问题

Aop轻量级框架:Aspects

[AOP] 3. Spring AOP中提供的种种Aspects - Tracing相关

iOS AOP框架Aspects实现原理