李峰峰博客

Runtime 拾遗

2020-05-12

1、self 和 super

super 是每个开发者都很熟悉的东西,我们经常在重写父类方法的时候,经常通过 super 去调用一下父类的实现,例如:

1
2
3
4
5
6
- (instancetype)init {
if (self = [super init]) {
// your code
}
return self;
}

那么 super 到底是什么呢?先看一个例子,假设继承链为 Son -> Parent -> NSObject,那么下面逻辑将会如何打印?

1
2
3
4
5
6
7
8
9
10
- (instancetype)init {
if (self = [super init]) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[self superclass] = %@", [self superclass]);

NSLog(@"[super class] = %@", [super class]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}

这个例子或许看起来很简单,按照常规理解,上面代码打印结果可能是:

1
2
3
4
[self class] = Son
[self superclass] = Parent
[super class] = Parent
[super superclass] = NSObject

然而,实际打印结果却并非如此,正确打印结果为:

1
2
3
4
[self class] = Son
[self superclass] = Parent
[super class] = Son
[super superclass] = Parent


结果很意外,[super class] 竟然还是 Son,[super superclass] 竟然还是 Parent。

为了探索原理,现在使 Son 重写 Parent 类的 test 方法,并在其中调用 [super test],如下:

1
2
3
4
5
6
7
@implementation Son

- (void)test {
[super test];
}

@end

执行如下命令:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Son.m

将 Son.m 转成 C/C++ 代码,test 实现如下:

1
2
3
static void _I_Son_test(Son * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("test"));
}

为了逻辑更清晰,对 [super test] 的实现简化处理后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct __rw_objc_super arg = {
self,
class_getSuperclass(objc_getClass("Son"))
};

objc_msgSendSuper(arg, @selector(test));

__rw_objc_super 对应结构体如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;

/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};

简化处理后:

1
2
3
4
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};

根据以上实现可知,[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
2
3
4
5
6
7
@implementation Parent

- (void)test {
NSLog(@"self = %@", self);
}

@end

打印结果:

1
self = <Son: 0x100609aa0>


对于前面为什么 [super class] = Son 也比较容易理解了,class 方法的实现如下:

1
2
3
4
// 由于是调用的是实例方法,所以是减号开头的方法
- (Class)class {
return object_getClass(self);
}

class 方法查找路径为:Parent -> NSObject -> nil,最终会在 NSObject 中找到 class 方法。

调用 class 方法时,消息接受者为 self(即 Son 的实例对象),所以 [super class] = Son[super superclass] = Parent 也是同理。

2、isKindOfClass 和 isMemberOfClass

先看一个例子,TestObject 是 NSObject 子类,下面代码打印结果是什么?

1
2
3
4
NSLog(@"result1 = %d", [[NSObject class] isKindOfClass:[NSObject class]]);
NSLog(@"result2 = %d", [[NSObject class] isMemberOfClass:[NSObject class]]);
NSLog(@"result3 = %d", [[TestObject class] isKindOfClass:[TestObject class]]);
NSLog(@"result4 = %d", [[TestObject class] isMemberOfClass:[TestObject class]]);

等同于:

1
2
3
4
NSLog(@"result1 = %d", [NSObject isKindOfClass:[NSObject class]]);
NSLog(@"result2 = %d", [NSObject isMemberOfClass:[NSObject class]]);
NSLog(@"result3 = %d", [TestObject isKindOfClass:[TestObject class]]);
NSLog(@"result4 = %d", [TestObject isMemberOfClass:[TestObject class]]);

打印结果:

1
2
3
4
result1 = 1
result2 = 0
result3 = 0
result4 = 0

结果看起来很奇怪,原因是什么呢?可以看下 isKindOfClassisMemberOfClass 的源码:

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
- (BOOL)isMemberOfClass:(Class)cls {
// 判断 self 是否是 cls 类型
return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
// 判断 self 是否是 cls 类型或 cls 的子类
// 注意下面是有个 for 循环的,顺着 superclass 查找
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

+ (BOOL)isMemberOfClass:(Class)cls {
// 这里 self 是 class,类对象的类对象,就是元类。所以这里是判断元类是否是 cls 类型
return object_getClass((id)self) == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
// 这里 self 就是 class,所以这里是判断元类是否是 cls 类型或 cls 的子类
// 注意下面是有个 for 循环的,顺着 superclass 查找
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

根据以上源码可以知道,前面例子中调用的都是类方法,以下面逻辑为例:

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
2
3
4
5
6
7
8
9
10
11
12
class_getInstanceMethod:获取实例方法
class_getClassMethod:获取类方法

method_getImplementation:获取方法的实现 IMP
method_setImplemrntation:设置方法的实现 IMP

method_getTypeEncoding:获取方法编码

class_addMethod:新增方法

class_replaceMethod:替换方法的实现 IMP。如:A 替换 B,即:B 指向 A,A 还是指向 A
method_exchangeImplementations:交换两个方法的实现 IMP。如:A 交换 B,即:B 指向 A,A 指向 B

(2) Method Swizzling 的实现

为了保证 Hook 相关逻辑一定被执行到,一般将相关逻辑放在 +load 方法中。

Hook 逻辑如果多次执行,可能会引起 Hook 无效假象。例如,Hook 逻辑执行了两次,会导致方法交换后,又被交换回原始状态。为了避免 +load 方法被主动调用导致 Hook 逻辑多次执行,一般还需要保证相关逻辑只被执行一次。

Hook 的目的一般都是知道原方法何时被执行,并在原方法执行前后增加一些其他逻辑。假设原方法是 xxxxMethod,与 swizzled_xxxxMethod 进行交换,只需要在 swizzled_xxxxMethod 中调用 [self swizzled_xxxxMethod] 即可调用原方法的实现。这里不会引起死循环是因为两个方法的实现已经替换了。

Method Swizzling 的简单(但有缺陷)实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <objc/runtime.h>

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithClass:self originalSEL:@selector(xxxxMethod) swizzledSEL:@selector(swizzled_xxxxMethod)];
});
}

