1、self 和 super
super
是每个开发者都很熟悉的东西,我们经常在重写父类方法的时候,经常通过 super
去调用一下父类的实现,例如:
1 | - (instancetype)init { |
那么 super
到底是什么呢?先看一个例子,假设继承链为 Son -> Parent -> NSObject,那么下面逻辑将会如何打印?
1 | - (instancetype)init { |
这个例子或许看起来很简单,按照常规理解,上面代码打印结果可能是:
1 | [self class] = Son |
然而,实际打印结果却并非如此,正确打印结果为:
1 | [self class] = Son |
结果很意外,[super class]
竟然还是 Son,[super superclass]
竟然还是 Parent。
为了探索原理,现在使 Son 重写 Parent 类的 test 方法,并在其中调用 [super test]
,如下:
1 | @implementation Son |
执行如下命令:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Son.m |
将 Son.m 转成 C/C++ 代码,test 实现如下:
1 | static void _I_Son_test(Son * self, SEL _cmd) { |
为了逻辑更清晰,对 [super test]
的实现简化处理后如下:
1 | struct __rw_objc_super arg = { |
简化处理后:
1 | struct objc_super { |
根据以上实现可知,[super test]
转成 C/C++ 后实际上是执行 objc_msgSendSuper
函数。
在 objc_msgSendSuper
函数中,有 2 个参数:
- 第 1 个参数是
objc_super
结构体
结构体里面有两个变量,一个是接收消息的receiver
,可以看到这里receiver = self
,一个是当前类的父类super_class
。 - 第 2 个参数是当前方法的
selector
。
对于 objc_super
结构体中两个成员变量的作用:
- receiver
消息接受者,receiver = self
,即 Son 的实例对象。 - super_class
从super_class
开始查找selector
,即从 Parent 开始查找 test 方法。
也就是说,Son 中调用的 [super test]
是从 Parent 开始查找 test 对应 selector
,查找到 selector
后,以 self
(即 Son 的实例对象) 为消息接受者发送消息。这么设计的原因也很好理解,如果不是从 Parent 开始查找方法而是从 Son 开始查找方法,那边对于 test 方法的调用,将会陷入递归死循环。
这个也很容易验证,我们使 Parent 中 test 方法实现如下:
1 | @implementation Parent |
打印结果:
1 | self = <Son: 0x100609aa0> |
对于前面为什么 [super class] = Son
也比较容易理解了,class
方法的实现如下:
1 | // 由于是调用的是实例方法,所以是减号开头的方法 |
class
方法查找路径为:Parent -> NSObject -> nil,最终会在 NSObject 中找到 class
方法。
调用 class
方法时,消息接受者为 self
(即 Son 的实例对象),所以 [super class] = Son
。[super superclass] = Parent
也是同理。
2、isKindOfClass 和 isMemberOfClass
先看一个例子,TestObject 是 NSObject 子类,下面代码打印结果是什么?
1 | NSLog(@"result1 = %d", [[NSObject class] isKindOfClass:[NSObject class]]); |
等同于:
1 | NSLog(@"result1 = %d", [NSObject isKindOfClass:[NSObject class]]); |
打印结果:
1 | result1 = 1 |
结果看起来很奇怪,原因是什么呢?可以看下 isKindOfClass
、isMemberOfClass
的源码:
1 | - (BOOL)isMemberOfClass:(Class)cls { |
根据以上源码可以知道,前面例子中调用的都是类方法,以下面逻辑为例:
1 | [[NSObject class] isKindOfClass:[NSObject class]] |
object_getClass(self)
即 self->isa
的指向,[NSObject class]
就是 NSObject 类本身,其 isa
指向是 NSObject 元类,NSObject 元类是 NSObject 的子类。所以上面判断结果是 YES。
对于下面逻辑:
1 | [[NSObject class] isMemberOfClass:[NSObject class]] |
同样,NSObject 元类是 NSObject 的子类,自然不等于 NSObject,所以判断结果为 NO。
对于下面逻辑:
1 | [[TestObject class] isKindOfClass:[TestObject class]] |
TestObject->isa
是 TestObject 元类,TestObject 元类继承自 NSObject 元类,NSObject 元类继承自 NSObject。所以上面逻辑判断路径是:
TestObject 元类 -> NSObject 元类 -> NSObject,逐个判断他们是否是 TestObject,结果肯定是 NO。
下面逻辑同理:
1 | [[TestObject class] isMemberOfClass:[TestObject class]] |
TestObject 元类肯定不是 TestObject,所以判断结果为 NO。
最后,再附上这张经典的图:
3、Method Swizzling
(1) Method Swizzling 原理简述
Method Swizzling 即常说的 Hook(后续也直接称为 Hook),利用 Runtime 动态替换方法的实现。
Runtime 提供了很多和方法操作相关的 API:
1 | class_getInstanceMethod:获取实例方法 |
(2) Method Swizzling 的实现
为了保证 Hook 相关逻辑一定被执行到,一般将相关逻辑放在 +load
方法中。
Hook 逻辑如果多次执行,可能会引起 Hook 无效假象。例如,Hook 逻辑执行了两次,会导致方法交换后,又被交换回原始状态。为了避免 +load 方法被主动调用导致 Hook 逻辑多次执行,一般还需要保证相关逻辑只被执行一次。
Hook 的目的一般都是知道原方法何时被执行,并在原方法执行前后增加一些其他逻辑。假设原方法是 xxxxMethod
,与 swizzled_xxxxMethod
进行交换,只需要在 swizzled_xxxxMethod
中调用 [self swizzled_xxxxMethod]
即可调用原方法的实现。这里不会引起死循环是因为两个方法的实现已经替换了。
Method Swizzling 的简单(但有缺陷)实现如下:
1 |
|
以上是 Hook 一个方法的核心逻辑,演示了如果交换两个方法的实现。但是该实现是有缺陷的。
例如,子类中 Hook 了父类的方法,将 parentMethod
方法(仅在父类有实现)与 swizzled_parentMethod
方法进行交换,但是 swizzled_parentMethod
方法是在子类实现的:
1 | @implementation SonObject |
子类调用 parentMethod 是没有问题的:
1 | [[SonObject new] parentMethod]; |
但是父类再调用 parentMethod 就会发生 Crash:
1 | [[ParentObject new] parentMethod]; |
Crash 内容:
1 | -[ParentObject swizzled_parentMethod]: unrecognized selector sent to instance 0x2821cc740 |
可以看到,父类再调用 parentMethod
时,由于交换了方法的实现,实际调用的是 swizzled_parentMethod
,但是父类并没有实现该方法,引起了 Crash。
改进:Hook 应当只影响当前类而不影响父类
实现方式也很简单,假设要被 Hook 的方法是 originalMethod
,要与 originalMethod
交换实现的方法是 swizzled_originalMethod
,先使用 class_addMethod
尝试添加一下 originalMethod
。
如果添加失败,说明当前类中已经实现了 originalMethod
,直接 Hook 即可。
如果添加成功,说明当前类之前没有实现 originalMethod
,但是这次新增了 originalMethod
,所以再调用 class_replaceMethod
将 swizzled_originalMethod
的实现替换为 originalMethod
的实现即可。
改进后实现如下:
1 | + (void)methodSwizzlingWithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL { |
这样就保证了子类 parentMethod
方法(我们使用 class_addMethod
手动添加的)的实现被交换成了 swizzled_parentMethod
,而父类的 parentMethod
方法没有被交换,不受任何影响。
需要注意的是,类方法是存储在元类中的,如果 Hook 的是类方法,需要与元类的相关方法进行交换,实现如下:
1 | + (void)classMethodSwizzlingWithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL { |
(3) 数组、字典的方法交换
在 iOS 中 NSArray
、NSDictionary
等这些类都是类簇(Class Clusters)。所以当我们直接对这些类进行 Hook 时,不会产生我们想要的效果,需要对这些类的“真身”进行 Hook:
- 本文章采用 知识共享署名 4.0 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。