所谓的面向切面编程(AOP),原理就是在不更改正常业务的流程的前提下,经过一个动态代理类,实现对目标对象嵌入的附加的操做。ios
简单说,就是在不影响咱们如今正常业务的状况下,对某些类的某些方法嵌入操做。咱们能够很通俗的理解一个方法能够有方法前和方法后这两个切面,固然还能够把方法执行过程看过一个整的切面去hook。git
在咱们的iOS开发中,AOP的实现方法就是使用Runtime的Swizzling Method改变selector指向的实现,在新的实现中添加新的操做,执行完新实现以后,再处理以前的实现逻辑。github
Aspects是iOS平台比较成熟的AOP的框架,此次咱们主要来研究一下这个库的源码。编程
基于Aspects 1.4.1版本。安全
Aspects给出了两个方法,一个类方法一个实例方法,使用起来很是简单。框架
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;
传参也很容易理解,selector天然就是咱们要hook的方法,options使咱们要hook的位置,下面具体再说,block是一个回调,也就是咱们所说的要嵌入的代码逻辑,error就是hook失败,固然了失败的缘由较多,咱们下面会提到。ide
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. };
options也是一个枚举的类型,看一下里面定义的字段就很容易明白了,AspectPositionAfter是表示嵌入的放大要在被hook方法原来逻辑以前以后执行,AspectPositionBefore是以前执行,AspectPositionInstead表示要用嵌入的代码替换掉以前的逻辑,AspectOptionAutomaticRemoval表示hook执行后,移除hook。函数
由于是NS_OPTIONS类型,可多选。源码分析
除了上述核心的方法是经过NSObject的Category的方式给出,还有如下几个类比较重要。.net
AspectsContainer
: 一个对象或者类的全部的Aspects总体状况
AspectIdentifier
: 一个Aspects的具体内容,这里主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所须要用到的具体信息:包括方法签名、参数等等
AspectInfo
: 一个 Aspect 执行环境,主要是 NSInvocation 信息。
Aspects给出的两个方法最终都是调用了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(^{ // 是否容许hook - if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { //取到sel对应的container (一个类不一样sel对应不一样的container?) 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; }
__block AspectIdentifier *identifier = nil;
每次在添加hook的时候,都会先建立一个AspectIdentifier。
__block是为了能在下面的block中修改identifier。
aspect_performLocked
是封装了一个自旋锁。
在自旋锁中会有一个if语句来判断selector是否能被hook。那咱们就先来看一下是否能被hook的判断方法aspect_isSelectorAllowedAndTrack
黑名单
aspect_isSelectorAllowedAndTrack
方法中维护了一个NSSet,在初始化的时候加入了一些方法名,在源码中是下面这些。
[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
这说明了后面这几个方法是不容许被hook的,若是hook了这些方法会有错误的信息提示。
在hook的方法中,dealloc属于一个特殊状况,由于这个方法是在对象要被销毁的时候建立,因此Aspects为了安全起见,在hook dealloc方法的时候。options只容许时AspectPositionBefore,也就是插入的逻辑只能在dealloc原有逻辑以前处理,不容许替换或者在dealloc以后。
这个就没啥疑问了,若是咱们hook了一个根本不存在的方法也会有错误提示。
方法只容许hook一次(元类相关)
这个有点麻烦,由于从错误提示的枚举来看,他是对应AspectErrorSelectorAlreadyHookedInClassHierarchy
这一项的,从字面意思来看是说方法已经被hook了。
最开始我对这个类的层级不是很明白,个人最初理解的类的层级是父类和子类不能同时hook,通过验证这种理解是错误的。
后来我仔细地看了一下源码,他的元类判断中是这么写的:
if (class_isMetaClass(object_getClass(self))) {}
判断的是object_getClass(self),经过runtime的源码咱们能够知道,object_getClass获得的是传参的isa指针指向的结构,意识就是self若是是对象,object_getClass(self)获得的是对应的类,若是self是类,那就获得了元类。
那何时会提示这个错误呢,我举一个例子吧。我建立了一个TestHookViewController
类,继承自UIViewController
,若是我在TestHookViewController
中像下面这样写就会有错误提示了。
[[UIViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) { NSLog(@"%s",__func__); } error:NULL]; [[TestHookViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) { NSLog(@"%s",__func__); } error:NULL];
固然了,这两个hook的先后位置不一样,打印台输出的提示也是不同的,虽然都是一个类层级方法只容许hook一次的错误缘由。这个你们自行尝试一下。
if语句里面就是关于方法是否重复hook的判断逻辑,这里牵扯到一个相关类,AspectTracker。咱们如今就来看一下这个类。
虽然是说AspectTracker类,可是代码结构仍是接着上面的我们说到的位置继续往下走。
由于AspectTracker主要就是用在方法只容许hook一次的判断中。
Aspects维护了一个字典,来储存被hook方法的类和对应的AspectTracker。
Class currentClass = [self class]; AspectTracker *tracker = swizzledClassesDict[currentClass];
经过上面的方式取到对应的AspectTracker。这里提一句,这里的代码咱们应该先看下面的,要先了解AspectTracker是怎么存储到字典里面的。
这里咱们要看下面这个do-while循环
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)));
若是最开始没有tracker,会初始化一个,而后存到字典中。最开始的时候subclassTracker为nil,因此selector会add到tracker.selectorNames。
而后currentClass从新赋值
currentClass = class_getSuperclass(currentClass)
再次执行do逻辑里面的代码,此次subclassTracker就有值了(上一次循环的tracker),就会执行[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
咱们进到addSubclassTracker
源码中能够看到,tracker被存到selectorNamesToSubclassTrackers
这个字典中,关键的是这个字典的key是selectorName,value是一个集合,tracker是放在这个集合里面的。为何要经过集合来存tracker呢?
由于这里是有子类的状况的,一个类的子类可能有多个,若是在不一样的子类中hook了这个父类的一个方法,也就是父类中的这一个selector被屡次hook,因此也会有不一样的tracker,因此使用一个集合来储存。
其实说到这个地方你们就差很少能够理解了,若是知足了if (class_isMetaClass(object_getClass(self)))
这个判断,咱们会把这个类hook的方法经过封装为AspectTracker
来进行记录,固然包括他的层层父类,都对对应一个AspectTracker
,并且父类中的还会记录子类中hook的方法。这部分代码最好是debug跟一下,会明显一点。
上面咱们先看了tracker是怎样被存起来的,接来下再来看关于只能被hook一次的判断。
首先要判断子类中是否hook
if ([tracker subclassHasHookedSelectorName:selectorName]) { //内部省略 }
subclassHasHookedSelectorName
内部实现很简单
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName { return self.selectorNamesToSubclassTrackers[selectorName] != nil; }
就是查询一下selectorNamesToSubclassTrackers这个字典中,经过seletorName是否能取到值,上面已经说过了,这个字典中经过key:selectorName value: set的方法储存了子类的tracker。
若是能取到值,就说明子类中已经hook了这个方法了,父类中就不能在hook。
若是没能取到值,说明当前类可能就是个子类,此时须要看一下他的父类中是否hook了这个selector,因此就会执行下面的的do-while循环。 此处的代码就不展现了。
但这个位置初步的关于方法可否被hook就已经判断完了。若是能够seletor能够被hook,继续if里面的代码。
咱们直接说Swizzling Method这一最重要的逻辑吧。
Swizzling Method主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另外一个是对传入的 selector 进行 swizzling。
咱们来看aspect_prepareClassAndHookSelector
方法的源码。
首先是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 (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方法 aspect_swizzleForwardInvocation(subclass); // subclass的class方法交替换 替换为statedClass的class方法 subclass元类也替换 aspect_hookedGetClass(subclass, statedClass); aspect_hookedGetClass(object_getClass(subclass), statedClass); objc_registerClassPair(subclass); } // 改变当前类的isa指针指向 object_setClass(self, subclass); return subclass; }
咱们从源码中能够看出逻辑,主要是动态建立了一个subclass,名为subclass,其实最终把咱们hook的类的isa指针指向了这个subclass,实为是一个父类。
aspect_swizzleForwardInvocation(subclass);
上面这个方法是替换了forwardInvocation:
方法。
固然了,也是替换的这个subclass的forwardInvocation:
方法,把forwardInvocation:
替换为__ASPECTS_ARE_BEING_CALLED__
这个方法,主要的hook后的代码执行处理逻辑都在这个__ASPECTS_ARE_BEING_CALLED__
中。
在替换了aspect_hookClass
方法以后,同时修改了 subclass以及其subclass metaclass的class方法,使他返回当前对象的class。这个地方有点绕,其实最终目的就是把全部的swizzling都放到了这个subclass中处理,不影响原来的类,并且对于外部的使用者,又能够把它当作原对象使用。
执行完aspect_hookClass
完以后,forwardInvocation:
方法已经被替换,下面会执行swizzling selector 的代码。
在swizzling selector的时候,将selector指向了消息转发IMP,同时生成一个aliasSelector,指向原方法的IMP。
这里代码就不往外粘了。
其实上面已经把整个过程分析完了,咱们也知道,最后转发的代码最终会在__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; 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]; //根据aliasSelector找到以前的逻辑 执行 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)]; }
从源码中很容易看出来,分别处理不一样的hook点,而后中间有根据
aliasSelector找到以前方法的实现,而后执行。
初步的源码分析就是这个样子,没有太关注一些细节,也存在一些本身如今还不是很熟悉的处理方式,毕竟涉及到太多的swizzling,消息转发一类的方法,这一块的只是须要后期在多研究runtime来提升。
代码中有一些其余的比较小的方法没有讲到,你们本身自行看一下。
https://wereadteam.github.io/...