博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Runtime源码 方法调用的过程
阅读量:6582 次
发布时间:2019-06-24

本文共 12315 字,大约阅读时间需要 41 分钟。

前言

Objective-C语言的一大特性就是动态的,根据的描述:在runtime之前,消息和方法并不是绑定在一起的,编译器会把方法调用转换为objc_msgSend(receiver, selector),如果方法中带有参数则转换为objc_msgSend(receiver, selector, arg1, arg2, ...)接下来我们通过源码一窥究竟,在次之前我们先了解几个基本概念

  • SEL 在objc.h文件中我们可以看到如下代码:
/// An opaque type that represents a method selector.typedef struct objc_selector *SEL;复制代码

SEL其实就是一个不透明的类型它代表一个方法选择子,在编译期,会根据方法名字生成一个ID。

  • IMP 在objc.h文件中我们可以看到IMP:
/// A pointer to the function of a method implementation. #if !OBJC_OLD_DISPATCH_PROTOTYPEStypedef void (*IMP)(void /* id, SEL, ... */ ); #elsetypedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); #endif复制代码

他是一个函数指针,指向方法实现的首地址。

  • Method
/// An opaque type that represents a method in a class definition.typedef struct objc_method *Method;  struct objc_method {    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;    char * _Nullable method_types                            OBJC2_UNAVAILABLE;    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;}                                                            OBJC2_UNAVAILABLE;复制代码

它保存了SEL到IMP和方法类型,所以我们可以通过SEL调用对应的IMP

方法调用的流程

objc_msgSend的消息分发分为以下几个步骤: 我们找到objc _msgSend源码,都是汇编,不过注释比较详尽

/******************************************************************** * * id objc_msgSend(id self, SEL	_cmd,...); * IMP objc_msgLookup(id self, SEL _cmd, ...); * * objc_msgLookup ABI: * IMP returned in r11 * Forwarding returned in Z flag * r10 reserved for our use but not used * ********************************************************************/		.data	.align 3	.globl _objc_debug_taggedpointer_classes_objc_debug_taggedpointer_classes:	.fill 16, 8, 0	.globl _objc_debug_taggedpointer_ext_classes_objc_debug_taggedpointer_ext_classes:	.fill 256, 8, 0	ENTRY _objc_msgSend	UNWIND _objc_msgSend, NoFrame	MESSENGER_START	NilTest	NORMAL	GetIsaFast NORMAL		// r10 = self->isa	CacheLookup NORMAL, CALL	// calls IMP on success	NilTestReturnZero NORMAL	GetIsaSupport NORMAL// cache miss: go search the method listsLCacheMiss:	// isa still in r10	MESSENGER_END_SLOW	jmp	__objc_msgSend_uncached	END_ENTRY _objc_msgSend		ENTRY _objc_msgLookup	NilTest	NORMAL	GetIsaFast NORMAL		// r10 = self->isa	CacheLookup NORMAL, LOOKUP	// returns IMP on success	NilTestReturnIMP NORMAL	GetIsaSupport NORMAL// cache miss: go search the method listsLCacheMiss:	// isa still in r10	jmp	__objc_msgLookup_uncached	END_ENTRY _objc_msgLookup		ENTRY _objc_msgSend_fixup	int3	END_ENTRY _objc_msgSend_fixup		STATIC_ENTRY _objc_msgSend_fixedup	// Load _cmd from the message_ref	movq	8(%a2), %a2	jmp	_objc_msgSend	END_ENTRY _objc_msgSend_fixedup复制代码

就此我们大概可以了解到其调用流程:

  • 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象

  • 从缓存里寻找,找到了则分发,否则

  • 利用objc-class.mm中_ class _lookupMethodAndLoadCache3方法去寻找selector

    • 如果支持GC,忽略掉非GC环境的方法(retain等)
    • 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
    • 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
    • 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
    • 转发这个selector,否则
    • 报错,抛出异常

这里的**_ class _lookupMethodAndLoadCache3其实就是对lookUpImpOrForward**方法的调用:

/************************************************************************ _class_lookupMethodAndLoadCache.* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().* This lookup avoids optimistic cache scan because the dispatcher * already tried that.**********************************************************************/IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls){            return lookUpImpOrForward(cls, sel, obj,                               YES/*initialize*/, NO/*cache*/, YES/*resolver*/);}复制代码

对第五个参数cache传值为NO,因为在此之前已经做了一个查找这里CacheLookup NORMAL, CALL,这里是对缓存查找的一个优化。

接下来看一下lookUpImpOrForward的一些关键实现细节

  • 缓存查找优化
// Optimistic cache lookup if (cache)       methodPC = _cache_getImp(cls, sel);     if (methodPC) return methodPC;     }复制代码

这里有个判断,是否需要缓存查找,如果cache为NO则进入下一步

  • 检查被释放类
// Check for freed classif (cls == _class_getFreedObjectClass())	return (IMP) _freedHandler;复制代码

