一.什么是runtime?
- 话说每次面试都问runtime,你不会runtime能拿到像我66k的工资吗?所以关于runtime,咱还是得好好学一学的.
- OC是一门面向对象的高级语言,内部封装的是C语言,那么是通过什么方式来封装的?
答案就是:runtime.runtime是一套由c、c++、汇编实现的API,它为OC语言加入了面向对象特性和动态运行的功能. - 小拓展
runtime源码地址 --->数字最大的就是最新的版本
runtimeAPI--------->当然你也可以dash这个软件查看API
二.探究runtime
2.1 我们平时写得代码,到底是怎么执行的呢?
- 我们新建一个项目,在main.m里面编写如下代码
@interface YKPerson : NSObject
@end
@implementation YKPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
YKPerson *p = [YKPerson new];
}
return 0;
}
- 使用clang编译器编译main.m
- 注释:clang编译器把OC代码转化为C++语言代码,runtime执行编译后的C++代码.
-
使用方法:在终端运行
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
得到一个main.cpp文件.
- 在main.cpp里面搜索
YKPerson
得到转换后的c++代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
YKPerson *p = ((YKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YKPerson"), sel_registerName("new"));
}
return 0;
}
-
(YKPerson *(*)(id, SEL)
这是一个函数指针,返回值为YKPerson *
,参数为id
和SEL
. - ①
((void *)objc_msgSend)((id)objc_getClass("YKPerson"), sel_registerName("new"))
是一个objc_msgSend
函数.
②去掉(void *)
、去掉(id)
这些强转表示,得到(objc_msgSend)(objc_getClass("YKPerson"), sel_registerName("new"))
这个函数 - 所以上面代码表示的就是: 将
(objc_msgSend)(objc_getClass("YKPerson"), sel_registerName("new"))
这个函数强转为(YKPerson *(*)(id, SEL)
这个函数指针.
答案:可以看到,我们写的OC代码在底层使用(objc_msgSend)
这个函数实现的.
2.2 objc_msgSend是怎样的存在?
- 消息发送机制使用的是汇编,效率很高.
- objc_msgSend_stret : 返回一个结构体
- objc_msgSend_fpret: 返回浮点数
- objc_msgSendSuper: 从父类的方法列表开始查找
2.3 优化
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface YKPerson : NSObject
-(void)eat;
@end
@implementation YKPerson
-(void)eat
{
NSLog(@"%@",@"person_eat");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//1.第一种方法
YKPerson *p = [YKPerson new];
[p eat];
//2.第二种方法,跳过消息转发,速度更快
SEL sel = @selector(eat);
IMP imp = [p methodForSelector:sel];
imp(p, sel);
}
return 0;
}
打印:
2019-01-29 20:58:44.390796+0800 runtimeDemo[13962:1473883] person_eat
2019-01-29 20:58:44.391023+0800 runtimeDemo[13962:1473883] person_eat
三.msgSend的三个阶段
3.1 消息发送阶段
- 判断receiver是否为nil,为nil退出msgSend函数
- 通过isa指针找到类对象,从类对象的cache查找方法实现
- cache没有的话,就从receiver的methodList查找.
- methodList有的话,缓存到receiver的cache并执行代码
methodList没有的话,通过superClass找到父类,去父类的cache中查找 - 父类cache没有的话,就从父类methodList中查找
- 父类methodList中有的话,缓存到receiver的cache并执行代码
父类methodList没有的话,继续通过superClass查找父类的父类 - 一直找到NSObject.如果还没有,就进入动态方法解析阶段
3.2 动态方法解析阶段
- 是否会进入动态方法解析
如果重载了resolver()方法解析
并且没有triedResolver
,才会进入动态方法解析
如果没有重载resolver()方法解析
或者已经triedResolver
一次了,那么进入消息转发阶段 - 已经进入动态方法解析
重载- (BOOL)resolveInstanceMethod()
,给类添加方法.
将triedResolver赋值YES.
不管你返回YES还是NO,源码中都会执行retry()
,重新回到消息发送阶段.
3.3 消息转发阶段
- 调用
forwardingTargetForSelector()
拿到返回值ret. - 如果ret不为nil,调用
msgSend(ret, sel)
如果是为nil,调用methodSignatureForSelector()
拿到方法签名 - 如果方法签名为nil,报
unrecognized selecotr to instance
如果不为nil,调用forwardInvocation()
四.方法缓存
struct bucket_t {
IMP _imp;//方法实现为value
cache_key_t _key;//SEL的地址值为key
}
struct cache_t {
struct bucket_t *_buckets;//数组,里面都是散列表
uint32_t _mask; //mask的值=cache_t的容量 -1 (减1)
uint32_t _occupied; //已经缓存的方法数量
}
#cache_t会自动扩容
#bucket_t是一个hashMap,通过方法名称(key) & mask的值 = hashMap的value(也就是方法的imp).
//oc的类
struct objc_class : objc_object {
// Class ISA;
Class superclass;
#方法缓存
cache_t cache; // formerly cache pointer and vtable
#:bits用来获取具体的类信息,bits&FAST_DATA_MASK就可以拿到struct class_rw_t的信息.
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
- iOS的方法缓存是利用 散列表 实现的.
用空间换时间
- SEL & mask 直接得到对应的bucket索引.
注意: SEL & mask 永远<= mask
- 因为不同的SEL & mask可能得到同样的值,所以如果发现某一个位置已经被占用,apple会将mask-1重新进行&操作,直到找到空位置.
- 如果散列表全部满了,会进行扩容,扩容会清除原来的bucket.
五.其他
struct method_t {
SEL name; //函数名称
const char *types; //函数类型(函数的返回值类型+函数的参数类型)
IMP imp; //函数地址
}