探秘Runtime - Runtime消息发送机制
栏目: Objective-C · 发布时间: 5年前
内容简介:在OC中方法调用是通过例如下面的OC代码会被转换为发送消息的第二个参数是一个
在OC中方法调用是通过 Runtime
实现的, Runtime
进行方法调用本质上是发送消息,通过 objc_msgSend()
函数进行消息发送。
例如下面的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
中查找。
cache_t
如果每次进行方法调用时,都按照对象模型来进行方法列表的查找,这样是很消耗时间的。 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 麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。