OC Runtime

Runtime 简称运行时。OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。

Runtime 简介

Runtime 简称运行时。是一套 纯 C (C 和汇编) 写的 API,OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。

  • 对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
  • 对于 OC 的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 事实证明:
    • 在编译阶段,OC 可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
    • 在编译阶段,C 语言调用未实现的函数就会报错。
  • 如果向某个对象传递消息,在底层,所有的方法都是普通的 C 语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全取决于运行期决定,甚至可能在运行期改变,这些特性使得 Objective-C 变成一门真正的动态语言。
  • 在 Runtime 中,对象可以用 C 语言中的结构体表示,而方法可以用 C 函数来实现,另外再加上了一些额外的特性。这些结构体和函数被 Runtime 函数封装后,让 OC 的面向对象编程变为可能。

Objective-C 中的数据结构

描述 Objective-C 对象所有的数据结构定义都在 Runtime 的头文件里,下面我们逐一分析。

id

运行期系统如何知道某个对象的类型呢?对象类型并不是在编译期就知道了,而是要在运行期查找。Objective-C 有个特殊的类型 id,它可以表示 Objective-C 的任意对象类型,id 类型定义在 Runtime 的头文件中:

1
2
3
struct objc_object {
Class isa;
} *id;

由此可见,每个对象结构体的首个成员是 Class 类的变量。该变量定义了对象所属的类,通常称为 isa 指针。

objc_object

objc_object 是表示一个类的实例的结构体 它的定义如下 (objc/objc.h):

1
2
3
4
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,这个结构体只有一个字体,即指向其类的 isa 指针。这样,当我们向一个 Objective-C 对象发送消息时,运行时库会根据实例对象的 isa 指针找到这个实例对象所属的类。Runtime 库会在类的方法列表及父类的方法列表中去寻找与消息对应的 selector 指向的方法,找到后即运行这个方法。

Class

Class 对象也定义在 Runtime 的头文件中,查看 objc/runtime.h 中的 objc_class 结构体: Objective-C 中,类是由 Class 类型来表示的,它实际上是一个指 向 objc_class 结构体的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
}

下面说下 Class 的结构体中的几个主要变量:

  • isa: 结构体的首个变量也是 isa 指针,这说明 Class 本身也是 Objective-C 中的对象。isa 指针非常重要,对象需要通过 isa 指针找到它的类,类需要通过 isa 找到它的元类。这在调用实例方法和类方法的时候起到重要的作用.
  • super_class: 结构体里还有个变量是 super_class,它定义了本类的超类。类对象所属类型(isa 指针所指向的类型)是另外一个类,叫做 “元类”。
  • ivars: 成员变量列表,类的成员变量都在 ivars 里面。
  • methodLists: 方法列表,类的实例方法都在 methodLists 里,类方法在元类的 methodLists 里面。methodLists 是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是 Category 实现的原理,同时也说明了 Category 只可以为对象添加成员方法,不能添加成员变量。
  • cache: 方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到 cache 列表中,下次调用的时候,会优先从 cache 列表中寻找,如果 cache 没有,才从 methodLists 中查找方法。提高效率。

元类 (Meta Class)

meta-class 是一个类对象的类。 在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息 (即调用类方法)。既然是对象,那么它也是一个 objc_object 指针,它包含一个指向其类的一个 isa 指针。那么,这个 isa 指针指向什么呢? 为了调用类方法,这个类的 isa 指针必须指向一个包含这些类方法的一个 objc_class 结构体。这就引出了 meta-class 的概念,meta-class 中存储着一个类的所有类方法。 所以,调用类方法的这个类对象的 isa 指针指向的就是 meta-class 当我们向一个对象发送消息时,runtime 会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的 meta-class 的方法列表中查找。

再深入一下,meta-class 也是一个类,也可以向它发送一个消息,那么它的 isa 又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的 meta-class 的 isa 指向基类的 meta-class,以此作为它们的所属类。

即,任何 NSObject 继承体系下的 meta-class 都使用 NSObject 的 meta-class 作为自己的所属类,而基类的 meta-class 的 isa 指针是指向它自己。

通过上面的描述,再加上对 objc_class 结构体中 super_class 指针的分析,我们就可以描绘出类及相应 meta-class 类的一个继承体系了,如下

me<x>taClass.png

看图说话: 上图中:superclass 指针代表继承关系,isa 指针代表实例所属的类。 类也是一个对象,它是另外一个类的实例,这个就是 “元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的 isa 指向类,类对象的 isa 指向元类,元类对象的 isa 指针指向一个 “根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

Class 是一个指向 objc_class 结构体的指针,而 id 是一个指向 objc_object 结构体的指针,其中的 isa 是一个指向 objc_class 结构体的指针。其中的 id 就是我们所说的对象,Class 就是我们所说的类。 2.isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 isKindOfClass: 方法来确定实例对象的类。因为 KVO 的实现机制就是将被观察对象的 isa 指针指向一个中间类而不是真实的类。

Category

Category 是表示一个指向分类的结构体的指针,其定义如下:

