全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题
本文主要内容
一.Runtime数据结构
二.Runtime类对象与元类对象
三.Runtime方法缓存&消息传递详述
四.Runtime消息转发
五.Runtime Method-Swizzling
六.Runtime动态添加方法
七.Runtime动态方法解析
一.Runtime数据结构
- objc_object
- objc_class
- isa指针
- method_t
1、objc_object
所有的对象都是id类型的,id类型对象对应到Runtime中就是objc_object
结构体。objc_object
包含以下内容:isa_t共用体、关于isa操作相关、弱引用相关、关联对象相关、内存管理相关
。
2、objc_class
OC中使用的Class
对应到Runtime
中就是objc_class
结构体,objc_class
继承自objc_object
,所以Class
也是个对象。objc_class
包含以下内容:Class super Class、cache_t cache、class_data_bits_t bits
。
3、isa指针
- 共用体isa_t,指针型isa的isa的值代表Class的地址,非指针型isa的isa的值的部分代表Class的地址
- isa的指向:关于对象,其指向类对象;关于类对象,其指向元类对象。
4、cache_t
- 用于快速查找方法执行函数
- 是可增量扩展的哈希表结构
- 是局部性原理的最佳应用
局部性原理:一般在调用方法时,往往调用的就那几个方法,这几个方法被调用的频次是最高的,如果把调用频次最高的这几个方法放到缓存中,下次的命中率会更高。
数据结构说明: key对应OC语言中的SEL。
5、class_data_bits_t
- class_data_bits_t主要是对class_rw_t的封装
- class_rw_t代表了类相关的读写信息、对class_ro_t的封装
- class_ro_t代表了类相关的只读信息
class_rw_t:
class_ro_t:
class_ro_t:
- 函数四要素:名称、返回值、参数、函数体
Type Encodings
- const char* types;
Runtime总体结构:
二.Runtime类对象与元类对象
- 类对象存储实例方法列表等信息
- 元类对象存储类方法列表等信息
类对象和元类对象的区别和联系
- 实例对象能通过isa指针找到它的类对象,类对象中存储实例方法列表等信息;
- 类对象能通过isa指针找到它的元类对象,元类对象中存储类方法列表等信息;
- 类对象和元类对象都是objc_class数据结构,由于objc_class继承自objc_object,所以才有isa指针,才能实现实例可以通过isa指针找到类对象,进而可以访问实例方法列表等信息,类对象可以通过isa指针找到元类对象,进而可以访问类方法列表等信息。
消息传递简述
//
void objc_msgSend(void /* id self, SEL op,... */)
void objc_msgSendSuper(void /* struct objc_super *super, SEL op,... */)
struct objc_super {
// Specifies an instance of a class
// 接收者实际就是当前对象(self)
_unsafe_unretained id receiver;
}
注意⚠️:无论是[self class]还是[super class],当前这条消息的接收者都是
当前对象
self。
消息传递流程
问题1:如下输出结果是什么?
#import "Mobile.h"
@interface Phone: Mobile
@end
@implementation Phone
- (id)init {
self = [super init];
if (self) {
// 结果: Phone Phone
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([self class]));
}
return self;
}
答:在消息传递过程中,[self class]的消息接收者是当前对象,即Phone。Phone的实例在初始化后,通过[self class]打印类信息或类名称,通过实例对象(instance of Subclass)的isa指针找到Phone的类对象(Subclass(class)),类对象中没有class方法,再通过superClass找到父类Mobile对象(Superclass(class)),仍然没有class方法,继续查找直到根类对象中实现class。通过[super class]打印类信息或类名称,实际的接收者也还是当前对象Phone,而[super class]的含义是跨越了类对象的查找直接到父类(Superclass(class))开始查找,直到查找到根类对象NSObject。
三.Runtime方法缓存&消息传递详述
第一步、缓存查找
实际上就是根据给定的方法选择器SEL,查找对应bucket_t中的IMP,即方法实现
。涉及到Hash哈希查找,首先根据给定的方法选择器,通过Hash哈希函数映射出bucket_t在数据中的位置。
第二步、当前类中查找
- 对于
已排序好
的列表,采用二分查找
算法查找方法对应执行函数 - 对于没有排序的列表,采用
一般遍历
查找方法对应执行函数
第三步、父类逐级查找
消息传递总结
消息传递过程中首先依次看缓存是否命中
、当前类方法列表是否命中
、逐级父类方法列表是否命中
,然后缓存是否命中
是通过哈希查找
方式查找;当前类方法列表是否命中
涉及对于已排序好
的列表,采用二分查找
算法查找方法对应执行函数,对于没有排序
的列表,采用一般遍历
查找方法对应执行函数;逐级父类方法列表是否命中
根据superclass指针逐级查找。
四.Runtime消息转发
对于实例方法的消息转发流程:
- 1.首先系统会回调
resolvelnstanceMethod:
类方法(类方法消息转发回调resolveClassMethod:
方法),参数为SEL
类型的方法选择器,返回类型为BOOL
类型,相当于告诉系统:我们要不要解决当前实例方法的实现,如果这一步返回YES,或者给予此方法选择器所对应方法实现的实现时,相当于通知系统当前消息已处理,会结束此消息转发流程; - 2.如果这一步返回NO,会回调
forwardingTargetForSelector:
实例方法,参数为SEL
类型的方法选择器,返回类型为id
类型,相当于告诉系统:这个选择器或这此实例方法的调用应该由哪个对象进行处理。转发对象是谁。如果指定了转发目标,系统会把这条消息转发给返回的转发目标,结束本次消息转发流程; - 3.如果没有返回任何转发目标,即返回nil。此时会调用
methodSignatureForSe lector:
实例方法,参数为SEL
类型的方法选择器,方法的返回值类型为NS MethodSignature
对象,该对象是对方法选择器返回值的类型和它的参数个数、参数类型的封装,如果该方法返回方法签名,系统会接着调用forwardInvocation:
方法,如果该方法能处理消息,则结束本次消息转发流程; - 4.如果
methodSignatureForSelector:
方法返回为nil或者forwardInvocat ion:
无法处理消息则被标记为”消息无法处理”,即“unrecognized selector sent to instance”。
消息转发验证代码
RuntimeObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RuntimeObject : NSObject
- (void)test;
@end
RuntimeObject.m
#import "RuntimeObject.h"
@implementation RuntimeObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果是test方法,打印日志
if (sel == @selector(test)) {
NSLog(@"resolveInstanceMethod:");
return NO;
} else {
// 返回父类的默认调用
return [super resolveClassMethod:sel];
}
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector:");
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
NSLog(@"methodSignatureForSelector:");
// v代表返回值是void类型的
// @代表第一个参数类型是id,即self
// :代表第二个参数是SEL类型的,即@selector(test)
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation:");
}
@end
AppDelegate.h中调用
#import "RuntimeObject.h"
RuntimeObject *obj = [[RuntimeObject alloc] init];
// 调用test方法,只有声明,没有实现
[obj test];
结果:
2022-08-10 20:17:21.320828+0800 01-Runtime消息转发[66889:15765818] resolveInstanceMethod:
2022-08-10 20:17:21.320945+0800 01-Runtime消息转发[66889:15765818] forwardingTargetForSelector:
2022-08-10 20:17:21.321040+0800 01-Runtime消息转发[66889:15765818] methodSignatureForSelector:
2022-08-10 20:17:21.321153+0800 01-Runtime消息转发[66889:15765818] forwardInvocation:
五.Runtime Method-Swizzling`
1、objc_object
什么是Method-Swizzling?
方法交换验证代码
RuntimeObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RuntimeObject : NSObject
- (void)test;
- (void)otherTest;
@end
RuntimeObject.m
#import "RuntimeObject.h"
@implementation RuntimeObject
+ (void)load {
// 获取test方法实现结构
Method test = class_getInstanceMethod(self, @selector(test));
// 获取otherTest方法实现结构
Method otherTest = class_getInstanceMethod(self, @selector(otherTest));
// 交换两个方法的实现
method_exchangeImplementations(test, otherTest);
}
- (void)test {
NSLog(@"test");
}
- (void)otherTest {
// 在调用此方法前,已经将两个方法的实现进行了交换
// 实际上是调用test的具体实现
[self otherTest];
NSLog(@"otherTest");
}
@end
结果:
2022-08-10 20:18:03.814878+0800 01-Runtime消息转发[67020:15767056] test
2022-08-10 20:18:03.814996+0800 01-Runtime消息转发[67020:15767056] otherTest
六.Runtime动态添加方法`
动态添加方法验证代码
RuntimeObject.m
void testImp(void) {
NSLog(@"test invoke");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果是test方法,打印日志
if (sel == @selector(test)) {
NSLog(@"resolveInstanceMethod:");
// 动态添加test方法的实现
class_addMethod(self, @selector(test), testImp, "v@:");
return YES;
} else {
// 返回父类的默认调用
return [super resolveClassMethod:sel];
}
}
结果:
2022-08-10 20:32:33.993726+0800 01-Runtime消息转发[69545:15784461] resolveInstanceMethod:
2022-08-10 20:32:34.034447+0800 01-Runtime消息转发[69545:15784461] test invoke
问题:是否使用过performSelector:系统方法?
实际上是考察Runtime动态添加方法的特性!
Xcode内部全局搜索快捷键:shift+command+o
七.Runtime动态方法解析`
问题:是否使用过@dynamic编译器关键字?
声明的属性在实现当中标识为@dynamic时,相当于getter和setter方法在运行时添加,而非在编译时声明好。
- 动态运行时语言将函数决议推迟到运行时
- 编译时语言在编译期进行函数决议
本文总结
问题1:[obj foo]和objc_msgSend()函数之间有什么关系?
[obj foo] 《===》 objc_msqSend(self, @selector(foo))
问题2:runtime如何通过Selector找到对应的IMP地址的?
缓存是否命中
、当前类方法列表是否命中
、逐级父类方法列表是否命中
问题3:能否向编译后的类中增加实例变量?
编译前的类已经完成实例变量的布局,runtime数据结构中的class_ro_t,编译后无法修改!但可以在动态添加的类中增加实例变量
。