前言 在iOS日常开发中,对某些方法进行hook是很常见的操作。最常见的是使用Category在+load
中进行方法swizzle,它是针对类的,会改变这个类所有实例的行为。但是有时候我们只想针对单个实例进行hook,这种方法就显得无力了。而Aspects
框架可以搞定这个问题。 它的原理是通过Runtime
动态的创建子类,把实例的isa
指针指向新创建的子类,然后在子类中对hook的方法进行处理,这样就支持了对单个实例的hook。Aspects
框架支持对类和实例的hook,API很易用,可以方便的让你在任何地方进行hook,是线程安全的。但是Aspects
框架也有一些缺陷,一不小心就会掉坑里面,我会通过源码解析进行说明。
源码解析 我主要使用图示对Aspects
的源码进行说明,建议参考源码一起查看。要看懂这些内容,需要对isa指针
,消息转发机制
,runtime
有一定的了解,本文中不会对这些内容展开来讲,因为要把这些东西讲清楚,每一项都需要单独写一篇文章了。
主要流程解析
它第一个流程是使用关联对象添加Container
,在这个过程中会进行一些前置条件的判断,例如这个方法是否支持被hook等,如果条件验证通过,就会把这次hook的信息保存起来,在方法调用的时候,查询出来使用。
第二个流程是动态创建子类,如果是针对类的hook,则不会走这一步。
第三步是替换这个类的forwardInvocation:
方法为__ASPECTS_ARE_BEING_CALLED__
,这个方法内部会查找到之前创建的Container,然后根据Container中的逻辑进行实际的调用。
第四步是将原有方法的IMP
改为_objc_msgForward
,改完后当调用原有方法时,就会调用_objc_msgForward
,从而触发forwardInvocation:
方法。
我对它的流程做了一个简化的图示,标有每个流程的序号,后面会对每个流程进行解析。流程如下:
图示中的取出对象类型
,是指的调用hook的对象的类型,如果是实例对象
,那么就走类
路径;如果是类
对象,则走元类
路径;如果是kvo
等实际类型不一致的情况,则走其它子类
路径。
①添加Container流程 这个流程中,把hook的逻辑封装成Container,并使用关联对象进行保存。这个过程中会判断hook的方法是否被支持、判断被hook类的继承关系、验证回调block正确性等操作。具体图示如下:
关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 static id aspect_add(id self , SEL selector, AspectOptions options, id block, NSError **error) { ... 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]; 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 ]; }); NSString *selectorName = NSStringFromSelector (selector); if ([disallowedSelectorList containsObject:selectorName]) { NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted." , selectorName]; AspectError(AspectErrorSelectorBlacklisted, errorDescription); return NO ; } 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 ; } if (class_isMetaClass(object_getClass(self ))) { ... } return YES ; } - (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 ; } }
从源码中可以看到,不支持的hook方法有[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
。其中retain
, release
, autorelease
在arc下是被禁用的,框架本身是hook
了forwardInvocation:
进行实现的,所以对它的hook也不支持。
dealloc
只支持AspectPositionBefore
类型,使用AspectPositionInstead
会导致系统默认的dealloc
操作被替换无法执行而出现问题。 AspectPositionAfter
类型,调用时对象可能已经已经被释放了,从而引发野指针错误。
Aspects
禁止有继承关系的类hook同一个方法,具体可以参见它的一个issue ,它报告了这样操作会导致死循环,我会在文章后面再进行说明。
Aspects
使用block
进行hook的调用,涉及到方法参数的传递和返回值问题,所以其中会对block进行校验。
②runtime创建子类 iOS中的KVO
就是通过runtime
动态创建子类,然后在子类中重写对应的setter
方法来实现的,Aspects
支持对单个实例的hook原理与此有一些类似。图示如下: 具体说明请查看源码中的注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static void aspect_prepareClassAndHookSelector(NSObject *self , SEL selector, NSError **error) { NSCParameterAssert (selector); 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); if ([className hasSuffix:AspectsSubclassSuffix]) { return baseClass; }else if (class_isMetaClass(baseClass)) { return aspect_swizzleClassInPlace((Class)self ); }else if (statedClass != baseClass) { return aspect_swizzleClassInPlace(baseClass); } 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 ; } aspect_swizzleForwardInvocation(subclass); 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__
。源码如下:
1 2 3 4 5 6 7 8 9 10 static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:" ;static void aspect_swizzleForwardInvocation(Class klass) { NSCParameterAssert (klass); 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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]
后会从父类查找foo
的IMP
,查到后发现父类的IMP
已经被替换为_objc_msgForward
,然后传入self
调用。 因为是传入的self
,所以实际会调用到它自身的forwardInvocation:
,这样就导致了死循环。
针对单个实例的hook,hook后使用kvo没问题,使用kvo后hook会出现问题。 这里通过代码进行说明,以Animal对象为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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]; [animal aspect_hookSelector:@selector (setName:) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> aspectInfo, NSString *name){ NSLog (@"aspects hook setName" ); } error:nil ]; animal.name = @"ChangedAnimalName" ; } }
异常原因分析图示如下:
上面是继承链和方法调用流程的图示,可以看出,_NSSetObjectValueAndNotify
是被aspects__setName:
调用的,_NSSetObjectValueAndNotify
的内部实现逻辑是取调用它的selector
,去父类查找方法,即aspects__setName:
方法,而Animal
对象并没有这个方法的实现,这就导致了crash。
与category的共存问题 先用aspects
进行hook,再使用category
进行hook,会导致crash。反之则没有问题。样例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @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]; animal.name = @"ChangedAnimalName" ; } }
这个与__ASPECTS_ARE_BEING_CALLED__
的内部逻辑有关,里面会对调用的方法添加前缀aspect__
进行调用,以调用到原始的IMP
,但是category
hook后破坏了这个流程。图示如下:
根据上述图示,实际只有aspects__setName
,没有aspects__lx_setName
,导致找不到方法而crash
基于类的hook,如果对同一个类同时hook类方法和实例方法,那么后hook的方法调用时会crash。样例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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]; [Animal testClassMethod]; } }
这样的调用在日常开发中非常正常,但是它会导致crash。它是由于aspect_swizzleClassInPlace
方法中的逻辑缺陷导致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static Class aspect_swizzleClassInPlace(Class klass) { NSCParameterAssert (klass); NSString *className = NSStringFromClass (klass); NSLog (@"aspect_swizzleClassInPlace %@ %p" , klass, object_getClass(klass)); _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 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,这里进行说明。
1 2 3 4 5 6 7 8 9 10 @interface Animal : NSObject + (void )testClassMethod; @end [object_getClass(Animal) aspect_hookSelector:@selector (testClassMethod) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> aspectInfo){ NSLog (@"aspects hook setName" ); } error:null];