1
2
3
4
5
6
7
8
typedef struct objc_category *Category
struct objc_category{
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中 instance_methods 列表是 objc_class 中方法列表的一个子集,而 class_methods 列表是元类方法列表的一个子集。 可发现,类别中没有 ivar 成员变量指针,也就意味着:类别中不能够添加实例变量和属性

1
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表

SEL

SEL 是选择子的类型,选择子指的就是方法的名字。在 Runtime 的头文件中的定义如下:

1
typedef struct objc_selector *SEL;

它就是个映射到方法的 C 字符串,SEL 类型代表着方法的签名,在类对象的方法列表中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的 SEL 类型的对象,根据一个 SEL 对象就可以找到方法的地址,进而调用方法。SEL 又叫选择器,是表示一个方法的 selector 的指针,其定义如下: 方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识 (Int 类型的地址),这个标识就是 SEL。 两个类之间,只要方法名相同,那么方法的 SEL 就是一样的,每一个方法都对应着一个 SEL。所以在 Objective-C 同一个类 (及类的继承体系) 中,不能存在 2 个同名的方法,即使参数类型不同也不行 如在某一个类中定义以下两个方法:错误

1
2
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

当然,不同的类可以拥有相同的 selector,这个没有问题。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP。 工程中的所有的 SEL 组成一个 Set 集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的 SEL 就行了,SEL 实际上就是根据方法名 hash 化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比! 本质上,SEL 只是一个指向方法的指针(准确的说,只是一个根据方法名 hash 化了的 KEY 值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。 @selector () 就是取类方法的编号 通过下面三种方法可以获取 SEL: a、sel_registerName 函数 b、Objective-C 编译器提供的 @selector () c、NSSelectorFromString () 方法

Method

Method 代表类中的某个方法的类型,在 Runtime 的头文件中的定义如下:

1
typedef struct objc_method *Method;

objc_method 的结构体定义如下:

1
2
3
4
5
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
  • method_name:方法名。
  • method_types:方法类型,主要存储着方法的参数类型和返回值类型。
  • IMP:方法的实现,函数指针。(下文详解) class_copyMethodList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的成员方法列表。

Method 用于表示类定义中的方法 我们可以看到该结构体中包含一个 SEL 和 IMP,实际上相当于在 SEL 和 IMP 之间作了一个映射。有了 SEL,我们便可以找到对应的 IMP,从而调用方法的实现代码。

Ivar

Ivar 代表类中实例变量的类型,在 Runtime 的头文件中的定义如下:

1
typedef struct objc_ivar *Ivar;

objc_ivar 的定义如下:

1
2
3
4
5
6
7
8
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}

class_copyIvarList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的成员变量列表。

objc_property_t

objc_property_t 是属性,在 Runtime 的头文件中的的定义如下:

1
typedef struct objc_property *objc_property_t;

class_copyPropertyList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的属性列表。

IMP

IMP 在 Runtime 的头文件中的的定义如下:

1
typedef id (*IMP)(id, SEL, ...);

IMP 是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。IMP 实际上是一个函数指针,指向方法实现的地址。 其定义如下:

1
id (*IMP)(id, SEL,...)

第一个参数:是指向 self 的指针 (如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针) 第二个参数:是方法选择器 (selector) 接下来的参数:方法的参数列表。

前面介绍过的 SEL 就是为了查找方法的最终实现 IMP 的。由于每个方法对应唯一的 SEL,因此我们可以通过 SEL 方便快速准确地获得它所对应的 IMP,查找过程将在下面讨论。取得 IMP 后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的 C 语言函数一样来使用这个函数指针了。

Cache

Cache 在 Runtime 的头文件中的的定义如下:

1
typedef struct objc_cache *Cache

objc_cache 的定义如下:

1
2
3
4
5
struct objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

每调用一次方法后,不会直接在 isa 指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到 cache 列表中,下次的时候,就直接优先从 cache 列表中寻找,如果 cache 没有,才从 isa 指向的类的方法列表(methodLists)中查找方法。提高效率。

发送消息(objc_msgSend)

我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。在 Objective-C 中,调用方法是经常使用的。用 Objective-C 的术语来说,这叫做 “传递消息”(pass a message)。消息有 “名称”(name)或者 “选择子”(selector),也可以接受参数,而且可能还有返回值。 如果向某个对象传递消息,在底层,所有的方法都是普通的 C 语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全取决于运行期决定,甚至可能在运行期改变,这些特性使得 Objective-C 变成一门真正的动态语言。 给对象发送消息可以这样来写:

1
id returnValue = [someObject message:parm];

someObject 叫做 “接收者”(receiver),message 是 “选择子”(selector),选择子和参数结合起来就叫做 “消息”(message)。编译器看到此消息后,将其转换成 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend,其原型如下:

1
id objc_msgSend (id self, SEL _cmd, ...);

后面的… 表示这是个 “参数个数可变的函数”,能接受两个或两个以上的参数。第一个参数是接收者(receiver),第二个参数是选择子(selector),后续参数就是消息中传递的那些参数(parm),其顺序不变。

编译器会把上面的那个消息转换成:

1
id returnValue objc_mgSend(someObject, @selector(message:), parm);

objc_msgSend 发送消息的原理:

  • 第一步:检测这个 selector 是不是要被忽略的。
  • 第二步:检测这个 target 是不是 nil 对象。(nil 对象执行任何一个方法都不会 Crash,因为会被忽略掉)
  • 第三步:首先会根据 target (objc_object) 对象的 isa 指针获取它所对应的类 (objc_class)。
  • 第四步:查看缓存中是否存在方法,系统把近期发送过的消息记录在其中,Apple 认为这样可以提高效率:优先在类(class)的 cache 里面查找是否有与选择子(selector)名称相符的方法。 如果有,则找到 objc_method 中的 IMP 类型(函数指针)的成员 method_imp 去找到实现内容,并执行; 如果缓存中没有命中,那么到该类的方法表 (methodLists) 查找该方法,依次从后往前查找。
  • 第五步:如果没有在类(class)找到,再到父类(super_class)查找,直至根类。
  • 第六步:一旦找到与选择子(selector)名称相符的方法,就跳至其实现代码。
  • 第七步:如果没有找到,就会执行消息转发(message forwarding)的第一步动态解析。

如果是调用类方法 objc_class 中的 isa 指向该类的元类 (metaclass) 如果是调用类方法的话,那么就会利用 objc_class 中的成员 isa 找到元类 (metaclass),然后寻找方法,直至根 metaclass, 没有找到的话则仍然进入动态解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <objc/message.h>
// 创建person对象
Person *p = [[Person alloc] init];
// 调用对象方法
[p eat];
// 本质:让对象发送消息
objc_msgSend(p, @selector(eat));
// 调用类方法的方式:两种
// 第一种通过类名调用
[Person eat];
// 第二种通过类对象调用
[[Person class] eat];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 本质:让类对象发送消息
objc_msgSend([Person class], @selector(eat));

任何方法调用本质:就是发送一个消息(用 runtime 发送消息,OC 底层实现通过 runtime 实现),每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

QQ20190802-141627.png

消息转发 (message forwarding)

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以 [object message] 的方式调用方法,如果 object 无法响应 message 消息时,编译器会报错。但如果是以 perform... 的形式来调用,则需要等到运行时才能确定 object 是否能接收 message 消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用 respondsToSelector: 来判断一下。如下代码所示:

1
2
3
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用 respondsToSelector: 判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓 消息转发 (message forwarding) 机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:

1
2
-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'

这段异常信息实际上是由 NSObject 的 doesNotRecognizeSelector 方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。 消息转发机制基本上分为三个步骤:

    1. 动态方法解析
    1. 备用接收者
    1. 完整转发 下面我们详细讨论一下这三个步骤。

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod:(实例方法) 或者 +resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个” 处理方法””。不过使用该方法的前提是我们已经实现了该 “处理方法”,只需要在运行时通过 class_addMethod 函数动态添加到类里面就可以了。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
void otherEat(id self, SEL cmd) {
NSLog(@"blog.yoonangel.com");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
class_addMethod(self, sel, (IMP)otherEat, "[email protected]");
return YES;
}
return [super resolveInstanceMethod:sel];
}

class_addMethod 方法可谓是核心,那么依次来看它的参数的含义:

  • first:添加到哪个类
  • second:添加方法的方法编号(选择子)
  • third:添加方法的函数实现 (IMP 函数指针)
  • fourth:IMP 指针指向的函数返回值和参数类型 v 代表无返回值 void @代表 id 类型对象 ->self : 代表选择子 SEL->_cmd

这种方案更多的是为了实现 @dynamic 属性。

备用接收者

如果在上一步无法处理消息,则 Runtime 会继续调以下方法:

1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非 nil 的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是 self 自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理 aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:

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
28
29
30
31
32
33
34
35
36
37
@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[SUTRuntimeMethodHelper alloc] init];
}
return self;
}
- (void)test {
[self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 将消息转发给_helper来处理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括 selector,目标 (target) 和参数。我们可以在 forwardInvocation 方法中选择将消息转发给其它对象。 forwardInvocation: 方法的实现有两个任务:

    1. 定位可以响应封装在 anInvocation 中的消息的对象。这个对象不需要能处理所有未知消息。
    1. 使用 anInvocation 作为参数,将消息发送到选中的对象。anInvocation 将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。 还有一个很重要的问题,我们必须重写以下方法:

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建 NSInvocation 对象。因此我们必须重写这个方法,为给定的 selector 提供一个合适的方法签名。 完整的示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}

NSObject 的 forwardInvocation: 方法实现只是简单调用了 doesNotRecognizeSelector: 方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。 从某种意义上来讲,forwardInvocation: 就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象,这取决于具体的实现。

消息转发与多重继承

回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟 “多重继承” 的某些特性,让对象可以 “继承” 其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。 不过消息转发虽然类似于继承,但 NSObject 的一些方法还是能区分两者。如 respondsToSelector:isKindOfClass: 只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector])
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

当一个对象在收到无法解读的消息之后,它会将消息实施转发。转发的主要步骤如下:

消息转发步骤:

  • 第一步:对象在收到无法解读的消息后,首先调用 resolveInstanceMethod:方法决定是否动态添加方法。如果返回 YES,则调用 class_addMethod 动态添加方法,消息得到处理,结束;如果返回 NO,则进入下一步;
  • 第二步:当前接收者还有第二次机会处理未知的选择子,在这一步中,运行期系统会问:能不能把这条消息转给其他接收者来处理。会进入 forwardingTargetForSelector: 方法,用于指定备选对象响应这个 selector,不能指定为 self。如果返回某个对象则会调用对象的方法,结束。如果返回 nil,则进入下一步;
  • 第三步:这步我们要通过 methodSignatureForSelector: 方法签名,如果返回 nil,则消息无法处理。如果返回 methodSignature,则进入下一步;
  • 第四步:这步调用 forwardInvocation:方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入 doesNotRecognizeSelector 方法,抛出异常,此异常表示选择子最终未能得到处理。
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
消息转发第一步:对象在收到无法解读的消息后,首先调用此方法,可用于动态添加方法,方法决定是否动态添加方法。如果返回YES,则调用class_addMethod动态添加方法,消息得到处理,结束;如果返回NO,则进入下一步;
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
/**
当前接收者还有第二次机会处理未知的选择子,在这一步中,运行期系统会问:能不能把这条消息转给其他接收者来处理。会进入此方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入下一步;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
/**
这步我们要通过该方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步。
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"study"])
{
return [NSMethodSignature signatureWithObjCTypes:"[email protected]:"];
}
return [super methodSignatureForSelector:aSelector];
}
/**
这步调用该方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法。
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(play)];
[anInvocation invokeWithTarget:self];
}
/**
抛出异常,此异常表示选择子最终未能得到处理。
*/
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"无法处理消息:%@", NSStringFromSelector(aSelector));
}

QQ20190804-183729.png

接收者在每一步中均有机会处理消息,步骤越靠后,处理消息的代价越大。最好在第一步就能处理完,这样系统就可以把此方法缓存起来了。

关联对象 (AssociatedObject)

使用场景: 可以在类别中添加属性 有时我们需要在对象中存放相关信息,Objective-C 中有一种强大的特性可以解决此类问题,就是 “关联对象”。 可以给某个对象关联许多其他对象,这些对象通过 “键” 来区分。存储对象值时,可以指明 “存储策略”,用以维护相应地 “内存管理语义”。存储策略由名为 “objc_AssociationPolicy” 的枚举所定义。下表中列出了该枚举值得取值,同时还列出了与之等下的 @property 属性:假如关联对象成为了属性,那么他就会具备对应的语义。

1. 设置关联值 参数说明: object:与谁关联,通常是传 self key:唯一键,在获取值时通过该键获取,通常是使用 static const void * 来声明 value:关联所设置的值 policy:内存管理策略,比如使用 copy

1
2
// 以给定的键和策略为某对象设置关联对象值。
void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)

2. 获取关联值 参数说明: object:与谁关联,通常是传 self,在设置关联时所指定的与哪个对象关联的那个对象 key:唯一键,在设置关联时所指定的键

1
2
// 根据给定的键从某对象中获取对应的对象值。
id objc_getAssociatedObject(id object, const void *key)

3. 取消关联

1
2
// 移除指定对象的全部关联对象。
void objc_removeAssociatedObjects(id object)

关联策略

1
2
3
4
5
6
7
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用关联,通常是基本数据类型 @property (assign) or @ property (unsafe_unretained)
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示强引用关联对象,是线程安全的 @property (nonatomic, strong)
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示关联对象copy,是线程安全的 @property (nonatomic, copy)
OBJC_ASSOCIATION_RETAIN = 01401, // 表示强引用关联对象,不是线程安全的 @property (atomic, strong)
OBJC_ASSOCIATION_COPY = 01403 // 表示关联对象copy,不是线程安全的 @property (atomic, copy)
};

交换方法 (method swizzing) 黑魔法

开发使用场景: 系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。

  1. 简单的说就是方法交换。

  2. Objective-C 中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是 selector 的名字。利用 Objective-C 的动态特性,可以实现在运行时偷换 selector 对应的方法实现,达到给方法挂钩的目的

  3. 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector 的本质其实就是方法名,IMP 有点类似函数指针,指向具体的 Method 实现,通过 selector 就可以找到对应的 IMP

QQ20190804-184655.png

QQ20190804-184728.png

Objective-C 中提供了三种 API 来动态替换类方法或实例方法的实现:

  1. class_replaceMethod 替换类方法的定义。
1
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
  1. method_exchangeImplementations 交换两个方法的实现。
1
method_exchangeImplementations(Method m1, Method m2)
  1. method_setImplementation 设置一个方法的实现
1
method_setImplementation(Method m, IMP imp)

三种方法的区别

  • class_replaceMethod:当类中没有想替换的原方法时,该方法调用 class_addMethod 来为该类增加一个新方法,也正因如此,class_replaceMethod 在调用时需要传入 types 参数,而其余两个却不需要。
  • method_exchangeImplementations:内部实现就是调用了两次 method_setImplementation 方法。 再来看看他们的使用场景:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(willMoveToSuperview:);
