Objective-C 的 RunTime(一):基础知识

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 指针:

    1. 实例对象的 isa 指针指向类对象,类对象的 isa 指针指向元类对象
    2. 所有元类对象的 isa 指针都直接指向 NSObject 的元类对象(因此,NSObject 的元类对象也被称为根元类)
    3. 根元类的 isa 指针指向自己

    关于 superclass 指针:

    1. 实例对象没有 superclass 指针
    2. 类对象的 superclass 指针指向(父类的类对象),(父类的类对象)的 superclass 指针指向(根类的类对象),(根类的类对象)的 superclass 指针指向 nil
    3. 元类对象的 superclass 指针指向(父类的元类对象),(父类的元类对象)的 superclass 指针指向(根类的元类对象)
    4. (根类的元类对象)的 superclass 指针指向(根类的类对象),(根类的类对象)的 superclass 指针最终指向 nil

    注意:

    1. 同一个类的所有实例对象,有且仅有一个与之对应的类对象
    2. 每个类对象有且仅有一个与之对应的元类对象
    3. 每一个类对象有且仅有一个父类对象
    4. 每一个元类对象有且仅有一个父元类对象
  • 实例对象的内存布局

    Person 类继承自 NSObjectMan 类继承自 Person 类,Person 类和 Man 类各自有成员变量和属性,如下所示:
    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];
    };
    

    ① SELselector 在 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 条规则:

    1. 同一个类,selector 不能重复
    2. 不同的类,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]; 方法调用被编译器转换为:

    1. objc_msgSend(receiver, selector);(不带参数)
    2. objc_msgSend(recevier, selector, org1, org2, …);(带参数)
  • 运行时:消息发送阶段

    消息接受者 recevier 寻找对应的 selector

    1. 通过 recevierisa 指针找到 recevier 对应的 Class(类)
    2. Class(类)的 Cache(方法缓存)的散列表中寻找对应的 IMP(方法实现)
    3. 如果在 Cache(方法缓存)中没有找到对应的 IMP(方法实现),则继续在 Class(类)的 Method List(方法列表)中找寻找对应的 selector。如果找到,则填充到 Cache(方法缓存)中,并返回 selector
    4. 如果在 Class(类)中没有找到这个 selector,则继续在 receviersuperclass(父类)中寻找
    5. 一旦找到对应的 selector,则直接执行 selector 对应的 IMP(方法实现)
    6. 如果找不到对应的 selector,则 RunTime 系统进入消息转发阶段
  • 运行时:消息转发阶段
    运行时:消息转发阶段
    ① 动态方法解析:通过重写当前 recevier+resolveInstanceMethod:(对象方法)或者 +resolveClassMethod:(类方法),利用 RunTime 的 class_addMethod 函数给当前 recevier 动态地添加其他方法实现

    ② 消息接受者重定向:如果上一步中没有添加其他方法实现,则可重写当前 recevier-forwardingTargetForSelector:(对象方法)或者 +forwardingTargetForSelector:(类方法),将消息动态地转发给其他对象处理

    ③ 完整的消息重定向:如果上一步的返回值为 nil 或者 self,则可重写当前 recevier-methodSignatureForSelector:(对象方法)或者 +methodSignatureForSelector:(类方法),获取消息的参数和返回值类型

    1. 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(方法签名),则 RunTime 系统就会创建一个 NSInvocation 对象,并调用当前 recevier-forwardInvocation:(对象方法)或者 +forwardInvocation:(类方法),给予此次消息发送最后一次寻找 IMP(方法实现)的机会
    2. 如果 receviermethodSignatureForSelector: 返回 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: 方法中:

    1. 可以将当前对象不能识别的消息,转发给其他对象

    2. 可以将当前对象不能识别的消息,翻译成其他消息

    3. 可以将当前对象不能识别的消息,简单地"吃掉"(这样,不能识别的消息既不会转发给其他对象,也不会报错)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值