RunTime 简介
-
什么是 RunTime?
我们都知道,将源代码转换为可执行的程序,通常要经过 5 个步骤:预处理、编译、汇编、链接、运行。不同的编译型语言,在这 5 个步骤中所进行的操作又有些不同
C 作为一门静态语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现
Objective-C 作为一门动态语言,在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的是哪个方法。只有在运行期间才检查变量的数据类型,同时在运行期间才会根据方法名查找要调用的具体方法。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么
Objective-C 把一些决定性的工作从(编译阶段、链接阶段)推迟到(运行时阶段)的机制,使得 Objective-C 变得更加的灵活。我们甚至可以在程序运行的时候,动态地去修改一个方法的实现,这也为大为流行的『热更新』提供了可能性
实现 Objective-C 运行时机制的一切基础就是 RunTime。RunTime 实际上是一个动态库,这个动态库使我们可以在程序运行时动态地创建对象、检查对象,修改类和对象的方法
实例对象 && 类对象 && 元类对象
-
Object(实例对象)
Object(实例对象)被定义为指向
struct objc_object
的指针,其数据结构如下:// A pointer to an instance of a class.(指向一个类的一个实例的指针) // Objective-C 中的 id 类型,被定义为一个指向 struct objc_object 的指针,即被定义为一个指向实例对象的指针 typedef struct objc_object *id; // Represents an instance of a class.(表示一个类的一个实例) struct objc_object { Class _Nonnull isa; // 指向该实例对象所属的类 // 编译时,根据开发者所定义的类的不同,编译器会以 struct objc_object 为模板,生成不同的 struct XXX_IMPL,并在 struct XXX_IMPL 中添加该类的实例对象所特有的成员变量 // 成员变量 0 // 成员变量 1 // 成员变量 2 // ... };
当对一个实例对象进行成员变量访问时,比如
receiver->_ variable = 10;
,RunTime 会根据成员变量在实例对象(struct objc_object
)中的偏移量,找到相应的成员变量,然后进行访问当对一个实例对象进行方法调用时,比如
[receiver selector];
,RunTime 会通过实例对象(struct objc_object
)的isa
指针找到对应的类对象(struct objc_class
),然后在类对象(struct objc_class
)的methodLists
(方法列表数组)中查找相应方法的地址,然后跳转执行 -
Class(类对象)
Class(类对象)被定义为指向
struct objc_class
的指针,其数据结构如下:// An opaque type that represents an Objective-C class.(一个不透明的类型,用于表示 Objective-C 中的一个类) typedef struct objc_class *Class; struct objc_class { Class _Nonnull isa; // is-a 指针,对象的 is-a 指针指向类对象,类对象的 is-a 指针指向元类对象,元类对象的 is-a 指针指向根元类对象 #if !__OBJC2__ Class _Nullable super_class; // 父类 const char * _Nonnull name; // 类名 long version; // 类的版本信息,默认为 0 long info; // 类的信息,供运行时使用的一些标识位,如 CLS_CLASS(0x1L) 表示类对象,其中包含对象方法和成员变量。CLS_META(0x2L) 表示元类对象,其中包含类方法 long instance_size; // 该类的实例变量大小 struct objc_ivar_list * _Nullable ivars; // 该类的成员变量链表(一维) struct objc_method_list * _Nullable * _Nullable methodLists; // 该类的方法列表链表(二维) struct objc_cache * _Nonnull cache; // 该类的方法缓存 struct objc_protocol_list * _Nullable protocols; // 该类遵守的协议链表 #endif };
struct objc_class
中存储的是(用于描述类简要信息)与(用于描述实例对象详细信息)的变量,即struct objc_class
存放的都是元数据(meta-data)struct objc_class
的第一个成员变量是isa
指针,在 Objective-C 体系结构中,isa
指针用于表示一个对象所属的类。换句话说,在 Objective-C 中类的本质也是一个对象,称之为类对象在 Objective-C 中成员变量与属性都是依附在实例对象上的,并且在 Objective-C 中不存在所谓的 类成员变量 与 类属性
当对一个类对象进行方法调用时,比如
[Receiver selector];
,RunTime 会通过类对象(struct objc_class
)的isa
指针找到对应的元类对象(struct objc_class
),然后在元类对象(struct objc_class
)的methodLists
(方法列表数组)中找到相应方法的地址,然后跳转执行 -
Meta-Class(元类对象)
在 Objective-C 中:
一个 实例对象 所属的类叫做 类对象,用于描述实例对象本身所具有的特征
一个 类对象 所属的类叫做 元类对象,用于描述类对象本身所具有的特征在 Objective-C 中,
isa
指针用于表示一个对象所属的类:
实例对象(struct objc_object
)的isa
指针指向类对象(struct objc_class
)
类对象(struct objc_class
)的isa
指针指向元类对象(struct objc_class
)
元类对象(struct objc_class
)的isa
指针指向根元类对象(struct objc_class
)
因此,元类对象也用struct objc_class
表示。与类对象不同的是,元类对象的struct objc_class
结构体中存储的是(用于描述类对象详细信息)的变量对象方法的调用过程为:通过实例对象的
isa
指针找到其所属的类对象,在类对象的方法列表数组中寻找对应的 selector,然后跳转执行
类方法的调用过程为:通过类对象的 isa 指针找到其所属的元类对象,在元类对象的方法列表数组中寻找对应的 selector,然后跳转执行举个例子:
// stringWithFormat: 消息被发送给了 NSString 的类对象,NSString 的类对象通过自己的 isa 指针找到 NSString 的元类对象 // 在 NSString 的元类对象的方法列表中找到对应的 stringWithFormat: 方法,然后执行该方法 NSString* str = [NSString stringWithFormat:@"%@ = %d", @"result", 3];
-
实例对象、类对象、元类对象 之间的关系
关于isa
指针:- 实例对象的
isa
指针指向类对象,类对象的isa
指针指向元类对象 - 所有元类对象的
isa
指针都直接指向NSObject
的元类对象(因此,NSObject
的元类对象也被称为根元类) - 根元类的
isa
指针指向自己
关于
superclass
指针:- 实例对象没有
superclass
指针 - 类对象的
superclass
指针指向(父类的类对象),(父类的类对象)的superclass
指针指向(根类的类对象),(根类的类对象)的superclass
指针指向nil
- 元类对象的
superclass
指针指向(父类的元类对象),(父类的元类对象)的superclass
指针指向(根类的元类对象) - (根类的元类对象)的
superclass
指针指向(根类的类对象),(根类的类对象)的superclass
指针最终指向nil
注意:
- 同一个类的所有实例对象,有且仅有一个与之对应的类对象
- 每个类对象有且仅有一个与之对应的元类对象
- 每一个类对象有且仅有一个父类对象
- 每一个元类对象有且仅有一个父元类对象
- 实例对象的
-
实例对象的内存布局
Person
类继承自NSObject
,Man
类继承自Person
类,Person
类和Man
类各自有成员变量和属性,如下所示:
使用 XCode 命令行工具,将 Man.m 转换成 Man.cpp:// 如果转换失败,请检查:XCode - Preferences... - Locations - Command Line Tool 选项是否正确地选择了当前的 XCode 版本 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Man.m -o Man.cpp
在 Man.cpp 中得到表示 NSObject 实例对象、Person 实例对象、Man 实例对象 的结构体,如下所示:
// RunTime 中表示 NSObject 实例对象的结构体 struct NSObject_IMPL { Class isa; }; // RunTime 中表示 Person 实例对象的结构体 struct Person_IMPL { struct NSObject_IMPL NSObject_IVARS; int personA; NSString *personB; }; // RunTime 中表示 Man 实例对象的结构体 struct Man_IMPL { struct Person_IMPL Person_IVARS; int manA; NSString *manB; int _manC; NSString * _Nonnull _manD; }; // Man 实例对象的内存布局等价于 struct Man_IMPL { Class isa; // 继承自 NSObject 类的成员变量 int personA; // 继承自 Person 类的成员变量 NSString *personB; // 继承自 Person 类的成员变量 int manA; // Man 类自身的成员变量 NSString *manB; // Man 类自身的成员变量 int _manC; // Man 类自身的成员变量 NSString * _Nonnull _manD; // Man 类自身的成员变量 }; // 注意: // 1.在 Objective-C 中,实例对象的本质是一个结构体 // 2.实例对象结构体的第一个元素恒定为 isa 指针,指向其对应的类对象 // 3.实例对象结构体中包含继承自父类的成员变量(@public、@protected) // 4.实例对象结构体中包含其自身所有的成员变量(@public、@protected、@private、@package)
成员变量 && 属性 && 方法 && 协议
-
Ivar(成员变量)
Ivar(成员变量)被定义为指向
struct objc_ivar
的指针,其数据结构如下:// An opaque type that represents an instance variable.(一个不透明的类型,用于表示一个成员变量) typedef struct objc_ivar *Ivar; // 用于描述实例对象的单个成员变量 struct objc_ivar { char * _Nullable ivar_name; // 成员变量名称 char * _Nullable ivar_type; // 成员变量类型(并不是真实的成员变量类型,而是经过类型编码的 C 字符串) int ivar_offset; // 成员变量相对于基地址的偏移量(Byte,基地址指的是实例对象结构体的首地址) #ifdef __LP64__ int space; #endif }; // 用于描述实例对象所具有的成员变量列表 struct objc_ivar_list { int ivar_count; // 成员变量的总个数 #ifdef __LP64__ int space; #endif /* variable length structure(可变长度的结构体数组) */ struct objc_ivar ivar_list[1]; }
-
Property(属性)
Property(属性)被定义为指向
struct objc_property
的指针,其数据结构如下:// An opaque type that represents an Objective-C declared property.(一个不透明的类型,用于表示一个属性) typedef struct objc_property *objc_property_t; // 用于描述实例对象的单个属性(RunTime 源码中未找到,根据 -rewrite-objc 后的 c++ 文件所推测) struct objc_property { char * _Nullable property_name; // 属性名称 char * _Nullable property_attributes; // 属性特性 }; // 用于描述实例对象所具有的属性列表(RunTime 源码中未找到,根据 -rewrite-objc 后的 c++ 文件所推测) struct property_list_t { int entsize; // 单个属性的大小,sizeof(struct objc_property) int property_count; // 属性的总个数 /* variable length structure(可变长度的结构体数组) */ struct objc_property property_list[1]; }
其中,
objc_property.property_attributes
是一串用于表示属性特性的字符串编码// 特性编码总以 T属性类型编码 开头,总以 V属性所对应的成员变量 结尾,中间是属性修饰符编码,所有的编码之间以逗号隔开(T-Type, V-Value) // 举例如下: // 1.属性 name 所对应的特性编码为 T@"NSString",&,N,V_name @property (nonatomic, strong) NSString* name; // 2.属性 age 所对应的特性编码为 Ti,V_age @property (atomic, assign) int age; // 3.属性 survival 所对应的特性编码为 TB,R,N,GisSurvival,V_survival @property (nonatomic, assign ,getter=isSurvival, readonly) bool survival; // 用于描述属性的单个特性 typedef struct { const char * _Nonnull name; // 特性名称 const char * _Nonnull value; // 特性值(通常为空) } objc_property_attribute_t;
属性修饰符编码: ① 原子性 atomic 默认值,空 nonatomic N ② 读写权限 readwrite 默认值,空 readonly R getter G方法名 setter S方法名(带冒号) ③ 内存管理 assign 默认值,空 weak W(弱引用) strong &(强引用) copy C
属性类型编码: c 代表 char 类型 i 代表 int 类型 s 代表 short 类型 l 代表 long 类型(在 64 位的程序上也被视为一个 32 位的数据) q 代表 long long 类型 C 代表 unsigned char 类型 I 代表 unsigned int 类型 S 代表 unsigned short 类型 L 代表 unsigned long 类型 Q 代表 unsigned long long 类型 f 代表 float 类型 d 代表 double 类型 B 代表 C++ 中的 bool 或者 C99 中的 _Bool v 代表 void 类型 . * 代表 char * 类型 @ 代表实例对象 # 代表类对象(Class) : 代表方法 selector(SEL) [array type] 代表 array {name=type…} 代表结构体 (name=type…) 代表 union bnum 代表 num 个的 bit 位 ^type 代表指向 type 类型的指针 ? 代表一个未知的类型 (此外,此编码还用于表示函数指针) 关于类型编码更详细的介绍,请参考苹果开发者文档(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html) NSLog(@"%s", @encode(int)); // 输出 i NSLog(@"%s", @encode(unsigned int)); // 输出 I NSLog(@"%s", @encode(NSInteger)); // 输出 q NSLog(@"%s", @encode(NSString)); // 输出 {NSString=#}
-
Method(方法)
Method(方法)被定义为指向
struct objc_method
的指针,其数据结构如下:// An opaque type that represents a method in a class definition.(一个不透明的类型,用于表示 实例对象或者类对象 中的方法) typedef struct objc_method *Method; // 用于描述 实例对象或者类对象 的单个方法 struct objc_method { SEL _Nonnull method_name; // 方法名称 char * _Nullable method_types; // 方法类型(类型编码,用于存储方法的返回值类型和参数类型) IMP _Nonnull method_imp; // 方法实现(本质上是一个函数指针) }; // 用于描述 实例对象或者类对象 的方法列表 struct objc_method_list { struct objc_method_list * _Nullable obsolete; // 指向下一个方法链表(已废弃) int method_count; // 本方法列表中方法的总个数 #ifdef __LP64__ int space; #endif /* variable length structure(可变长度的结构体数组) */ struct objc_method method_list[1]; };
① SEL 是
selector
在 Objective-C 中的表示类型(Swift 中是Selector
类)
selector
是方法选择器(也叫:方法编号、方法名称),可以理解为用于区分方法的 ID,其数据结构如下:// An opaque type that represents a method selector.(一个不透明的类型,用于表示一个方法选择器) typedef struct objc_selector *SEL; // SEL 是一个指向 struct objc_selector 的指针,其本质是一个 int 类型的地址,地址中存放着方法的名字 // A method selector is a C string that has been registered (or "mapped") with the Objective-C runtime. (方法选择器是一个已在 Objecte-C 运行时中注册(或"映射")的C字符串) // Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.(由编译器生成的方法选择器在加载类时由运行时自动映射) selector 本质上是个映射到方法的 C 字符串(用于保存方法名称的 C 字符串) 可以用 Objective-C 编译器命令 @selector() 或者 RunTime 的 sel_registerName() 函数来手动将方法名映射到运行时以获得一个 SEL 类型的方法选择器
在 Objective-C 中关于方法的名称有 2 条规则:
- 同一个类,
selector
不能重复 - 不同的类,
selector
可以重复
在 C 中可以通过不同的参数类型对函数进行重载(即函数名称和参数个数相同,但参数类型不同)
在 Objective-C 中,因为selector
只存储了方法的名称,并没有存储方法的参数类型,所以没有办法通过selector
区分具有相同方法名称、不同参数类型的方法。这也是 Objective-C 不支持方法重载的最根本的原因// 在 C 中,可以通过不同的参数类型对方法进行重载 int caculate(int num1, int num2); float caculate(float num1, float num2); // 在 Objective-C 中,无法通过不同的参数类型对方法进行重载(以下的两个方法会报重复定义的错误) -(int)caculateNum1:(int)num1 num2:(int)num2; -(float)caculateNum1:(float)num1 num2:(float)num2; // 如果要避免重复定义,只能通过不同的方法名进行区分 -(int)caculateIntNum1:(int)num1 num2:(int)num2; -(float)caculateFloatNum1:(float)num1 num2:(float)num2;
还需要注意一点:
在不同的类中相同名字的方法所对应的selector
是相同的,即使这些方法,名字相同而变量类型不同,也会导致它们具有相同的selector
② 方法类型 用于存储方法的返回值类型和参数类型,请参考属性的类型编码
// 注意:类型编码后面的数字表示的是该类型的起始位置(字节) // v16@0:8 等价于 v@: -(void)test; // i24@0:8i16i20 等价于 i@:ii -(int)caculateIntNum1:(int)num1 num2:(int)num2; // f24@0:8f16f20 等价于 f@:ff -(float)caculateFloat:(float)num1 num2:(float)num2; // @36@0:8@16i24f28B32 等价于 @@:@ifB -(NSString *)introduceName:(NSString *)name age:(int)age height:(float)height sex:(bool)sex;
③ IMP 本质上是一个函数指针,指向方法在内存中的代码实现,其数据结构如下:
// A pointer to the function of a method implementation. (一个指向方法实现的函数指针) // 注意:IMP 指向的函数 与 objc_msgSend 函数,类型相同。参数都包含:id、SEL、可变参数列表 #if !OBJC_OLD_DISPATCH_PROTOTYPES typedef void (*IMP)(void /* id, SEL, ... */ ); #else typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); #endif
- 同一个类,
-
Cache(方法缓存)
Cache(方法缓存、类缓存)被定义为指向
struct objc_cache
的指针,其数据结构如下:typedef struct objc_cache *Cache; struct objc_cache { unsigned int mask; // 当前能到达的最大 index(从 0 开始计算),缓存的长度 total = mask + 1 unsigned int occupied; // 被占用的槽位数量,因为缓存是以散列表的形式存在的,所以会有空槽 Method _Nullable buckets[1]; // 用数组表示的 hash 表,每一个 Method 代表一个方法缓存 };
如果每次调用对象的方法时,都按照对象的继承链依次遍历方法列表,会导致方法查找效率低下。基于局部性原理:当对象的某个方法被调用时,该方法在以后很可能会再次被调用。为了加快方法调用的速度,RunTime 会将调用过的方法存放在
Cache
中进行缓存,并且每当调用对象的方法时,都会优先从Cache
中进行查找 -
Protocol(协议)
Protocol(协议)被定义为
struct protocol_t
,其数据结构如下:// 用于表示 Objective-C 中的一个协议(继承自 struct objc_object) struct protocol_t : objc_object { const char *mangledName; // 协议名称(C++ 命名重整技术 name mangling) struct protocol_list_t *protocols; // 本协议所遵守的协议列表 method_list_t *instanceMethods; // 实例方法列表(必须实现) method_list_t *classMethods; // 类方法列表(必须实现) method_list_t *optionalInstanceMethods; // 实例方法列表(可选实现) method_list_t *optionalClassMethods; // 类方法列表(可选实现) property_list_t *instanceProperties; // 实例属性 uint32_t size; // sizeof(struct protocol_t) uint32_t flags; // 标志位 }; // 协议列表 struct objc_protocol_list { struct objc_protocol_list * _Nullable next; // 指向下一个协议链表 long count; // 本协议列表中协议的总个数 __unsafe_unretained Protocol * _Nullable list[1]; // 可变长度的结构体数组(协议列表) };
RunTime 消息机制
Objective-C 中,方法的调用都是类似 [receiver selector];
的形式,其本质是让对象在运行时动态地发送消息。我们来看看 receiver
对象调用 selector
方法时,在『编译阶段』和『运行阶段』分别会发生什么:
-
编译阶段
[receiver selector];
方法调用被编译器转换为:objc_msgSend(receiver, selector);
(不带参数)objc_msgSend(recevier, selector, org1, org2, …);
(带参数)
-
运行时:消息发送阶段
消息接受者
recevier
寻找对应的selector
:- 通过
recevier
的isa
指针找到recevier
对应的Class
(类) - 在
Class
(类)的Cache
(方法缓存)的散列表中寻找对应的IMP
(方法实现) - 如果在
Cache
(方法缓存)中没有找到对应的IMP
(方法实现),则继续在Class
(类)的Method List
(方法列表)中找寻找对应的selector
。如果找到,则填充到Cache
(方法缓存)中,并返回selector
- 如果在
Class
(类)中没有找到这个selector
,则继续在recevier
的superclass
(父类)中寻找 - 一旦找到对应的
selector
,则直接执行selector
对应的IMP
(方法实现) - 如果找不到对应的
selector
,则 RunTime 系统进入消息转发阶段
- 通过
-
运行时:消息转发阶段
① 动态方法解析:通过重写当前recevier
的+resolveInstanceMethod:
(对象方法)或者+resolveClassMethod:
(类方法),利用 RunTime 的class_addMethod
函数给当前recevier
动态地添加其他方法实现② 消息接受者重定向:如果上一步中没有添加其他方法实现,则可重写当前
recevier
的-forwardingTargetForSelector:
(对象方法)或者+forwardingTargetForSelector:
(类方法),将消息动态地转发给其他对象处理③ 完整的消息重定向:如果上一步的返回值为
nil
或者self
,则可重写当前recevier
的-methodSignatureForSelector:
(对象方法)或者+methodSignatureForSelector:
(类方法),获取消息的参数和返回值类型- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(方法签名),则 RunTime 系统就会创建一个NSInvocation
对象,并调用当前recevier
的-forwardInvocation:
(对象方法)或者+forwardInvocation:
(类方法),给予此次消息发送最后一次寻找IMP
(方法实现)的机会 - 如果
recevier
的methodSignatureForSelector:
返回 nil,则 RunTime 系统发出-doesNotRecognizeSelector:
(对象方法)或者+doesNotRecognizeSelector:
(类方法)消息,程序也就崩溃了
- 如果
-
① 代码示例:动态方法解析
RunTime 会调用当前对象(
recevier
)的+resolveInstanceMethod:
或者+resolveClassMethod:
,让程序有机会为未找到的方法提供一个函数实现。前者在(对象方法未找到时)调用,后者在(类方法未找到时)调用。可以通过重写当前对象(recevier
)的这两个方法,添加其他函数实现,并返回 YES,那么 RunTime 就会重新启动一次消息发送的过程主要用到的方法如下:
// 对象方法未找到时调起,可以在此动态添加方法实现 +(BOOL)resolveInstanceMethod:(SEL)sel; // 类方法未找到时调起,可以在此动态添加方法实现 +(BOOL)resolveClassMethod:(SEL)sel; // 向指定的类中添加指定的方法 // param0.cls 被添加方法的类 // param1.name 方法的 selector // param2.imp 指向方法实现的函数指针 // param3.types 方法的类型 // return. 如果添加方法成功则返回 YES,否则返回 NO BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * _Nullable types);
代码示例:
#import "ViewController.h" #import <objc/runtime.h> @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; // 隐式调用 runMethod 方法 [self performSelector:@selector(runMethod)]; } // 1.重写该方法,添加对象方法的实现 +(BOOL)resolveInstanceMethod:(SEL)sel { // 如果是执行 runMethod 函数,就动态解析,指定新的 IMP if (sel == @selector(runMethod)) { class_addMethod([self class], sel, (IMP)runFunction, "v@:"); } return [super resolveInstanceMethod:sel]; } // runMethod 方法的函数实现 void runFunction(id recevier, SEL selector) { NSLog(@"%s", __func__); } @end
输出结果:
2021-04-14 17:27:20.262071+0800 RunTimeDemo[3519:198578] runFunction
从这个例子中,可以看出:
虽然ViewController.m
没有实现runMethod
方法,但是通过重写它的-resolveInstanceMethod:
,利用class_addMethod
函数动态添加对象方法的实现(runFunction
函数),并执行。从打印结果来看,成功调起了runFunction
函数 -
② 代码示例:消息接受者重定向
如果上一步中
+resolveInstanceMethod:
或者+resolveClassMethod:
没有添加其他函数实现,RunTime 就会进行下一步:消息接受者重定向
注意:无论+resolveInstanceMethod:
或者+resolveClassMethod:
是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,RunTime 都会进行到这一步RunTime 会调用当前对象(
recevier
)的-forwardingTargetForSelector:
或者+forwardingTargetForSelector:
方法,让程序有机会为未找到的方法提供一个备用接受者。前者用于(转发对象方法),后者用于(转发类方法)。可以通过重写当前对象(recevier
)的这两个方法,返回一个备用接受者,那么 RunTime 就会将消息转发给备用接受者处理主要用到的方法如下:
// 重定向对象方法的消息接收者,返回一个类或实例对象 -(id)forwardingTargetForSelector:(SEL)aSelector; // 重定向类方法的消息接收者,返回一个类或实例对象 +(id)forwardingTargetForSelector:(SEL)aSelector;
代码示例:
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject -(void)runMethod; @end // Person.m #import "Person.h" @implementation Person -(void)runMethod { NSLog(@"%s", __func__); } @end
#import "ViewController.h" #import <objc/runtime.h> #import "Person.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; // 隐式调用 runMethod 方法 [self performSelector:@selector(runMethod)]; } +(BOOL)resolveInstanceMethod:(SEL)sel { // 为了进行下一步:消息接收者重定向 return [super resolveInstanceMethod:sel]; } // 2.重写该方法,返回备用消息接收者 -(id)forwardingTargetForSelector:(SEL)aSelector { // 如果是执行 runMethod 方法,则返回 Person 对象作为备用消息接受者 if (aSelector == @selector(runMethod)) { Person* p = [[Person alloc] init]; return p; } return [super forwardingTargetForSelector:aSelector]; } @end
输出结果:
2021-04-14 18:07:47.837306+0800 RunTimeDemo[3685:215156] -[Person runMethod]
从这个例子中,可以看出:
虽然ViewController.m
没有实现runMethod
方法,+resolveInstanceMethod:
也没有添加其他函数实现。但是通过重写-forwardingTargetForSelector:
,返回备用接受者,把本应该ViewController
处理的消息,转发给了Person
对象去执行了。打印结果也证明成功实现了转发通过
-forwardingTargetForSelector:
或者+forwardingTargetForSelector:
可以重定向消息的接收者,该方法的返回值是一个对象,如果这个对象是不是nil
,也不是self
,则 RunTime 会将消息转发给这个对象执行。否则,继续进行下一步:完整的消息重定向 -
③ 代码示例:完整的消息重定向
如果经过动态方法解析、消息接受者重定向,RunTime 还是找不到相应的方法实现而无法响应消息,那么RunTime 就会调用当前对象(
recevier
)的methodSignatureForSelector:
方法,获取消息的参数和返回值类型:-
如果
methodSignatureForSelector:
方法返回了一个NSMethodSignature
对象(方法签名),那么 RunTime 就会创建一个NSInvocation
对象(即把与消息相关的全部细节都封装到NSInvocation
对象中),并调用当前对象(recevier
)的forwardInvocation:
方法,给予此次消息发送最后一次寻找IMP
的机会 -
如果
methodSignatureForSelector:
方法返回nil
,那么 RunTime 就会调用当前对象(recevier
)的doesNotRecognizeSelector:
方法,程序也就崩溃了
所以可以通过重写前对象(
recevier
)的methodSignatureForSelector:
与forwardInvocation:
,对消息进行转发主要用到的方法如下:
// 注意:对象方法和类方法消息转发第三步调用的方法同样不一样 // 对象方法调用的是: // 获取对象方法的参数和返回值类型,返回方法签名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; // 对象方法消息重定向 -(void)forwardInvocation:(NSInvocation *)anInvocation // 报错:未识别的对象方法 -(void)doesNotRecognizeSelector:(SEL)aSelector; // 类方法调用的是: // 获取类方法的参数和返回值类型,返回方法签名 +(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; // 类方法消息重定向 +(void)forwardInvocation:(NSInvocation *)anInvocation; // 报错:未识别的类方法 +(void)doesNotRecognizeSelector:(SEL)aSelector;
代码示例:
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject -(void)runMethod; @end // Person.m #import "Person.h" @implementation Person -(void)runMethod { NSLog(@"%s", __func__); } @end
#import "ViewController.h" #import <objc/runtime.h> #import "Person.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; // 隐式调用 runMethod 方法 [self performSelector:@selector(runMethod)]; } +(BOOL)resolveInstanceMethod:(SEL)sel { // 为了进行下一步:消息接收者重定向 return [super resolveInstanceMethod:sel]; } -(id)forwardingTargetForSelector:(SEL)aSelector { // 为了进行下一步:完整的消息重定向 return [super forwardingTargetForSelector:aSelector]; } // 3.1 重写该方法,获取消息的参数类型和返回值类型并返回方法签名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"runMethod"]) { NSMethodSignature* methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"]; return methodSignature; } return [super methodSignatureForSelector:aSelector]; } // 3.2 重写该方法进行完整的消息重定向 -(void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = anInvocation.selector; Person* p = [[Person alloc] init]; if ([p respondsToSelector:sel]) { [anInvocation invokeWithTarget:p]; return; } /* Dog* d = [[Dog alloc] init]; if ([d respondsToSelector:sel]) { [anInvocation invokeWithTarget:d]; return; } */ // ... [self doesNotRecognizeSelector:sel]; } // 3.3 报错:未识别的对象方法 -(void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"对象方法未找到"); } @end
输出结果:
2021-04-14 20:54:49.963125+0800 RunTimeDemo[3941:244153] -[Person runMethod]
从这个例子中,可以看出:
虽然ViewController.m
没有实现runMethod
方法,+resolveInstanceMethod:
没有添加其他函数实现,-forwardingTargetForSelector:
也没有将消息转发给其他对象。但是 RunTime 通过调用-methodSignatureForSelector:
获取了方法签名,进而创建了一个NSInvocation
对象,并通过调用-forwardInvocation:
把本应该ViewController
处理的消息,转发给了Person
对象去执行了。打印结果也证明成功实现了转发既然
-forwardingTargetForSelector:
和-forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?两者区别就在于
-forwardingTargetForSelector:
只能将消息转发给一个对象。而-forwardInvocation:
可以将消息转发给多个对象。可以将
forwardInvocation:
方法看成是,当前对象的(不能识别的消息的分发中心)。在forwardInvocation:
方法中:-
可以将当前对象不能识别的消息,转发给其他对象
-
可以将当前对象不能识别的消息,翻译成其他消息
-
可以将当前对象不能识别的消息,简单地"吃掉"(这样,不能识别的消息既不会转发给其他对象,也不会报错)
-