SEL swizzledSelector = @selector(myWillMoveToSuperview:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod = class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)myWillMoveToSuperview:(UIView *)newSuperview
{
NSLog(@"WillMoveToSuperview: %@", self);
[self myWillMoveToSuperview:newSuperview];
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
// 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
UIImage *image = [UIImage imageNamed:@"123"];
}
@end
@implementation UIImage (Image)
// 加载分类到内存的时候调用
+ (void)load
{
// 交换方法
// 获取imageWithName方法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
// 获取imageName方法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
// 交换方法地址,相当于交换实现方式
method_exchangeImplementations(imageWithName, imageName);
}
// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
// 这里调用imageWithName,相当于调用imageName
UIImage *image = [self imageWithName:name];
if (image == nil) {
NSLog(@"加载空的图片");
}
return image;
}
@end
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method objectAtIndex = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method db_objectAtIndex = class_getInstanceMethod(self, @selector(db_objectAtIndex:));
method_exchangeImplementations(objectAtIndex, db_objectAtIndex);
});
}
- (id)db_objectAtIndex:(NSUInteger)inex{
NSLog(@"%s",__FUNCTION__);
id item;
if ( self.count > inex ) {
item = [self db_objectAtIndex:inex];
}
else{
item = nil;
}
return item;
}

总结

1.class_replaceMethod,当需要替换的方法有可能不存在时,可以考虑使用该方法。 2.method_exchangeImplementations,当需要交换两个方法的时使用。 3.method_setImplementation 是最简单的用法,当仅仅需要为一个方法设置其实现方式时实现。

Swizzling 应该总是在 + load 中执行

在 Objective-C 中,运行时会自动调用每个类的两个方法。+load 会在类初始加载时调用,+initialize 会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于 method swizzling 会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load 能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize 在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling 应该总是在 dispatch_once 中执行

与上面相同,因为 swizzling 会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD 的 dispatch_once 可以确保这种行为,我们应该将其作为 method swizzling 的最佳实践。

选择器、方法与实现

在 Objective-C 中,选择器 (selector)、方法 (method) 和实现 (implementation) 是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是 Objective-C Runtime Reference 中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个 C 字符串,它是在 Objective-C 运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前 CPU 架构实现的标准 C 调用规范。每一个参数是指向对象自身的指针 (self),第二个参数是方法选择器。然后是方法的实际参数。

理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法 (Method),其中 key 是一个特定名称,即选择器 (SEL),其对应一个实现 (IMP),即指向底层 C 函数的指针。

为了 swizzle 一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

在 Cocoa 编程中,大部分的类都继承于 NSObject ,有些 NSObject 提供的方法仅仅是为了查询运动时系统的相关信息,这此方法都可以反查自己。比如 -isKindOfClass:-isMemberOfClass: 都是用于查询在继承体系中的位置。 -respondsToSelector: 指明是否接受特定的消息。 +conformsToProtocol: 指明是否要求实现在指定的协议中声明的方法。 -methodForSelector: 提供方法实现的地址。

简单概括下 Runtime 的方法列表和用法

  1. objc_getClass 获取类名
  2. objc_msgSend 调用对象的 sel
  3. class_getClassMethod 获取类方法
  4. method_exchangeImplementations 交换两个方法
  5. class_addMethod 给类添加方法
  6. class_copyIvarList 获取成员变量信息
  7. class_copyPropertyList 获取属性信息
  8. class_copyMethodList 获取方法信息
  9. class_copyProtocolList 获取协议信息
  10. objc_setAssociatedObject 动态关联 set 方法
  11. objc_getAssociatedObject 动态关联 get 方法
  12. ivar_getName 获取变量名 char * 类型
  13. ivar_getTypeEncoding 获取到属性变量的类型

实例 && 杂项

验证示例:方法调用,是否真的是转换为消息机制?

消息机制原理:对象根据方法编号 SEL 去映射表查找对应的方法实现。

QQ20190804-182447.png

注解:

  1. 必须要导入头文件 #import <objc/message.h>

  2. 我们导入系统的头文件,一般用尖括号。

  3. OC 解决消息机制方法提示步骤【查找 build setting -> 搜索 msg -> objc_msgSend(YES –> NO)】

  4. 最终生成消息机制,编译器做的事情,最终代码,需要把当前代码用 xcode 重新编译,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)

  5. 这里一般不会直接导入 <objc/runtime.h>

cb1wj-jxcr7.jpg

实例代码: OC 方法 <—> runtime 方法

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
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
说明:
eat(无参) 和 run(有参NSInteger) 是 LNPerson模型类中的私有方法「runtime 作用:可以调用私有方法」
示例分别以 OC写法 和 最底层写法 对照验证.
*/
- (void)msgSend
{
// 方法一:
//id objc = [NSObject alloc];
LNPerson *person = objc_msgSend(objc_getClass("LNPerson"), sel_registerName("alloc"));
//objc = [objc init];
person = objc_msgSend(person, sel_registerName("init"));
// 调用
//[objc eat];
//[objc run:10];
objc_msgSend(person,@selector(eat)); // 无参
objc_msgSend(person,@selector(run:),10); // 有残
}
/
注解:
// 用最底层写
objc_getClass(const char *name) 获取当前类
sel_registerName(const char *str) 注册个方法编号
objc_msgSend(id self:谁发送消息, SEL op:发送什么消息, ...)
让LNPerson这个类对象发送了一个alloc消息,返回一个分配好的内存对象给你,再发送一个消息初始化.
*/
// 方法二:
#pragma mark - 也许下面这种好理解一点
- (void)test
{
// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));
// objc = [objc init];
objc = objc_msgSend(objc, @selector(eat));
}