+ (void)methodSwizzlingWithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL {
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)swizzled_xxxxMethod {
[self swizzled_xxxxMethod];
// ...
}

以上是 Hook 一个方法的核心逻辑,演示了如果交换两个方法的实现。但是该实现是有缺陷的。

例如,子类中 Hook 了父类的方法,将 parentMethod 方法(仅在父类有实现)与 swizzled_parentMethod 方法进行交换,但是 swizzled_parentMethod 方法是在子类实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation SonObject

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// parentMethod 是父类中的方法
[self methodSwizzlingWithClass:self originalSEL:@selector(parentMethod) swizzledSEL:@selector(swizzled_parentMethod)];
});
}

// 注意这是在子类实现的
- (void)swizzled_parentMethod {
// 重新调用一下原方法
[self swizzled_parentMethod];
NSLog(@"swizzled_parentMethod");
}

// ...

@end

子类调用 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_replaceMethodswizzled_originalMethod 的实现替换为 originalMethod 的实现即可。

改进后实现如下:

1
2
3
4
5
6
7
8
9
10
11
+ (void)methodSwizzlingWithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL {
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);

BOOL success = class_addMethod(cls, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

这样就保证了子类 parentMethod 方法(我们使用 class_addMethod 手动添加的)的实现被交换成了 swizzled_parentMethod,而父类的 parentMethod 方法没有被交换,不受任何影响。

需要注意的是,类方法是存储在元类中的,如果 Hook 的是类方法,需要与元类的相关方法进行交换,实现如下:

1
2
3
4
5
6
7
8
9
10
11
+ (void)classMethodSwizzlingWithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL {
Method originalMethod = class_getClassMethod(cls, originalSEL);
Method swizzledMethod = class_getClassMethod(cls, swizzledSEL);
Class metaClass = object_getClass(cls);
BOOL success = class_addMethod(metaClass, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(metaClass, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

(3) 数组、字典的方法交换

在 iOS 中 NSArrayNSDictionary 等这些类都是类簇(Class Clusters)。所以当我们直接对这些类进行 Hook 时,不会产生我们想要的效果,需要对这些类的“真身”进行 Hook: