zoukankan      html  css  js  c++  java
  • Aspects框架的源码解读及问题解析

    前言

    在iOS日常开发中,对某些方法进行hook是很常见的操作。最常见的是使用Category在+load中进行方法swizzle,它是针对类的,会改变这个类所有实例的行为。但是有时候我们只想针对单个实例进行hook,这种方法就显得无力了。而Aspects框架可以搞定这个问题。 它的原理是通过Runtime动态的创建子类,把实例的isa指针指向新创建的子类,然后在子类中对hook的方法进行处理,这样就支持了对单个实例的hook。Aspects框架支持对类和实例的hook,API很易用,可以方便的让你在任何地方进行hook,是线程安全的。但是Aspects框架也有一些缺陷,一不小心就会掉坑里面,我会通过源码解析进行说明。

    源码解析

    我主要使用图示对Aspects的源码进行说明,建议参考源码一起查看。要看懂这些内容,需要对isa指针消息转发机制runtime有一定的了解,本文中不会对这些内容展开来讲,因为要把这些东西讲清楚,每一项都需要单独写一篇文章了。

    主要流程解析

    1. 它第一个流程是使用关联对象添加Container,在这个过程中会进行一些前置条件的判断,例如这个方法是否支持被hook等,如果条件验证通过,就会把这次hook的信息保存起来,在方法调用的时候,查询出来使用。
    2. 第二个流程是动态创建子类,如果是针对类的hook,则不会走这一步。
    3. 第三步是替换这个类的forwardInvocation:方法为__ASPECTS_ARE_BEING_CALLED__,这个方法内部会查找到之前创建的Container,然后根据Container中的逻辑进行实际的调用。
    4. 第四步是将原有方法的IMP改为_objc_msgForward,改完后当调用原有方法时,就会调用_objc_msgForward,从而触发forwardInvocation:方法。

    我对它的流程做了一个简化的图示,标有每个流程的序号,后面会对每个流程进行解析。流程如下:

    图示中的取出对象类型,是指的调用hook的对象的类型,如果是实例对象,那么就走路径;如果是对象,则走元类路径;如果是kvo等实际类型不一致的情况,则走其它子类路径。

    ①添加Container流程

    这个流程中,把hook的逻辑封装成Container,并使用关联对象进行保存。这个过程中会判断hook的方法是否被支持、判断被hook类的继承关系、验证回调block正确性等操作。具体图示如下:

    关键代码如下:

    static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
        ...
        aspect_performLocked(^{ // 加锁
            // hook前置条件判断
            if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
                // 用selector作key,通过关联对象获得Container对象。
                AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
                // 内部会判断block与hook的selector是否匹配,不匹配返回nil。
                identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
                if (identifier) {
                    // 添加identifier,包含了hook的类型和回调。 
                    [aspectContainer addAspect:identifier withOptions:options];
    
                    // Modify the class to allow message interception.
                    aspect_prepareClassAndHookSelector(self, selector, error);
                }
            }
        });
        return identifier;
    }
    
    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];
        });
    
        // 这里对不支持hook的方法进行过滤
        NSString *selectorName = NSStringFromSelector(selector);
        if ([disallowedSelectorList containsObject:selectorName]) {
            NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
            AspectError(AspectErrorSelectorBlacklisted, errorDescription);
            return NO;
        }
    
        // 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;
        }
    
        // 这里禁止有继承关系的类hook同一个方法,代码量较多,不是关键内容,这里不贴出
        if (class_isMetaClass(object_getClass(self))) {
            ...
        }
    
        return YES;
    }
    
    /// AspectsContainer内部添加AspectIdentifier的实现。
    /// 这里可以看出对同一个方法的多次hook都会被调用,不会出现后面hook的覆盖前面的情况。
    - (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;
        }
    }
    
    复制代码
    1. 从源码中可以看到,不支持的hook方法有[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];。其中retainreleaseautorelease在arc下是被禁用的,框架本身是hookforwardInvocation:进行实现的,所以对它的hook也不支持。
    2. dealloc只支持AspectPositionBefore类型,使用AspectPositionInstead会导致系统默认的dealloc操作被替换无法执行而出现问题。 AspectPositionAfter类型,调用时对象可能已经已经被释放了,从而引发野指针错误。
    3. Aspects禁止有继承关系的类hook同一个方法,具体可以参见它的一个issue,它报告了这样操作会导致死循环,我会在文章后面再进行说明。
    4. Aspects使用block进行hook的调用,涉及到方法参数的传递和返回值问题,所以其中会对block进行校验。

    ②runtime创建子类

    iOS中的KVO就是通过runtime动态创建子类,然后在子类中重写对应的setter方法来实现的,Aspects支持对单个实例的hook原理与此有一些类似。图示如下:具体说明请查看源码中的注释

    // 执行hook
    static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
        NSCParameterAssert(selector);
        // 针对实例类型,会通过runtime动态创建子类。类类型则直接hook。
        Class klass = aspect_hookClass(self, error);
        ...
    }
    
    static Class aspect_hookClass(NSObject *self, NSError **error) {
        NSCParameterAssert(self);
        Class statedClass = self.class;
    	Class baseClass = object_getClass(self);
    	NSString *className = NSStringFromClass(baseClass);
    
        // 已经被hook过的类,直接返回
    	if ([className hasSuffix:AspectsSubclassSuffix]) {
    		return baseClass;
    
        // 是元类(MetaClass),则代表是对类进行hook。(非单个实例)
    	}else if (class_isMetaClass(baseClass)) {
            // 内部是将类的forwardInvocation:方法替换为__ASPECTS_ARE_BEING_CALLED__
            return aspect_swizzleClassInPlace((Class)self);
        // 可能是一个KVO对象等情况,传入实际的类型进行hook。
        }else if (statedClass != baseClass) {
            return aspect_swizzleClassInPlace(baseClass);
        }
    
        // 单个实例的情况,动态创建子类进行hook.
    	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;
            }
            // 内部是将类的forwardInvocation:方法替换为__ASPECTS_ARE_BEING_CALLED__
    		aspect_swizzleForwardInvocation(subclass);
            // 重写class方法,返回之前的类型,而不是新创建的子类。避免hook后,类型判断出现问题。
    		aspect_hookedGetClass(subclass, statedClass);
    		aspect_hookedGetClass(object_getClass(subclass), statedClass);
    		objc_registerClassPair(subclass);
    	}
    
    	object_setClass(self, subclass);
    	return subclass;
    }
    
    复制代码

    ③替换forwardInvocation:

    这部分就是把原有的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));
    }
    复制代码

    替换后的对应关系图示如下:

    ④hook方法交换IMP:

    图示如下:

    第③步和第④步可能有些同学会感到疑惑,为什么要替换forwardInvocation以及为什么要将hook的方法的IMP替换为_objc_msgForward,这个和iOS的消息转发机制有关,可以自行查找相关资料,这里就不做说明了。需要注意的是有些框架也是通过iOS的消息发送机制来做一些操作,例如JSPatch,使用的时候需要注意,避免发生冲突。

    被hook方法的调用流程

    当hook注入后,对hook方法进行调用时,调用流程就会发生变化。图示如下:

    从上述解析过程中,我们可以看到Aspects这个框架是设计的很巧妙的,从中可以看到非常多runtime知识的应用。但是作者并不推荐在实际项目中进行使用:

    因为Apsects对类的底层进行了修改,这种修改是基础方面的修改,需要考虑到各种场景和边界问题,一旦某方面考虑不周,就会引发出一些未知问题。另外这个框架是有缺陷的,很久没有进行更新了,我对它的已知问题点进行了总结,在下面进行说明。如果有未总结到位的,欢迎补充。

    问题点

    基于类的hooking,同一条继承链条上的所有类,一个方法只能被hook一次,后hook的无效。

    之前这样会出现死循环,后面作者进行了修改,对这个行为进行了禁止并加了错误提示。详见这个issue

    @interface A : NSObject
    - (void)foo;
    @end
    
    @implementation A
    - (void)foo {
        NSLog(@"%s", __PRETTY_FUNCTION__);
    }
    @end
    
    @interface B : A @end
    
    @implementation B
    - (void)foo {
        NSLog(@"%s", __PRETTY_FUNCTION__);
        [super foo]; // 导致死循环的代码
    }
    @end
    
    int main(int argc, char *argv[]) {
        [B aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
            NSLog(@"before -[B foo]");
        }];
        [A aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
            NSLog(@"before -[A foo]");
        }];
    
        B *b = [[B alloc] init];
        [b foo]; // 调用后死循环
    }
    复制代码

    我们都知道,super是从它的父类开始查找方法,然后传入self进行调用。 根据我们之前对源码的解析,在这里调用[super foo]后会从父类查找fooIMP,查到后发现父类的IMP已经被替换为_objc_msgForward,然后传入self调用。 因为是传入的self,所以实际会调用到它自身的forwardInvocation:,这样就导致了死循环。

    针对单个实例的hook,hook后使用kvo没问题,使用kvo后hook会出现问题。

    这里通过代码进行说明,以Animal对象为例:

    @interface Animal : NSObject
    @property(strong, nonatomic) NSString * name;
    @end
    
    @implementation Animal
    - (void)testKVO {
        [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        self.name = @"Animal";
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"observeValueForKeyPath keypath:%@ name:%@", keyPath, self.name);
    }
    
    - (void)dealloc {
        [self removeObserver:self forKeyPath:@"name"];
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Animal *animal = [[Animal alloc] init];
            [animal testKVO];
            // 这里如果改为针对类进行hook,则不会存在问题,因为类hook修改的是Animal类,而实例hook修改的是NSKVONotifying_Animal类
            [animal aspect_hookSelector:@selector(setName:) 
                            withOptions:AspectPositionAfter 
                             usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
                NSLog(@"aspects hook setName");
            } error:nil];
            // 这里会crash
            animal.name = @"ChangedAnimalName";
        }
    }
    复制代码

    异常原因分析图示如下:

    上面是继承链和方法调用流程的图示,可以看出,_NSSetObjectValueAndNotify是被aspects__setName:调用的,_NSSetObjectValueAndNotify的内部实现逻辑是取调用它的selector,去父类查找方法,即aspects__setName:方法,而Animal对象并没有这个方法的实现,这就导致了crash。

    与category的共存问题

    先用aspects进行hook,再使用category进行hook,会导致crash。反之则没有问题。样例代码如下:

    @interface Animal : NSObject
    @property(strong, nonatomic) NSString * name;
    @end
    
    @implementation Animal
    - (void)setName:(NSString *)name {
        NSLog(@"%s", __func__);
        _name = name;
    }
    @end
    
    @interface Animal(hook)
    + (void)categoryHook;
    @end
    
    @implementation Animal(hook)
    + (void)categoryHook {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [super class];
            SEL originalSelector = @selector(setName:);
            SEL swizzledSelector = @selector(lx_setName:);
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            method_exchangeImplementations(originalMethod, swizzledMethod);
        });
    }
    
    - (void)lx_setName:(NSString *)name {
        NSLog(@"%s", __func__);
        [self lx_setName:name];
    }
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Animal *animal = [[Animal alloc] init];
            [Animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
                NSLog(@"aspects hook setName");
            } error:nil];
            
            [Animal categoryHook];
            // 调用后crash:[Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0
            animal.name = @"ChangedAnimalName";
        }
    }
    复制代码

    这个与__ASPECTS_ARE_BEING_CALLED__的内部逻辑有关,里面会对调用的方法添加前缀aspect__进行调用,以调用到原始的IMP,但是category hook后破坏了这个流程。图示如下:

    根据上述图示,实际只有aspects__setName,没有aspects__lx_setName,导致找不到方法而crash

    基于类的hook,如果对同一个类同时hook类方法和实例方法,那么后hook的方法调用时会crash。样例代码如下:

    @interface Animal : NSObject
    - (void)testInstanceMethod;
    + (void)testClassMethod;
    @end
    
    @implementation Animal
    - (void)testInstanceMethod {
        NSLog(@"%s", __func__);
    }
    + (void)testClassMethod {
        NSLog(@"%s", __func__);
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Animal *animal = [[Animal alloc] init];
            [Animal aspect_hookSelector:@selector(testInstanceMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
                NSLog(@"aspects hook testInstanceMethod");
            } error:nil];
            
            [object_getClass([Animal class]) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
                NSLog(@"aspects hook testClassMethod");
            } error:nil];
            
            [animal testInstanceMethod];
            // crash: "+[Animal testClassMethod]: unrecognized selector sent to class 0x1000114a0"
            [Animal testClassMethod];
        }
    }
    复制代码

    这样的调用在日常开发中非常正常,但是它会导致crash。它是由于aspect_swizzleClassInPlace方法中的逻辑缺陷导致的。

    static Class aspect_swizzleClassInPlace(Class klass) {
        NSCParameterAssert(klass);
        // Animal类对象与Animal元类对象会得到同一个字符串。
        NSString *className = NSStringFromClass(klass);
        NSLog(@"aspect_swizzleClassInPlace %@ %p", klass, object_getClass(klass));
        _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
            // 类对象和元类对象得到同一个className,这里后加入的会被错误的过滤掉。
            if (![swizzledClasses containsObject:className]) {
                aspect_swizzleForwardInvocation(klass);
                [swizzledClasses addObject:className];
            }
        });
        return klass;
    }
    复制代码

    从上述代码可以看到,它的去重逻辑只是简单的字符串判断,取Animal的元类名得到同一个字符串Animal,导致后添加的被过滤,当调用后被hook的方法后,执行_objc_msgForward,因为后hook的aspect_swizzleForwardInvocation被过滤了没有执行,所以找不到forwardInvocation:IMP,导致了crash。

    _objc_msgForward会出现冲突的问题

    内部是通过消息转发机制来实现的,使用时要注意,避免与其它使用_objc_msgForward或相关逻辑的框架发生冲突。

    性能问题

    hook后的方法,通过原有消息机制找到IMP后,并不会直接调用。而是会进行消息转发进入到__ASPECTS_ARE_BEING_CALLED__方法,内部再通过key取出相应的Coantiner进行调用,相对于未hook之前,额外增加了调用成本。所以不建议对频繁调用的方法和在项目中大量使用。

    线程问题

    框架内部为了保证线程安全,有进行加锁,但是使用的是自旋锁OSSpinLock,存在线程反转的问题,在iOS10已经被标记为弃用。

    对类方法的hook,需要使用object_getClass来获取元类对象进行hook

    这个不是框架问题,而是有些同学不知道如何对类方法进行hook,这里进行说明。

    @interface Animal : NSObject
    + (void)testClassMethod;
    @end
    
    // 需要通过object_getClass来获取元类对象进行hook
    [object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod)     
                                     withOptions:AspectPositionAfter 
                                      usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"aspects hook setName");
    } error:null];
    
    https://juejin.cn/post/7025783407540076581
    ------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。
  • 相关阅读:
    Redis
    Zookeeper的安装配置及基本开发
    【Unity Shader】新书封面 — Low Polygon风格的渲染
    Hive基本原理及环境搭建
    Hadoop开发环境搭建
    java常用排序算法
    企业人事管理系统项目拾金
    Linux27:分区、格式化与修复
    Linux26:查询磁盘和监控系统资源
    Linux25:文件系统特点与XFS文件系统
  • 原文地址:https://www.cnblogs.com/feng9exe/p/15618287.html
Copyright © 2011-2022 走看看