_class _getFreedObjectClass的实现:

/************************************************************************ _class_getFreedObjectClass.  Return a pointer to the dummy freed* object class.  Freed objects get their isa pointers replaced with* a pointer to the freedObjectClass, so that we can catch usages of* the freed object.**********************************************************************/static Class _class_getFreedObjectClass(void){    return (Class)freedObjectClass;}复制代码

注释写到,这里返回的被释放对象的指针,不是太理解,备注这以后再看看

  • 懒加载+initialize
// Check for +initialize    if (initialize  &&  !cls->isInitialized()) {        _class_initialize (_class_getNonMetaClass(cls, inst));        // If sel == initialize, _class_initialize will send +initialize and         // then the messenger will send +initialize again after this         // procedure finishes. Of course, if this is not being called         // from the messenger then it won't happen. 2778172    }复制代码

在方法调用过程中,如果类没有被初始化的时候,会调用_class_initialize对类进行初始化,关于+initialize可以看之前的。

  • 加锁保证原子性
// The lock is held to make method-lookup + cache-fill atomic     // with respect to method addition. Otherwise, a category could     // be added but ignored indefinitely because the cache was re-filled     // with the old value after the cache flush on behalf of the category.retry:    methodListLock.lock();    // Try this class's cache.    methodPC = _cache_getImp(cls, sel);    if (methodPC) goto done;复制代码

这里又做了一次缓存查找,因为上一步执行了+initialize

加锁这一部分只有一行简单的代码,其主要目的保证方法查找以及缓存填充(cache-fill)的原子性,保证在运行以下代码时不会有新方法添加导致缓存被冲洗(flush)。

  • 本类的方法列表查找
// Try this class's method lists.meth = _class_getMethodNoSuper_nolock(cls, sel);if (meth) {log_and_fill_cache(cls, cls, meth, sel); methodPC = method_getImplementation(meth); goto done;}复制代码

这里调用了log_ and_ fill_cache这个后面来看,接下里就是

  • 父类方法列表查找
// Try superclass caches and method lists.    curClass = cls;    while ((curClass = curClass->superclass)) {        // Superclass cache.        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);        if (meth) {            if (meth != (Method)1) {                // Found the method in a superclass. Cache it in this class.                log_and_fill_cache(cls, curClass, meth, sel);                methodPC = method_getImplementation(meth);                goto done;            }            else {                // Found a forward:: entry in a superclass.                // Stop searching, but don't cache yet; call method                 // resolver for this class first.                break;            }        }        // Superclass method list.        meth = _class_getMethodNoSuper_nolock(curClass, sel);        if (meth) {            log_and_fill_cache(cls, curClass, meth, sel);            methodPC = method_getImplementation(meth);            goto done;        }    }复制代码

关于消息在列表方法查找的过程,根据官方文档如下:

这里沿着集成体系对父类的方法列表进行查找,找到了就调用log_ and_ fill_cache

log_ and_ fill_cach的实现:

记录:

