<简书 — 刘小壮> https://www.jianshu.com/p/014af0de67cdgit
在OC中方法调用是经过Runtime
实现的,Runtime
进行方法调用本质上是发送消息,经过objc_msgSend()
函数进行消息发送。github
例以下面的OC代码会被转换为Runtime
代码。数组
原方法:[object testMethod]
转换后的调用:objc_msgSend(object, @selector(testMethod));
复制代码
发送消息的第二个参数是一个SEL
类型的参数,在项目里常常会出现,不一样的类定义了相同的方法,这样就会有相同的SEL
。那么问题就来了,也是不少人博客里都问过的一个问题,不一样类的SEL
是同一个吗?缓存
然而,事实是经过咱们的验证,建立两个不一样的类,并定义两个相同的方法,经过@selector()
获取SEL
并打印。咱们发现SEL
都是同一个对象,地址都是相同的。由此证实,不一样类的相同SEL
是同一个对象。多线程
@interface TestObject : NSObject
- (void)testMethod;
@end
@interface TestObject2 : NSObject
- (void)testMethod;
@end
// TestObject2实现文件也同样
@implementation TestObject
- (void)testMethod {
NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end
// 结果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
复制代码
在Runtime
中维护了一个SEL
的表,这个表存储SEL
不按照类来存储,只要相同的SEL
就会被看作一个,并存储到表中。在项目加载时,会将全部方法都加载到这个表中,而动态生成的方法也会被加载到表中。架构
咱们在方法内部能够经过
self
获取到当前对象,可是self
又是从哪来的呢?函数
方法实现的本质也是C函数,C函数除了方法传入的参数外,还会有两个默认参数,这两个参数在经过objc_msgSend()
调用时也会传入。这两个参数在Runtime
中并无声明,而是在编译时自动生成的。源码分析
从objc_msgSend
的声明中能够看出这两个隐藏参数的存在。布局
objc_msgSend(void /* id self, SEL op, ... */ )
复制代码
self
,调用当前方法的对象。_cmd
,当前被调用方法的SEL
。虽然这两个参数在调用和实现方法中都没有明确声明,可是咱们仍然可使用它。响应对象就是self
,被调用方法的selector
是_cmd
。优化
- (void)method {
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
复制代码
一个对象被建立后,自身的类及其父类一直到NSObject
类的部分,都会包含在对象的内存中,例如其父类的实例变量。当经过[super class]
的方式调用其父类的方法时,会建立一个结构体。
struct objc_super { id receiver; Class class; };
复制代码
对super
的调用会被转化为objc_msgSendSuper()
的调用,并在其内部调用objc_msgSend()
函数。有一点须要注意,尽管是经过[super class]
的方式调用的,但传入的receiver
对象仍然是self
,返回结果也是self
的class
。由此可知,当前对象不管调用任何方法,receiver都是当前对象。
objc_msgSend(objc_super->receiver, @selector(class))
复制代码
在objc_msg.s
中,存在多个版本的objc_msgSend
函数。内部实现逻辑大致一致,都是经过汇编实现的,只是根据不一样的状况有不一样的调用。
objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret
复制代码
在上面源码中,带有super
的会在外界传入一个objc_super
的结构体对象。stret
表示返回的是struct
类型,super2
是objc_msgSendSuper()
的一种实现方式,不对外暴露。
struct objc_super {
id receiver;
Class class;
};
复制代码
fp
则表示返回一个long double
的浮点型,而fp2
则返回一个complex long double
的复杂浮点型,其余float
、double
的普通浮点型都用objc_msgSend
。除了上面这些状况外,其余都经过objc_msgSend()
调用。
当一个对象被建立时,系统会为其分配内存,并完成默认的初始化工做,例如对实例变量进行初始化。对象第一个变量是指向其类对象的指针-isa
,isa
指针能够访问其类对象,而且经过其类对象拥有访问其全部继承者链中的类。
isa
指针不是语言的一部分,主要为Runtime
机制提供服务。
当对象接收到一条消息时,消息函数随着对象isa
指针到类的结构体中,在method list
中查找方法selector
。若是在本类中找不到对应的selector
,则objc_msgSend
会向其父类的method list
中查找selector
,若是还不能找到则沿着继承关系一直向上查找,直到找到NSObject
类。
Runtime
在selector
查找的过程作了优化,为类的结构体中增长了cache
字段,每一个类都有独立的cache
,在一个selector
被调用后就会加入到cache
中。在每次搜索方法列表以前,都会先检查cache
中有没有,若是没有才调用方法列表,这样会提升方法的查找效率。
若是经过OC代码的调用都会走消息发送的阶段,若是不想要消息发送的过程,能够获取到方法的函数指针直接调用。经过NSObject
的methodForSelector:
方法能够获取到函数指针,获取到指针后须要对指针进行类型转换,转换为和调用函数相符的函数指针,而后发起调用便可。
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
复制代码
在Runtime
中,objc_msgSend
函数也是开源的,但其是经过汇编代码实现的,arm64
架构代码能够在objc-msg-arm64.s
中找到。在Runtime
中,不少执行频率比较高的函数,都是用汇编写的。
objc_msgSend
并非彻底开源的,在_class_lookupMethodAndLoadCache3
函数中已经获取到Class
参数了。因此在下面中有一个确定是对象中获取isa_t
的过程,从方法命名和注释来看,应该是GetIsaFast
汇编命令。若是这样的话,就能够从消息发送到调用流程衔接起来了。
ENTRY _objc_msgSend
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL // calls IMP on success
NilTestSupport NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r11
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
END_ENTRY _objc_msgSend
复制代码
MESSENGER_START
:消息开始执行。NilTest
:判断接收消息的对象是否为nil
,若是为nil
则直接返回,这就是对nil
发送消息无效的缘由。GetIsaFast
:快速获取到isa
指向的对象,是一个类对象或元类对象。CacheLookup
:从ache list
中获取缓存selector
,若是查到则调用其对应的IMP
。LCacheMiss
:缓存没有命中,则执行此条汇编下面的方法。MethodTableLookup
:若是缓存中没有找到,则从method list
中查找。若是每次进行方法调用时,都按照对象模型来进行方法列表的查找,这样是很消耗时间的。Runtime
为了优化调用时间,在objc_class
中添加了一个cache_t
类型的cache
字段,经过缓存来优化调用时间。
在执行objc_msgSend
函数的消息发送过程当中,同一个方法第一次调用是没有缓存的,但调用以后就会存在缓存,以后的调用就直接调用缓存。因此方法的调用,能够分为有缓存和无缓存两种,这两种状况下的调用堆栈是不一样的。
首先是从缓存中查找IMP
,可是因为cache3
调用lookUpImpOrForward
函数时,已经查找过cache
了,因此传入的是NO
,不进入查找cahce
的代码块中。
struct cache_t {
// 存储被缓存方法的哈希表
struct bucket_t *_buckets;
// 占用的总大小
mask_t _mask;
// 已使用大小
mask_t _occupied;
}
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
复制代码
当给一个对象发送消息时,Runtime
会沿着isa
找到对应的类对象,但并不会马上查找method_list
,而是先查找cache_list
,若是有缓存的话优先查找缓存,没有再查找方法列表。
这是Runtime
对查找method
的优化,理论上来讲在cache
中的method
被访问的频率会更高。cache_list
由cache_t
定义,内部有一个bucket_t
的数组,数组中保存IMP
和key
,经过key
找到对应的IMP
并调用。具体源码能够查看objc-cache.mm
。
若是类对象没有被初始化,而且lookUpImpOrForward
函数的initialize
参数为YES
,则表示须要对该类进行建立。函数内部主要是一些基础的初始化操做,并且会递归检查父类,若是父类未初始化,则先初始化其父类对象。
STATIC_ENTRY _cache_getImp
mov r9, r0
CacheLookup NORMAL
// cache hit, IMP in r12
mov r0, r12
bx lr // return imp
CacheLookup2 GETIMP
// cache miss, return nil
mov r0, #0
bx lr
END_ENTRY _cache_getImp
复制代码
下面会进入cache_getImp
的代码中,然而这个函数不是开源的,可是有一部分源码能够看到,是经过汇编写的。其内部调用了CacheLookup
和CacheLookup2
两个函数,这两个函数也都是汇编写的。
通过第一次调用后,就会存在缓存。进入objc_msgSend
后会调用CacheLookup
命令,若是找到缓存则直接调用。可是Runtime
并非彻底开源的,内部不少实现咱们依然看不到,CacheLookup
命令内部也同样,只能看到调用完命令后就开始执行咱们的方法了。
CacheLookup NORMAL, CALL
复制代码
在上面objc_msgSend
汇编实现中,存在一个MethodTableLookup
的汇编调用。在这条汇编调用中,调用了查找方法列表的C函数。下面是精简版代码。
.macro MethodTableLookup
// 调用MethodTableLookup并在内部执行cache3函数(C函数)
blx __class_lookupMethodAndLoadCache3
mov r12, r0 // r12 = IMP
.endmacro
复制代码
在MethodTableLookup
中经过调用_class_lookupMethodAndLoadCache3
函数,来查找方法列表。函数内部是经过lookUpImpOrForward
函数实现的,在调用时cache
字段传入NO
,表示不须要查找缓存了,由于在cache3
函数上面已经经过汇编查找过了。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
// 经过cache3内部调用lookUpImpOrForward函数
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码
lookUpImpOrForward
函数是支持多线程的,因此内部会有不少锁操做。其内部有一个rwlock_t
类型的runtimeLock
变量,有runtimeLock
控制读写锁。其内部有不少逻辑代码,这里把函数内部实现作了精简,把核心代码贴到下面。
经过类对象的isRealized
函数,判断当前类是否被实现,若是没有被实现,则经过realizeClass
函数实现该类。在realizeClass
函数中,会设置version
、rw
、superClass
等一些信息。
// 执行查找imp和转发的代码
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 若是cache是YES,则从缓存中查找IMP。若是是从cache3函数进来,则不会执行cache_getImp()函数
if (cache) {
// 经过cache_getImp函数查找IMP,查找到则返回IMP并结束调用
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
// 判断类是否已经被建立,若是没有被建立,则将类实例化
if (!cls->isRealized()) {
// 对类进行实例化操做
realizeClass(cls);
}
// 第一次调用当前类的话,执行initialize的代码
if (initialize && !cls->isInitialized()) {
// 对类进行初始化,并开辟内存空间
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
runtimeLock.assertReading();
// 尝试获取这个类的缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
// 若是没有从cache中查找到,则从方法列表中获取Method
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 若是获取到对应的Method,则加入缓存并从Method获取IMP
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
// 循环获取这个类的缓存IMP 或 方法列表的IMP
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 获取父类缓存的IMP
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 若是发现父类的方法,而且再也不缓存中,在下面的函数中缓存方法
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 在父类的方法列表中,获取method_t对象。若是找到则缓存查找到的IMP
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 若是没有找到,则尝试动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}
// 若是没有IMP被发现,而且动态方法解析也没有处理,则进入消息转发阶段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
复制代码
在方法第一次调用时,能够经过cache_getImp
函数查找到缓存的IMP
。但若是是第一次调用,就查不到缓存的IMP
,就会进入到getMethodNoSuper_nolock
函数中执行。下面是getMethod
函数的关键代码。
getMethodNoSuper_nolock(Class cls, SEL sel) {
// 根据for循环,从methodList列表中,从头开始遍历,每次遍历后向后移动一位地址。
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// 对sel参数和method_t作匹配,若是匹配上则返回。
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
复制代码
当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象isa
所指向类的方法列表,并用调用方法的SEL
和遍历的method_t
结构体的name
字段作对比,若是相等则将IMP
函数指针返回。
// 根据传入的SEL,查找对应的method_t结构体
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
for (auto& meth : *mlist) {
// SEL本质上就是字符串,查找的过程就是进行字符串对比
if (meth.name == sel) return &meth;
}
}
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
return nil;
}
复制代码
在getMethod
函数中,主要是对Class
的methods
方法列表进行查找和匹配。类的方法列表都在Class
的class_data_bits_t
中,经过data()
函数从bits
中获取到class_rw_t
的结构体,而后获取到方法列表methods
,并遍历方法列表。
若是从当前类中获取不到对应的IMP
,则进入循环中。循环是从当前类出发,沿着继承者链的关系,一直向根类查找,直到找到对应的IMP
实现。
查找步骤和上面也同样,先经过cache_getImp
函数查找父类的缓存,若是找到则调用对应的实现。若是没找到缓存,表示第一次调用父类的方法,则调用getMethodNoSuper_nolock
函数从方法列表中获取实现。
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
复制代码
若是没有找到方法实现,则会进入动态方法决议的步骤。在if
语句中会判断传入的resolver
参数是否为YES
,而且会判断是否已经有过动态决议,由于下面是goto retry
,因此这段代码可能会执行屡次。
if (resolver && !triedResolver) {
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
复制代码
若是知足条件而且是第一次进行动态方法决议,则进入if
语句中调用_class_resolveMethod
函数。动态方法决议有两种,_class_resolveClassMethod
类方法决议和_class_resolveInstanceMethod
实例方法决议。
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
复制代码
在这两个动态方法决议的函数实现中,本质上都是经过objc_msgSend
函数,调用NSObject
中定义的resolveInstanceMethod:
和resolveClassMethod:
两个方法。
能够在这两个方法中动态添加方法,添加方法实现后,会在下面执行goto retry
,而后再次进入方法查找的过程当中。从triedResolver
参数能够看出,动态方法决议的机会只有一次,若是此次再没有找到,则进入消息转发流程。
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
复制代码
若是通过上面这些步骤,仍是没有找到方法实现的话,则进入动态消息转发中。在动态消息转发中,还能够对没有实现的方法作一些弥补措施。
下面是经过objc_msgSend
函数发送一条消息后,所通过的调用堆栈,调用顺序是从上到下的。
CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
复制代码
在调用objc_msgSend
函数后,会有一系列复杂的判断逻辑,总结以下。
SEL
是否须要忽略,例如Mac OS
中的垃圾处理机制启动的话,则忽略retain
、release
等方法,并返回一个_objc_ignored_method
的IMP
,用来标记忽略。nil
,由于在OC中对nil
发消息是无效的,这是由于在调用时就经过判断条件过滤掉了。cache_getImp
函数进行查找,若是找到缓存则直接返回IMP
。method list
,查找是否有对应的SEL
,若是有则获取到Method
对象,并从Method
对象中获取IMP
,并返回IMP
(这步查找结果是Method
对象)。SEL
,则去父类中查找。首先查找cache list
,若是缓存中没有则查找method list
,并以此类推直到查找到NSObject
为止。SEL
,则进入动态方法解析中。能够在resolveInstanceMethod
和resolveClassMethod
两个方法中动态添加实现。Crash
。整体能够被分为三部分:
objc_msgSend
函数后,内部的一些处理逻辑。IMP
的过程,会涉及到cache list
和method list
等。在cache list
中找不到方法的状况下,会经过MethodTableLookup
宏定义从类的方法列表中,查找对应的方法。在MethodTableLookup
中本质上也是调用_class_lookupMethodAndLoadCache3
函数,只是在传参时cache
字段传NO
,表示不从cache list
中查找。
在cache3
函数中,是直接调用的lookUpImpOrForward
函数,这个函数内部实现很复杂,能够看一下Runtime Analyze。在这个里面直接搜lookUpImpOrForward
函数名便可,能够详细看一下内部实现逻辑。
简书因为排版的问题,阅读体验并很差,布局、图片显示、代码等不少问题。因此建议到我Github
上,下载Runtime PDF
合集。把全部Runtime
文章总计九篇,都写在这个PDF
中,并且左侧有目录,方便阅读。
下载地址:Runtime PDF 麻烦各位大佬点个赞,谢谢!😁