objc_msgSend 参数概念

objc_msgSend(<#id _Nullable self#>, <#SEL _Nonnull op, …#>)

1、objc_msgSend
这是个最基本的用于发送消息的函数。
其实编译器会根据情况在objc_msgSendobjc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

2、SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()``或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

3、id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

消息机制「方法调用流程」


面试:消息机制方法调用流程❓
怎么去调用 eat 方法,
对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类 (Meta Class) 中方法列表)。

  1. OC 在向一个对象发送消息时,runtime 库会根据对象的 isa 指针找到该对象对应的类或其父类中查找方法。

  2. 注册方法编号(这里用方法编号的好处,可以快速查找)。

  3. 根据方法编号去查找对应方法。
  4. 找到只是最终函数实现地址,根据地址去方法区调用对应函数。

补充:一个 objc 对象的 isa 的指针指向什么?有什么作用?
每一个对象内部都有一个 isa 指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

isa 指针相关释义

上面也提到 OC 底层都是转化为 runtime 方式来实现的,类和类的实例(对象)都相对于的 isa 指针
我们可以在 Xcode 中使用 [Shift+Cmd+O] 快速打开文件 objc.h 能看到类的定义:

objc.h

QQ20190804-222311.png

isa: 是一个 Class 类型的指针

QQ20190804-222359.png

runtime 对象,类,元类的isa指针关系图

QQ20190804-222539.png

总结:runtime 对象,类,元类的 isa 指针关系图
1、每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的 isa 指针指向所属类
2、每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的 isa 指针指向元类
3、元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类通过 isa 指针最终指向的是一个根元类 (root meteClass)
4、根元类的 isa 指针指向本身,这样形成了一个封闭的内循环

常见作用

  1. 动态交换两个方法的实现
  2. 动态添加属性
  3. 实现字典转模型的自动转换
  4. 动态添加方法
  5. 拦截并替换方法
  6. 实现 NSCoding 的自动归档和解档

补充常用runtime示例:

  1. 添加属性和交换方法示例:UITextField占位文字颜色 placeholderColor
  2. 交换方法示例:交换dealloc方法实现,添加功能那个控制器被销毁了

开发场景「工作掌握」

runtime 交换方法

场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

需求:加载一张图片直接用 [UIImage imageNamed:@"image"]; 是无法知道到底有没有加载成功。给系统的 imageNamed 添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.

步骤

  1. 给系统的方法添加分类
  2. 自己实现一个带有扩展功能的方法
  3. 交换方法,只需要交换一次

场景代码:方法 + 调用 + 打印输出

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#import "UIImage+Image.h"
#import <objc/message.h>
@implementation UIImage (Image)
/
看清楚下面是不会有死循环的
调用 imageNamed => ln_imageNamed
调用 ln_imageNamed => imageNamed
*/
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"runtime交互方法 -> 图片加载成功");
} else {
NSLog(@"runtime交互方法 -> 图片加载失败");
}
return image;
}
/
注解:
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
所以第二步,我们要 自己实现一个带有扩展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/
/
作用:把类加载进内存的时候调用,只会调用一次
调用:方法应先交换,再去调用
*/
+ (void)load {
// 1.获取 imageNamed方法地址
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.获取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
- - -
//方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
//方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageView.image = [UIImage imageNamed:@"CoerLN"];
}
- - -
// 打印输出
2019-08-01 17:52:14.693 runtime[12761:543574] runtime交互方法 -> 图片加载成功

总结
我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的 imageNamed: 方法调用前,所以将代码写在了分类的 load 方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。

给系统分类动态添加属性

场景:给系统的类添加额外属性的时候,可以使用 runtime 动态添加属性方法。
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
注解:给系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了 @property,但是仅仅会自动生成 getset 方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过 runtime 就可以做到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

场景代码:方法 + 调用 + 打印

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
28
29
30
31
32
33
34
35
36
37
#import <Foundation/Foundation.h>
@interface NSObject (Property)
@property NSString *name;
@end
- - -
#import "NSObject+Property.h"
#import <objc/message.h>
//#import <objc/runtime.h>
@implementation NSObject (Property)
- (NSString *)name
{
// 利用参数key 将对象object中存储的对应值取出来
return objc_getAssociatedObject(self, @"name");
}
- (void)setName:(NSString *)name
{
/**
将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(<#id _Nonnull object#>:给哪个对象添加属性, <#const void * _Nonnull key#>:属性名称, <#id _Nullable value#>:属性值, <#objc_AssociationPolicy policy#>:保存策略)
*/
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"name---->%p",name);
}
@end
// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"CoderLN";
NSLog(@"runtime动态添加属性name==%@",objc.name);
// 打印输出
2019-08-01 19:37:10.530 runtime[12761:543574] runtime动态添加属性name == CoderLN

总结
其实,属性赋值的本质,就是让属性与一个对象产生关联,所以要给 NSObject 的分类的 name 属性赋值就是让 nameNSObject 产生关联,而 runtime 可以做到这一点。

#####

字典转模型

