AOP:Aspect Oriented Programming,译为面向切面编程,是能够经过预编译的方式和运行期动态实现,在不修改源代码的状况下,给程序动态统一添加功能的技术。html
面向对象编程(OOP)适合定义从上到下的关系,但不适用于从左到右,计算机中任何一门新技术或者新概念的出现都是为了解决一个特定的问题的,咱们看下AOP解决了什么样的问题。android
例如一个电商系统,有不少业务模块的功能,使用OOP来实现核心业务是合理的,咱们须要实现一个日志系统,和模块功能不一样,日志系统不属于业务代码。若是新建一个工具类,封装日志打印方法,再在原有类中进行调用,就增长了耦合性,咱们须要从业务代码中抽离日志系统,而后独立到非业务的功能代码中,这样咱们改变这些行为时,就不会影响现有业务代码。ios
当咱们使用各类技术来拦截方法,在方法执行先后作你想作的事,例如日志打印,就是所谓的AOP。git
说到iOS中AOP的方案第一个想到的应该就是 Method Swizzlegithub
得益于Objective-C这门语言的动态性,咱们可让程序在运行时作出一些改变,进而调用咱们本身定义的方法。使用Runtime 交换方法的核心就是:method_exchangeImplementations
, 它实际上将两个方法的实现进行交换:objective-c
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class aClass = [self class]; SEL originalSelector = @selector(method_original:); SEL swizzledSelector = @selector(method_swizzle:); Method originalMethod = class_getInstanceMethod(aClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
做为咱们常说的黑魔法 Method Swizzle 到底危险不危险,有没有最佳实践。编程
这里能够经过这篇回答一块儿深刻理解下。这里列出了一些 Method Swizzling 的陷阱:swift
你会把 Method Swizzling 修改方法实现的操做放在一个加号方法 +(void)load
里,并在应用程序的一开始就调用执行,一般放在 dispatch_once()
里面来调用。你绝大多数状况将不会碰到并发问题。缓存
这是 Method Swizzling 的一个问题。咱们的目标是改变某些代码。当你不仅是对一个UIButton类的实例进行了修改,而是程序中全部的UIButton实例,对原来的类侵入较大。多线程
命名冲突贯穿整个 Cocoa 的问题. 咱们经常在类名和类别方法名前加上前缀。不幸的是,命名冲突还是个折磨。可是swizzling其实也没必要过多考虑这个问题。咱们只须要在原始方法命名前作小小的改动来命名就好,好比一般咱们这样命名:
@interface UIView : NSObject - (void)setFrame:(NSRect)frame; @end @implementation UIView (MyViewAdditions) - (void)my_setFrame:(NSRect)frame { // do custom work [self my_setFrame:frame]; } + (void)load { [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)]; } @end
这段代码运行是没问题的,可是若是 my_setFrame
: 在别处被定义了会发生什么呢?好比在别的分类中,固然这个问题不只仅存在于swizzling 中,其余地方也可能会出现,这里能够有个变通的方法,利用函数指针来定义
@implementation UIView (MyViewAdditions) static void MySetFrame(id self, SEL _cmd, NSRect frame); static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame); static void MySetFrame(id self, SEL _cmd, NSRect frame) { // do custom work SetFrameIMP(self, _cmd, frame); } + (void)load { [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP]; } @end
我认为这是最大的问题。想正常调用 Method Swizzling 的方法将会是个问题。好比我想调用 my_setFrame
:
[self my_setFrame:frame];
Runtime 作的是 objc_msgSend(self, @selector(my_setFrame:), frame)
; Runtime去寻找my_setFrame
:的方法实现,但由于已经被交换了,事实上找到的方法实现是原始的 setFrame
: 的,若是想调用 Method Swizzling 的方法,能够经过上面的函数的方式来定义,不走Runtime 的消息发送流程。不过这种需求场景不多见。
多个swizzle方法的执行顺序也须要注意。假设 setFrame
: 只定义在 UIivew 中,想像一下按照下面的顺序执行:
[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)]; [UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)]; [UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
这里须要注意的是swizzle的顺序,多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是由于load类方法会默认从父类开始调用,不过这种场景不多,通常会选择一个类进行swizzle。
新方法的实现里面会调用本身同名的方法,看起来像递归,可是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂,这个问题是已彻底解决的了!
调试时无论经过bt 命令仍是 [NSThread callStackSymbols]
打印调用栈,其中掺杂着被swizzle的方法名,会显得一团槽!上面介绍的swizzle方案,使backtrace中打印出的方法名仍是很清晰的。但仍然很难去debug,由于很难记住swizzling影响过什么。给你的代码写好文档(即便只有你一我的会看到),统一管理一些swizzling的方法,而不是分散到业务的各个模块。相对于调试多线程问题 Method Swizzling 要简单不少。
Aspects 是 iOS 上的一个轻量级 AOP 库。它利用 Method Swizzling 技术为已有的类或者实例方法添加额外的代码,使用起来是很方便:
/// Adds a block of code before/instead/after the current `selector` for a specific class. + (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; /// Adds a block of code before/instead/after the current `selector` for a specific instance. - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;
Aspects 提供了2个 AOP 方法,一个用于类,一个用于实例。在肯定 hook 的 方法以后, Aspects 容许咱们选择 hook 的时机是在方法执行以前,仍是方法执行以后,甚至能够直接替换掉方法的实现。网上有不少介绍其实现原理的文章,在iOS开源社区中算是少有的精品代码,对深刻理解掌握ObjC 的消息发送机制颇有帮助。但其存在的缺陷就是性能较差,如官方所说
Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second.Aspects hooks deep into the class hierarchy and creates dynamic subclasses, much like KVO. There's known issues with this approach, and to this date (February 2019) I STRICTLY DO NOT RECOMMEND TO USE Aspects IN PRODUCTION CODE. We use it for partial test mocks in, PSPDFKit, an iOS PDF framework that ships with apps like Dropbox or Evernote, it's also very useful for quickly hacking something up.
官方强烈不推荐在生产环境中使用,通常用来在单测中作一些mock操做。咱们这边的性能测试也证实了这一点:在iPhone 6 真机上,循环100w次的方法调用(已经经过 Aspects hook 的方法)中会直接报 Terminated due to memory issue crash 错误信息。
MPSwizzler 这个是开源数据分析SDK MixPanel 中采用的一种 AOP 方案,原理不是很复杂,主要仍是基于ObjC 的运行时。
+ (void)swizzleSelector:(SEL)aSelector onClass:(Class)aClass withBlock:(swizzleBlock)aBlock named:(NSString *)aName { Method aMethod = class_getInstanceMethod(aClass, aSelector); if (aMethod) { uint numArgs = method_getNumberOfArguments(aMethod); if (numArgs >= MIN_ARGS && numArgs <= MAX_ARGS) { // 判断该方法是否在本身类的方法列表中,而不是父类 BOOL isLocal = [self isLocallyDefinedMethod:aMethod onClass:aClass]; IMP swizzledMethod = (IMP)mp_swizzledMethods[numArgs - 2]; MPSwizzle *swizzle = [self swizzleForMethod:aMethod]; if (isLocal) { if (!swizzle) { IMP originalMethod = method_getImplementation(aMethod); // Replace the local implementation of this method with the swizzled one method_setImplementation(aMethod,swizzledMethod); // Create and add the swizzle swizzle = [[MPSwizzle alloc] initWithBlock:aBlock named:aName forClass:aClass selector:aSelector originalMethod:originalMethod withNumArgs:numArgs]; [self setSwizzle:swizzle forMethod:aMethod]; } else { [swizzle.blocks setObject:aBlock forKey:aName]; } } else { // 若是是父类的方法会添加到自身,避免对父类侵入 IMP originalMethod = swizzle ? swizzle.originalMethod : method_getImplementation(aMethod); // Add the swizzle as a new local method on the class. if (!class_addMethod(aClass, aSelector, swizzledMethod, method_getTypeEncoding(aMethod))) { NSAssert(NO, @"SwizzlerAssert: Could not add swizzled for %@::%@, even though it didn't already exist locally", NSStringFromClass(aClass), NSStringFromSelector(aSelector)); return; } // Now re-get the Method, it should be the one we just added. Method newMethod = class_getInstanceMethod(aClass, aSelector); if (aMethod == newMethod) { NSAssert(NO, @"SwizzlerAssert: Newly added method for %@::%@ was the same as the old method", NSStringFromClass(aClass), NSStringFromSelector(aSelector)); return; } MPSwizzle *newSwizzle = [[MPSwizzle alloc] initWithBlock:aBlock named:aName forClass:aClass selector:aSelector originalMethod:originalMethod withNumArgs:numArgs]; [self setSwizzle:newSwizzle forMethod:newMethod]; } } else { NSAssert(NO, @"SwizzlerAssert: Cannot swizzle method with %d args", numArgs); } } else { NSAssert(NO, @"SwizzlerAssert: Cannot find method for %@ on %@", NSStringFromSelector(aSelector), NSStringFromClass(aClass)); } }
其中最主要的就是 method_setImplementation(aMethod,swizzledMethod); 其中 swizzledMethod 是根据原来方法的参数匹配到对应的以下几个函数:
static void mp_swizzledMethod_2(id self, SEL _cmd)
static void mp_swizzledMethod_3(id self, SEL _cmd, id arg)
static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2)
static void mp_swizzledMethod_5(id self, SEL _cmd, id arg, id arg2, id arg3)
这个几个函数内部实现大致同样的,以 mp_swizzledMethod_4
为例:
static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2) { Method aMethod = class_getInstanceMethod([self class], _cmd); // 1. 获取保存hook 的实体类 MPSwizzle *swizzle = (MPSwizzle *)[swizzles objectForKey:(__bridge id)((void *)aMethod)]; if (swizzle) { // 2. 先调用原来的方法 ((void(*)(id, SEL, id, id))swizzle.originalMethod)(self, _cmd, arg, arg2); NSEnumerator *blocks = [swizzle.blocks objectEnumerator]; swizzleBlock block; // 3. 再循环调用 hook 的方法块,可能绑定了多个 while ((block = [blocks nextObject])) { block(self, _cmd, arg, arg2); } } }
这个AOP的方案在多数SDK中也均采用了,好比 FBSDKSwizzler 、SASwizzler,相比于Aspects 性能好太多、但与 朴素的 Method Swizzling 相比还有差距。
利用 KVO 的运行时 ISA-swizzle 原理,动态建立子类、并重写相关方法,而且添加咱们想要的方法,而后在这个方法中调用原来的方法,从而达到 hook 的目的。这里以 ReactiveCocoa 的做为示例。
internal func swizzle(_ pairs: (Selector, Any)..., key hasSwizzledKey: AssociationKey<Bool>) { // 动态建立子类 let subclass: AnyClass = swizzleClass(self) ReactiveCocoa.synchronized(subclass) { let subclassAssociations = Associations(subclass as AnyObject) if !subclassAssociations.value(forKey: hasSwizzledKey) { subclassAssociations.setValue(true, forKey: hasSwizzledKey) for (selector, body) in pairs { let method = class_getInstanceMethod(subclass, selector)! let typeEncoding = method_getTypeEncoding(method)! if method_getImplementation(method) == _rac_objc_msgForward { let succeeds = class_addMethod(subclass, selector.interopAlias, imp_implementationWithBlock(body), typeEncoding) precondition(succeeds, "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version.") } else { // 经过 block 生成一个新的 IMP,为生成的子类添加该方法实现。 let succeeds = class_addMethod(subclass, selector, imp_implementationWithBlock(body), typeEncoding) precondition(succeeds, "RAC attempts to swizzle a selector that has already a runtime injected implementation. This is unsupported in the current version.") } } } } } internal func swizzleClass(_ instance: NSObject) -> AnyClass { if let knownSubclass = instance.associations.value(forKey: knownRuntimeSubclassKey) { return knownSubclass } let perceivedClass: AnyClass = instance.objcClass let realClass: AnyClass = object_getClass(instance)! let realClassAssociations = Associations(realClass as AnyObject) if perceivedClass != realClass { // If the class is already lying about what it is, it's probably a KVO // dynamic subclass or something else that we shouldn't subclass at runtime. synchronized(realClass) { let isSwizzled = realClassAssociations.value(forKey: runtimeSubclassedKey) if !isSwizzled { // 重写类的 -class 和 +class 方法,隐藏真实的子类类型 replaceGetClass(in: realClass, decoy: perceivedClass) realClassAssociations.setValue(true, forKey: runtimeSubclassedKey) } } return realClass } else { let name = subclassName(of: perceivedClass) let subclass: AnyClass = name.withCString { cString in if let existingClass = objc_getClass(cString) as! AnyClass? { return existingClass } else { let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0)! // 重写类的 -class 和 +class 方法,隐藏真实的子类类型 replaceGetClass(in: subclass, decoy: perceivedClass) objc_registerClassPair(subclass) return subclass } } object_setClass(instance, subclass) instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey) return subclass } }
其中RxSwift 中的 _RXObjCRuntime 也提供了相似的思路。
固然也能够不用本身经过objc_registerClassPair()
建立类,直接经过 KVO 由系统帮咱们生成子类,例如:
static void growing_viewDidAppear(UIViewController *kvo_self, SEL _sel, BOOL animated) { Class kvo_cls = object_getClass(kvo_self); Class origin_cls = class_getSuperclass(kvo_cls); IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel)); assert(origin_imp != NULL); void (*origin_method)(UIViewController *, SEL, BOOL) = (void (*)(UIViewController *, SEL, BOOL))origin_imp; // 调用原来的方法 origin_method(kvo_self, _sel, animated); // Do something } - (void)createKVOClass { [self addObserver:[GrowingKVOObserver shared] forKeyPath:kooUniqueKeyPath options:NSKeyValueObservingOptionNew context:nil]; GrowingKVORemover *remover = [[GrowingKVORemover alloc] init]; remover.target = self; remover.keyPath = growingUniqueKeyPath; objc_setAssociatedObject(self, &growingAssociatedRemoverKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 经过object_getClass 取到的class 是由系统生成的前缀为 NSKVONotifying_ 的类型 Class kvoCls = object_getClass(self); Class originCls = class_getSuperclass(kvoCls); const char *originViewDidAppearEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewDidAppear:))); // 添加咱们本身的实现 growing_viewDidAppear class_addMethod(kvoCls, @selector(viewDidAppear:), (IMP)growing_viewDidAppear, originViewDidAppearEncoding); }
这种利用KVO动态生成子类的AOP方案对原来的类侵入最小,由于它没有改变原始类的方法和实现的映射关系,也就不会影响到由原始类定义的其余的实例的方法调用。在一些好比更精确的计算页面加载时间的场景中会发挥很好的做用。可是这个AOP 的方案和其余一些SDK有冲突的情形,好比信鸽、Firebase 以及上面说的 RxSwift,在 RxSwift 中全部的消息机制都被统一成了信号,框架不推荐你使用 Delegate、KVO、Notification,尤为 KVO 会有异常错误的。
提升 iOS 的 AOP方案就不得不提到大名鼎鼎的 Fishook,它在作一些性能分析或者越狱分析中常常被用到。
你们都知道 ObjC 的方法之因此能够 Hook 是由于它的运行时特性,ObjC 的方法调用在底层都是 objc_msgSend(id, SEL) 的形式,这为咱们提供了交换方法实现(IMP)的机会,但 C 函数在编译连接时就肯定了函数指针的地址偏移量(Offset),这个偏移量在编译好的可执行文件中是固定的,而可执行文件每次被从新装载到内存中时被系统分配的起始地址(在 lldb 中用命令image List获取)是不断变化的。运行中的静态函数指针地址其实就等于上述 Offset + Mach0 文件在内存中的首地址。
既然 C 函数的指针地址是相对固定且不可修改的,那么 fishhook 又是怎么实现 对 C 函数的 Hook 呢?其实内部/自定义的 C 函数 fishhook 也 Hook 不了,它只能Hook Mach-O 外部(共享缓存库中)的函数,好比 NSLog、objc_msgSend 等动态符号表中的符号。
fishhook 利用了 MachO 的动态绑定机制,苹果的共享缓存库不会被编译进咱们的 MachO 文件,而是在动态连接(依靠动态链接器 dyld)时才去从新绑定。苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:
fishhook 正是利用了 PIC 技术作了这么两个操做:
这是Facebook 提供的官方示意图:
Lazy Symbol Pointer Table --> Indirect Symbol Table --> Symbol Table --> String Table
这张图主要在描述如何由一个字符串(好比 "NSLog"),根据它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,咱们经过 MachOView 工具来分析下这个步骤:
_la_sysmbol_ptr 该section 表示 懒加载的符号指针,其中的 value,是对保留字段的解析,表示在 Indirect Symbol Table 中的索引
经过 reserve1 找到 对应 section __la_symbol_ptr 在动态符号表(Indirect Symbols)中的位置,好比下图:#14 就是 __la_symbol_ptr section 所在的起始位置。
符号个数计算 是经过 sizeof(void (* )) 指针在64位上时8个字节大小,所要这个__la_symbol_ptr section 有 104 / 8 = 13 个符号,_NSLog 只是其中之一。
注意 Indirect Symbols 动态符号表,其中的Data 值 0x00CO (#192) 表示该符号在符号表中的索引
符号表中的第192号就是 _NSLog 符号,这个Data 0x00CE 就是字符串表中的索引
上面的索引 0x00CE 加上这个字符串表的起始值 0xD2B4 就是该符号在符号表中的位置,以下图所示:
以上梳理了fishhook 大概的流程,以后看代码的实现就不是很抽象了,须要对 MachO 文件的结构有较深刻的理解。既然fishhook 能够hook 系统静态的C 函数,那么也能够hook ObjC 中的 Runtime 相关的方法,好比 objc_msgSend
、method_getImplementation
、method_setImplementation
、method_exchangeImplementations
能够作一些有趣的攻防探索、其中越狱中经常使用的 Cydia Substrate 其中的 MobileHooker 底层就是调用 fishhook 和 ObjC 的 Runtime 来替换系统或者目标应用的函数。对其封装较好的 theos 或者 MonkeyDev 开发工具方便越狱进行hook 分析。须要注意的是 fishhook 对于变参函数的处理比较麻烦,不太方便拿到全部的可变的参数,须要借助汇编来操做栈和寄存器。关于这部分能够参见:TimeProfiler、 AppleTrace。
让咱们把镜头进一步向前推动,了解下 Thunk 技术。
Thunk 程序中文翻译为形实转换程序,简而言之Thunk程序就是一段代码块,这段代码块能够在调用真正的函数先后进行一些附加的计算和逻辑处理,或者提供将对原函数的直接调用转化为间接调用的能力。Thunk程序在有的地方又被称为跳板(trampoline)程序,Thunk程序不会破坏原始被调用函数的栈参数结构,只是提供了一个原始调用的hook的能力。Thunk技术能够在编译时和运行时两种场景下被使用。其主要的思想就是在运行时咱们本身在内存中构造一段指令让CPU执行。关于 Thunk 思想在iOS 中的实现能够参见 Thunk程序的实现原理以及在iOS中的应用 和 Thunk程序的实现原理以及在iOS中的应用 从背景理论到实践来分析这一思想。
关于Thunk 思想的具体实现能够参见下面几个三方库以相关的博客:
其中核心都会利用到 libffi 这个库,底层是汇编写的,libfii 能够理解为实现了C语言上的 Runtime。
以上iOS AOP 方案中大可能是基于运行时的,fishhook 是基于连接阶段的,而编译阶段可否实现AOP呢,插入咱们想要的代码呢?
做为 Xcode 内置的编译器 Clang 实际上是提供了一套插桩机制,用于代码覆盖检测,官方文档以下:Clang自带的代码覆盖工具,关于Clang 插桩的一个应用能够详见这篇文章,最终是由编译器在指定的位置帮咱们加上了特定的指令,生成最终的可执行文件,编写更多的自定义的插桩规则须要本身手写 llvm pass 。
这种依赖编译器作的AOP 方案,适用于与开发、测试阶段作一些检测工具,例如:代码覆盖、Code Lint、静态分析等。
以上介绍了iOS 中主流的 AOP 的方案和一些知名的框架,有编译期、连接期、运行时的,从源代码到程序装载到内存执行,整个过程的不一样阶段均可以有相应的方案进行选择。咱们的工具箱又多出了一些可供选择,同时进一步加深对静态和动态语言的理解,也对程序从静态到动态整个过程理解更加深刻。
同时咱们Android 和 iOS 无埋点SDK 3.0 均已开源,有兴趣能够关注下面github 仓库,了解咱们最新的开发进展。
Android:https://github.com/growingio/growingio-sdk-android-autotracker
iOS:https://github.com/growingio/growingio-sdk-ios-autotracker
关于 GrowingIO
GrowingIO 是国内领先的一站式数字化增加总体方案服务商。为产品、运营、市场、数据团队及管理者提供客户数据平台(CDP)、广告分析、产品分析、智能运营等产品和咨询服务,帮助企业在数字化转型的路上,提高数据驱动能力,实现更好的增加。
点击「此处」,注册 GrowingIO 15 天免费试用!