Method Swizzle
Method Swizzle
是基于runtime实现“黑魔法”。
写这篇主要是源于上周一次项目bug fix。
项目基于React Native 0.20版本开发,在调用相机拍照后,由于照片自身带有了旋转信息,因此照片在客户看来不是“正的”。
1 | - (void)imagePickerController:(UIImagePickerController *)picker |
可以看到其实就是需要对originalImage
进行方向修正。
然后跟踪了RN的实现,发现他是存在应用的临时变量里面,可是用的是CGImage。意味着我们即使取出来了这个Image,也无法获知这个Image的方向信息。当时第一反应是。。我自己来实现一个选择器。。。不过这个工作量略大,而且可能会造成很多不知原因的坑。晚上洗澡的时候,突然想到可以可以通过Method swizzle的方式去实现。
新建了一个category,在+(void)load
方法的替换了imagePickerController
方法,测试一下,OK。
什么时候用
Method Swizzle
是把锋利的刀,用的好,削铁如泥;用的不好,害人害己。
一般而言,如果能有好的方法解决,不推荐使用Method Swizzle
。原因就是难以跟踪问题。如A实现一个方法run
。一个开发在A(B)
这个category里面替换成了runFast
,而另一个开发在A(C)
里面替换成了runSlow
。那我们调用run
方法的时候到底是什么结果?
该怎么用
替换方法应该是在运行时确定唯一的,如果存在多次不确定的Method Swizzle
,我们就无法知道最后获取的IMP来源于来个方法。因此在哪里替换,怎么替换,对于不同类型的方法都不一样。
普通实例方法
替换普通实例方法比较简单,创建了一个对应类的分类,在分类中实现+(void)load
方法,在+(void)load
方法中进行替换。
1 | + (void)load { |
首先要知道为何在+(void)load
中实现替换。+(void)load
这个方法首先是在运行时执行,切只执行一次,因此就符合了我们在在程序运行期只执行一次替换
的想法。其次,+(void)load
的执行顺序是父类->子类->分类
的顺序,且不覆盖。因此,分类的+(void)load
不会影响类的+(void)load
也是我们正需要的。
类方法
实现类方法的实现思路也是一样的,不同的是我们不从实例方法列表中去获取相关方法实现
1 | + (void)load { |
区别有2点
class_getClassMethod(Class cls, SEL name)
替换掉class_getInstanceMethod(Class cls, SEL name)
。看得出方法的差异。- 实例方法的内容是记录在class的method list上的,而类方法是记录在meta-class 上的。
修改类簇
1 | + (void)load { |
官方文档
中详细讲解了什么是类簇。这里我们替换的是__NSDictionaryM
中对应的setObject:forKey:
AOP 和 Method Swizzle
有一定开发经验的人一定听说过AOP。用一句个人觉得比较经典的话来概括这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
。这里来看,通过Method Swizzle正好来实现AOP。在这方面,github上有一个实现非常好的开源库Aspects
这里的话,会用它来做一下分析,怎么去实现AOP。
功能分析
Aspects
这个库实现了AOP,那么实现到什么地步,能做到什么样的功能,可以从头文件定义中略知一二
1 | typedef NS_OPTIONS(NSUInteger, AspectOptions) { |
定义了AspectOptions
,从名字看出,分别可以做到将新方法插入到老方法之前/之后,替换原有的方法,仅在第一次替换原来的方法。
1 | @protocol AspectToken <NSObject> |
两个协议,实现后可以实现撤销插入/ 获得插入的实例信息,原有方法内容和参数列表。
1 | @interface NSObject (Aspects) |
对NSObject定义了一个分类,只要两个方法,分别是对类方法和实例方法的操作。
可以看到,Aspect的实现功能还是很强大的。在提供基本Method swizzle的基础上还实现了对不同插入位置的功能,提供可撤回的替换。
实现细节
在自己实现Method Swizzle
的时候,我们也会考虑到,如果我需要恢复被替换的方法怎么做?如果我仅仅想在某个方法执行前或执行后执行一个方法呢?比如我需要在所有的viewDidLoad
中插入一个log语句,这时候用Method Swizzle
显然是不合适,而Aspect
能做到的,也是让我们好奇的,来看看具体的实现方法。
1 | + (id<AspectToken>)aspect_hookSelector:(SEL)selector |
两个公开API进来后都是调用static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error)
这个静态方法,区别就是对类方法中替换要在self前用id修饰。
1 | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { |
方法中首先定义了一个AspectIdentifier
类型的实例变量:
1 | @interface AspectIdentifier : NSObject |
可以看到这个类的定义就是对Aspect定义的。
随后执行aspect_performLocked
:
1 | static void aspect_performLocked(dispatch_block_t block) { |
方法中创建了一个OSSpinLockLock
自旋锁,对block执行进行保护。
自旋锁是在多处理器系统(SMP)上为保护一段关键代码的执行或者关键数据的一种保护机制,是实现synchronization的一种手段。
传入的block中,首先先执行aspect_isSelectorAllowedAndTrack
方法
aspect_isSelectorAllowedAndTrack
比较长,从字面含义上来讲就是是否允许插入和追踪。
代码就不贴了。。简单说下思路。
我们知道有些方法是无法被替换的,有些hook的方法对插入的位置很敏感(好像很污的感觉)。这个方法就是对这些黑名单进行判断
首先是不能替换的,有release
,retain
,autorelease
,forwardInvocation
只能在添加block到hook方法前的:dealloc
被hook的类响应SEL的。。。喂你再去检查下好吧
接下来被hook的是不是元类,如果不是的话就可以愉快的返回YES啦。
如果是元类。。稍微麻烦点。梳理一下逻辑如下
swizzledClassesDict
是一个dictionary,里面存放的已经是以当前类为key,以AspectTracker
为value的键值对。AspectTracker
定义如下:
1 | @interface AspectTracker : NSObject |
意义如名字一样,实现的是一个跟踪对象。这个对象里面存放了跟踪的类,类的名字,选择器的名称已经子类跟踪对象的选择器。
获得tracker
后,判断是否已经hook了子类的相同选择器方法,注意只能在继承链上hook一次相同选择器的方法。
随后递归父类,查看是否在继承链上已经hook过了。
如果上述过程都能顺利进行下来的话,说明可以hook啦,这时候递归父类,将selector添加到tracker里面。
回到上面的aspect_performLocked
中,这时候我们得知是能hook啦,这时候根据传进来的self
和selector
创建一个AspectsContainer
。AspectsContainer
的定义如下:
1 | @interface AspectsContainer : NSObject |
这里有三个属性,都是array类型,从名字不难看到,存的是hook前,被hook的,hook后的。
创建AspectsContainer
之后,对identifier
初始化,如果成功初始化,向AspectsContainer
添加identifier
。
1 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { |
判断option,将AspectIdentifier
添加到对应的array
最后,执行aspect_prepareClassAndHookSelector
aspect_prepareClassAndHookSelector
是整个Aspect
里面最核心的部分了,前面的行为都是判断是否能hook和做相应的缓存操作,在这里才是真正的执行hook的地方。
1 | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { |
思路和我们自己动手实现Method Swizzle
是差不多的,过程也是获取到被hook的class,然后通过selector
获取到指定函数指针IMP
。然后将传入的方法(block)替换掉IMP
。
首先执行的是一个aspect_hookClass
方法,返回一个Class对象。
整体的思路如下
1 | Class statedClass = self.class; |
如果对runtime不熟悉的人可能不知道这两者的有什么不同。简单的说,self.class
返回的是这个Object
所属的类,而object_getClass
返回的是这个Object的元类,也就是类对象的类(很绕口)。接下来,就来判断元类是否被修改过(元类的类名被添加了特有的后缀),如果没有修改过,将对象的类进行hook:
1 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { |
理解起来不困难,用一个mutableSet存储类名,如果swizzledClasses
不在set里面的话,执行aspect_swizzleForwardInvocation
。这个方法就是替换forwardInvocation
这方法,目的是替换掉forwardInvocation
方法转发,采用自定以的__aspects_forwardInvocation
核心就是下面两句:
1 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); |
__ASPECTS_ARE_BEING_CALLED__
是最关键的方法,这个方法就是决定我们要替换的方法如何执行的地方。
1 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { |
这个方法里,实际上替换的就是forwardInvocation:
这个runtime方法,自造了一个调用方法。
目的就是从原有方法和hook方法去做处理。
过程就是先用临时变量获取invocation
的seletor,将invocation
的selecor替换成aliasSelector。通过传入的参数构造AspectInfo
。取得objectContainer
和classContainer
(都是AspectsContainer
)类型的。调用aspect_invoke
aspect_invoke
是一个宏方法(其实这里也不用写成宏)。
1 | #define aspect_invoke(aspects, info) \ |
调用了invokeWithInfo
判断aspect的option,如果需要一出,就讲他从对应的container中移除。invokeWithInfo
的方法如下:
1 | - (BOOL)invokeWithInfo:(id<AspectInfo>)info { |
这个部分就是从原有方法中取出参数列表赋给block,在这过程总检查是否block不符合原来方法。将self.block
设为blockInvocation
的target。
回到aspect_hookClass
,在上面特殊情况处理之后,就是一般情况,这时候创建动态子类,类名以AspectsSubclassSuffix
为指定后缀。aspect_hookedGetClass
中替换掉Class
方法,使其返回的是statedClass(被hook的类)
,这里将动态生成的子类的Class
和Meta Class
都替换成statedClass
,最后将subclass
注册进去,最后将subclass
设置成self
的类。这样就完成了hook的过程,self
执行的方法和信息都被我们hook到了。
回到aspect_prepareClassAndHookSelector
(感觉从很深的子树回来真的很不容易),接下来我们获得了我们要hook的Method
和IMP
,我们要判断是否有对应的IMP
1 | static BOOL aspect_isMsgForwardIMP(IMP impl) { |
_objc_msgForward_stret
和_objc_msgForward
的区别在这篇文章里面有讲解,简单的引用JSPatch作者的解释
大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self / _cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值。普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些 struct 是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self / _cmd 在寄存器的位置就变了。objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法 objc_msgSend_stret 代替。原理大概就是这样。在 NSMethodSignature 的 debugDescription 上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。
这里如果IMP == _objc_msgForward,说明找不到 class / selector 对应的 IMP,如果能找到的话,我们就可以进行下一步了。
首先要创建一个aliasSelector
选择器,用这个选择器去添加targetMethod
对应的IMP
。这个目的么,自然是保存下来将要被替换方法的实现。
接下来,class_replaceMethod
替换掉我们需要hook的方法,使用了aspect_getMsgForwardIMP
如下:
1 | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { |
这里对arm64做了特殊处理,原因就是上文提到实现问题,对返回的struct做特殊的处理。
至此,整个hook的过程就结束了,我们已经将需要hook的方法替换成了我们需要的实现。
回顾
写完后才发现整个思路虽然很清晰,但是跨越很大,如果去处理这个hook的类和方法需要做许多判断,然后将原有的类和方法的信息保存下来,以便于恢复,最后通过class_replaceMethod
的方法替换掉了forwardInvocation:
。这样通过实现了最后消息转发过程中hook,执行我们注入的方法了。
至于恢复被hook的方法,思路也很简单了,从cache中获取origin method,替换掉hook方法就行了。
总结
Aspect
的实现是基于对runtime强大的理解,通过hookforwardInvocation
方法,做到了对消息转发的改变,任何对象不能处理的方法最后都会到forwardInvovation
中,在这里我们能执行hook的方法和选择执行的时间,也达到了AOP
的思想。
Method Swizzle
也好,Aspect
也好,都是依赖对runtime的认识理解,尤其是Aspect
,在学习代码的过程也增长了自己对语言的认知和理解。
在此,项目中也用上了JSPatch
,试想如果结合Aspect
,通过runtime强大的功能,我们几乎能做到任何时间(坏坏的事情不要想哦)。未来如果有很好的用例,也会分享出来。