字典转模型的方式

  • 给模型中属性,在 .m 依次赋值(初学者)。
  • 字典转模型 KVC 实现
    • KVC 字典转模型弊端:必须保证,模型中的属性和字典中的 key 一一对应。
    • 如果不一致,就会调用 [<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key 找不到的错。
    • 分析:模型中的属性和字典的 key 不一一对应,系统就会调用 setValue:forUndefinedKey: 报错。
    • 解决:重写对象的 setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用 KVC,字典转模型了。
  • 字典转模型 Runtime 实现
    • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找 key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来);提供一个 NSObject 分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
    • 考虑情况
      1. 当字典的 key 和模型的属性匹配不上。
      2. 模型中嵌套模型(模型属性是另外一个模型对象)。
      3. 数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
    • 注解
      根据上面的三种特殊情况,先是字典的 key 和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为 runtime 是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为 nil,就会导致 crash,我们只需加一个判断即可。考虑三种情况下面一一注解
  • MJExtension 字典转模型实现
    • 底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(我之所以看不懂,是 MJ 封装了很多层而已)。

示例:runtime 字典转模型考虑三种情况

Runtime 字典模型

QQ20190804-223222.png

  1. runtime 字典转模型 –> 字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
// 思路:利用runtime 遍历模型中所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
/**
获取类中的所有成员变量
class_copyIvarList(Class _Nullable cls:表示获取哪个类中的成员变量, unsigned int * _Nullable outCount:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值)
返回值Ivar * =
指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到
*/
// 成员变量个数
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名,字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
// 而报错 (could not set nil as the value for the key age.)
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}

注解
这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名,字典中的 key (去掉 _ ,从第一个角标开始截取) 得到属性名。

原因

1
2
3
4
5
6
7
8
9
10
11
{
int _a; // 成员变量
}
@property (nonatomic, assign) NSInteger attitudes_count; // 属性
`Ivar:成员变量,以下划线开头`,
`Property 属性`
`class_copyPropertyList` 获取类里面属性
`class_copyIvarList` 获取类中的所有成员变量
这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用`runtime`字典转模型获取模型属性名的时候,最好获取成员属性名`Ivar`因为可能会有个属性是没有`setter`和`getter`方法的。
  1. runtime 字典转模型 –> 模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 思路:利用runtime 遍历模型中所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值
+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
// 成员变量个数
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替换: @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典,并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User模型, 转换成哪个模型
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict2:value];
}
}
// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
  1. runtime 字典转模型 –> 数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 思路:利用runtime 遍历模型中所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
// 成员变量个数
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//--------------------------- <#我是分割线#> ------------------------------//
//
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
// arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict3:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}

QQ20190804-223528.png

总结
我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

这里提到的你如果不是很清楚,建议参考我的 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。

其它作用「面试熟悉」

动态添加方法

场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

场景代码:方法 + 调用 + 打印输出

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
28
29
30
31
32
33
34
35
36
37
38
39
40
#import "Person.h"
#import <objc/message.h>
@implementation Person
/**
调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
作用:动态添加方法,处理未实现
注解:任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"roll:")) {
/**
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>:给哪个类添加方法, <#SEL _Nonnull name#>:添加哪个方法,即添加方法的方法编号, <#IMP _Nonnull imp#>:方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址)), <#const char * _Nullable types#>:方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd)
*/
// 给类添加roll:滚了多远方法
class_addMethod(self, sel, (IMP)LNRoll, "[email protected]:@");
return YES;
}
if ([NSStringFromSelector(sel) isEqualToString:@"go:"]) {
// 给类添加go:走了多远方法
class_addMethod(self, sel, (IMP)LNGO, "[email protected]:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 调用
Person *p = [[Person alloc] init];
// 执行某个方法
[p performSelector:@selector(roll:) withObject:@"11"];
[p performSelector:@selector(go:) withObject:@10];
// 打印输出
2016-03-17 19:05:03.917 runtime[12761:543574] 我滚了 11 米远的屎蛋
2016-03-17 19:05:04.617 runtime[12761:543574] 我走了 10 公里才到的家

实现 NSCoding 的自动归档和解档

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍 encodeObjectdecodeObjectForKey 方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

假设现在有一个 Movie 类,有 3 个属性。先看下 .h 文件

1
2
3
4
5
6
7
8
// Movie.h文件
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end

如果是正常写法,.m 文件应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Movie.m文件
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_movieId forKey:@"id"];
[aCoder encodeObject:_movieName forKey:@"name"];
[aCoder encodeObject:_pic_url forKey:@"url"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
self.movieId = [aDecoder decodeObjectForKey:@"id"];
self.movieName = [aDecoder decodeObjectForKey:@"name"];
self.pic_url = [aDecoder decodeObjectForKey:@"url"];
}
return self;
}
@end

如果这里有 100 个属性,那么我们也只能把 100 个属性都给写一遍吗。
不过你会使用 runtime 后,这里就有更简便的方法,如下。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。
下面看看更加简便的方法:两句代码搞定。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#import "Movie.h"
#import <objc/runtime.h>
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\
#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\
- - -
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder {
encodeRuntime(Movie)
}
- (id)initWithCoder:(NSCoder *)decoder {
initCoderRuntime(Movie)
}
@end

优化
上面是 encodeWithCoderinitWithCoder 这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

runtime 下 Class 的各项操作