/************************************************************************ log_and_fill_cache* Log this method call. If the logger permits it, fill the method cache.* cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question.**********************************************************************/static voidlog_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel){#if SUPPORT_MESSAGE_LOGGING    if (objcMsgLogEnabled) {        bool cacheIt = logMessageSend(implementer->isMetaClass(),                                       cls->nameForLogging(),                                      implementer->nameForLogging(),                                       sel);        if (!cacheIt) return;    }#endif    _cache_fill (cls, meth, sel);}复制代码

内部调用了**_cache _fill**,填充缓存:

/************************************************************************ _cache_fill.  Add the specified method to the specified class' cache.* Returns NO if the cache entry wasn't added: cache was busy, *  class is still being initialized, new entry is a duplicate.** Called only from _class_lookupMethodAndLoadCache and* class_respondsToMethod and _cache_addForwardEntry.** Cache locks: cacheUpdateLock must not be held.**********************************************************************/bool _cache_fill(Class cls, Method smt, SEL sel){    uintptr_t newOccupied;    uintptr_t index;    cache_entry **buckets;    cache_entry *entry;    Cache cache;    cacheUpdateLock.assertUnlocked();    // Never cache before +initialize is done    if (!cls->isInitialized()) {        return NO;    }    // Keep tally of cache additions    totalCacheFills += 1;    mutex_locker_t lock(cacheUpdateLock);    entry = (cache_entry *)smt;    cache = cls->cache;    // Make sure the entry wasn't added to the cache by some other thread     // before we grabbed the cacheUpdateLock.    // Don't use _cache_getMethod() because _cache_getMethod() doesn't     // return forward:: entries.    if (_cache_getImp(cls, sel)) {        return NO; // entry is already cached, didn't add new one    }    // Use the cache as-is if it is less than 3/4 full    newOccupied = cache->occupied + 1;    if ((newOccupied * 4) <= (cache->mask + 1) * 3) {        // Cache is less than 3/4 full.        cache->occupied = (unsigned int)newOccupied;    } else {        // Cache is too full. Expand it.        cache = _cache_expand (cls);        // Account for the addition        cache->occupied += 1;    }    // Scan for the first unused slot and insert there.    // There is guaranteed to be an empty slot because the     // minimum size is 4 and we resized at 3/4 full.    buckets = (cache_entry **)cache->buckets;    for (index = CACHE_HASH(sel, cache->mask);          buckets[index] != NULL;          index = (index+1) & cache->mask)    {        // empty    }    buckets[index] = entry;    return YES; // successfully added new cache entry}复制代码

这里还没找到实现则进入下一步,动态方法解析和消息转发,关于消息转发的细节我们下篇再看。

方法缓存

在上面截出的源码中我们多次看到了cache,下面我们就来看看这个,在runtime.hobjc-runtime-newcache的定义如下

struct objc_cache {    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;    unsigned int occupied                                    OBJC2_UNAVAILABLE;    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;};复制代码
struct cache_t {  struct bucket_t *_buckets;  mask_t _mask;  mask_t _occupied;  ...}复制代码

这就是cache在runtime层面的表示,里面的字段和代表的含义类似

  • buckets
    数组表示的hash表,每个元素代表一个方法缓存
  • mask
    当前能达到的最大index(从0开始),,所以缓存的size(total)是mask+1
  • occupied
    被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目

而在_ buckets中包含了一个个的cache_entrybucket_t(objc2.0的变更):

typedef struct {    SEL name;     // same layout as struct old_method    void *unused;    IMP imp;  // same layout as struct old_method} cache_entry;复制代码

cache_entry定义也包含了三个字段,分别是:

  • name,被缓存的方法名字
  • unused,保留字段,还没被使用。
  • imp,方法实现
struct bucket_t {private:    cache_key_t _key;    IMP _imp;    ...}复制代码

而bucket_t则没有了老的unused,包含了两个字段:

  • key,方法的标志(和之前的name对应)
  • imp, 方法的实现

后记

从runtime的源码我们知道了方法调用的流程和方法缓存,有些附带的问题答案也就呼之欲出了:

  • 方法缓存在元类的上,由第一节()我们就知道在objc_class的isa指向了他的元类,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。
  • 在方法调用的父类方法列表查找过程中,如果命中了也会调用_cache_fill (cls, meth, sel);,所以即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。
  • 缓存容量限制,在上面的代码中我们注意到这个判断:
// Use the cache as-is if it is less than 3/4 fullmask_t newOccupied = cache->occupied() + 1;mask_t capacity = cache->capacity();if (cache->isConstantEmptyCache()) {    // Cache is read-only. Replace it.    cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);}else if (newOccupied <= capacity / 4 * 3) {     // Cache is less than 3/4 full. Use it as-is.}else {     // Cache is too full. Expand it.     cache->expand();}复制代码

当cache为空时创建;当新的被占用槽数小于等于其容量的3/4时,直接使用;否则调用cache->expand();扩充容量:

void cache_t::expand(){    cacheUpdateLock.assertLocked();        uint32_t oldCapacity = capacity();    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;    if ((uint32_t)(mask_t)newCapacity != newCapacity) {        // mask overflow - can't grow further        // fixme this wastes one bit of mask        newCapacity = oldCapacity;    }    reallocate(oldCapacity, newCapacity);}复制代码
  • 为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存,多费事?
    散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的 这个问题么,我觉得有以下三个原因:
    • Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
    • list的方法还保存了除了selector和imp之外其他很多属性。
    • 散列表是有空槽的,会浪费空间。

相关资料:

美团酒旅博文:
官方文档:

转载地址:http://icino.baihongyu.com/

你可能感兴趣的文章
touch修改mtime和atime
查看>>
nodejs安装及windows环境配置
查看>>
转载:Beginning WF 4.0翻译——第三章(流程图工作流)
查看>>
mysql alter table
查看>>
芯片测试
查看>>
记录一次tomcat下项目没有加载成功
查看>>
在源代码中插入防止盗版代码片段的方式
查看>>
hdu 3367 Pseudoforest(最大生成树)
查看>>
一个人,一则故事,一份情愫,一个世界……
查看>>
ffserver联合ffmpeg建立媒体服务器
查看>>
下载稻草人下来刷新+gallery
查看>>
删除浏览器浏览器删除cookie方法
查看>>
微软URLRewriter.dll的url重写的简单使用(实现伪静态)
查看>>
leetcode -- Combination Sum II
查看>>
1z0-052 q209_7
查看>>
PIN码计算锦集
查看>>
[Unity3D]再次点击以退出程序
查看>>
架构师的97种习惯
查看>>
PHP 开发 APP 接口 学习笔记与总结 - XML 方式封装通信接口
查看>>
对一道编程题的后续思考
查看>>