1.runtime 部分函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN
以下的这些方法应该算是`runtime`在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。
0、class_copyPropertyList 获取类中所有的属性
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
0、class_copyMethodList 获取类的所有方法
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
0、class_copyIvarList 获取类中所有的成员变量(outCount 会返回成员变量的总数)
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
0、class_copyProtocolList 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
0、object_getClass 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
0、class_getInstanceMethod 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
0、class_addMethod 动态添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
0、class_replaceMethod 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
0、method_exchangeImplementations 交换两个方法的实现
method_exchangeImplementations(method1, method2);
0、根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义
Ivar oneCVIvar = class_getClassVariable([Person class], name);
0、根据名字得到实例变量的Ivar指针
Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
0、找到后可以直接对私有成员变量赋值(强制修改name属性)
object_setIvar(_per, oneIVIvar, @"age");
0、动态添加方法
class_addMethod([person class]:Class cls 类型, @selector(eat):待调用的方法名称, (IMP)myAddingFunction:(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction, 0:0代表没有参数);
0、获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name)
0、获得成员变量的名字
const char *ivar_getName(Ivar v);
0、将某个值跟某个对象关联起来,将某个值存储到某个对象中
void objc_setAssociatedObject(id object:表示关联者,是一个对象,变量名理所当然也是object , const void *key:获取被关联者的索引key ,id value :被关联者 ,objc_AssociationPolicy policy:关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC)
0、利用参数key 将对象object中存储的对应值取出来
id objc_getAssociatedObject(id object , const void *key)
*/

一道面试题的注解


下面的代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
@implementation Son : NSObject
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

先思考一下,会打印出来什么❓


答案:都输出 Son

  • class 获取当前方法的调用者的类,superClass 获取当前方法的调用者的父类,super 仅仅是一个编译指示器,就是给编译器看的,不是一个指针。
  • 本质:只要编译器看到 super 这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用

这个题目主要是考察关于 objc 中对 selfsuper 的理解:

  • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
  • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
  • 而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法
  • 调用 [self class] 时,会转化成 objc_msgSend 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_msgSend(id self, SEL op, ...)
- 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数.
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
objc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self);
}

Runtime & Runloop 常面问题整理

Runtime
01 /objc 在向一个对象发送消息时,发生了什么?
参考 1:根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass , 寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL , 根据 id 和 SEL 确认 IMP(指针函数), 在发送消息;
02 / 问题:什么时候会报 unrecognized selector 错误?iOS 有哪些机制来避免走到这一步?
参考 1:当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的 SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。
03 / 问题:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
参考 1:1、不能向编译后得到的类增加实例变量 2、能向运行时创建的类中添加实例变量。
分析:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,runtime 会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
04 / 问题:runtime 如何实现 weak 变量的自动置 nil?
参考 1:runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak 指向的对象内存地址是 a,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。
05 / 问题:给类添加一个属性后,在类结构体里哪些元素会发生变化?
参考 1:instance_size :实例的内存大小;objc_ivar_list *ivars: 属性列表
RunLoop
01 / 问题:runloop 是来做什么的?runloop 和线程有什么关系?主线程默认开启了 runloop 么?子线程呢?
参考 1:runloop: 从字面意思看:运行循环、跑圈,其实它内部就是 do-while 循环,在这个循环内部不断地处理各种任务(比如 Source、Timer、Observer)事件。runloop 和线程的关系:一个线程对应一个 RunLoop,主线程的 RunLoop 默认创建并启动,子线程的 RunLoop 需手动创建且手动启动(调用 run 方法)。RunLoop 只能选择一个 Mode 启动,如果当前 Mode 中没有任何 Source (Sources0、Sources1)、Timer,那么就直接退出 RunLoop。
02 / 问题:runloop 的 mode 是用来做什么的?有几种 mode?
参考 1:model: 是 runloop 里面的运行模式,不同的模式下的 runloop 处理的事件和消息有一定的差别。系统默认注册了 5 个 Mode:(1)kCFRunLoopDefaultMode: App 的默认 Mode,通常主线程是在这个 Mode 下运行的。(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意 iOS 对以上 5 中 model 进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
03 / 问题:为什么把 NSTimer 对象以 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动 scrollview 的时候 NSTimer 却不动了?
参考 1:nstime 对象是在 NSDefaultRunLoopMode 下面调用消息的,但是当我们滑动 scrollview 的时候,NSDefaultRunLoopMode 模式就自动切换到 UITrackingRunLoopMode 模式下面,却不可以继续响应 nstime 发送的消息。所以如果想在滑动 scrollview 的情况下面还调用 nstime 的消息,我们可以把 nsrunloop 的模式更改为 NSRunLoopCommonModes.
04 / 问题:苹果是如何实现 Autorelease Pool 的?
参考 1:Autorelease Pool 作用:缓存池,可以避免我们经常写 relase 的一种方式。其实就是延迟 release,将创建的对象,添加到最近的 autoreleasePool 中,等到 autoreleasePool 作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease.

References

Title: OC Runtime

Author: Tuski

Published: 08/02/2019 - 13:09:21

Updated: 09/10/2019 - 20:47:55

Link: http://www.perphet.com/2019/08/OC-Runtime/

Protocol: Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) Reprinted please keep the original link and author

Thx F Sup