探秘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 ,返回结果也是 selfclass由此可知,当前对象无论调用任何方法,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 类型, super2objc_msgSendSuper() 的一种实现方式,不对外暴露。

struct objc_super {
	id	receiver;
	Class	class;
};
复制代码

fp 则表示返回一个 long double 的浮点型,而 fp2 则返回一个 complex long double 的复杂浮点型,其他 floatdouble 的普通浮点型都用 objc_msgSend 。除了上面这些情况外,其他都通过 objc_msgSend() 调用。

消息发送流程

当一个对象被创建时,系统会为其分配内存,并完成默认的初始化工作,例如对实例变量进行初始化。对象第一个变量是指向其类对象的指针- isaisa 指针可以访问其类对象,并且通过其类对象拥有访问其所有继承者链中的类。

探秘Runtime - Runtime消息发送机制

isa 指针不是语言的一部分,主要为 Runtime 机制提供服务。

当对象接收到一条消息时,消息函数随着对象 isa 指针到类的结构体中,在 method list 中查找方法 selector 。如果在本类中找不到对应的 selector ,则 objc_msgSend 会向其父类的 method list 中查找 selector ,如果还不能找到则沿着继承关系一直向上查找,直到找到 NSObject 类。

Runtimeselector 查找的过程做了优化,为类的结构体中增加了 cache 字段,每个类都有独立的 cache ,在一个 selector 被调用后就会加入到 cache 中。在每次搜索方法列表之前,都会先检查 cache 中有没有,如果没有才调用方法列表,这样会提高方法的查找效率。

如果通过OC代码的调用都会走消息发送的阶段,如果不想要消息发送的过程,可以获取到方法的函数指针直接调用。通过 NSObjectmethodForSelector: 方法可以获取到函数指针,获取到指针后需要对指针进行类型转换,转换为和调用函数相符的函数指针,然后发起调用即可。

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_listcache_t 定义,内部有一个 bucket_t 的数组,数组中保存 IMPkey ,通过 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 的代码中,然而这个函数不是开源的,但是有一部分源码可以看到,是通过汇编写的。其内部调用了 CacheLookupCacheLookup2 两个函数,这两个函数也都是汇编写的。

经过第一次调用后,就会存在缓存。进入 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 函数中,会设置 versionrwsuperClass 等一些信息。

// 执行查找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 函数中,主要是对 Classmethods 方法列表进行查找和匹配。类的方法列表都在 Classclass_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 函数后,会有一系列复杂的判断逻辑,总结如下。

  1. 判断当前调用的 SEL 是否需要忽略,例如 Mac OS 中的垃圾处理机制启动的话,则忽略 retainrelease 等方法,并返回一个 _objc_ignored_methodIMP ,用来标记忽略。
  2. 判断接收消息的对象是否为 nil ,因为在OC中对 nil 发消息是无效的,这是因为在调用时就通过判断条件过滤掉了。
  3. 从方法的缓存列表中查找,通过 cache_getImp 函数进行查找,如果找到缓存则直接返回 IMP
  4. 查找当前类的 method list ,查找是否有对应的 SEL ,如果有则获取到 Method 对象,并从 Method 对象中获取 IMP ,并返回 IMP (这步查找结果是 Method 对象)。
  5. 如果在当前类中没有找到 SEL ,则去父类中查找。首先查找 cache list ,如果缓存中没有则查找 method list ,并以此类推直到查找到 NSObject 为止。
  6. 如果在类的继承体系中,始终没有查找到对应的 SEL ,则进入动态方法解析中。可以在 resolveInstanceMethodresolveClassMethod 两个方法中动态添加实现。
  7. 动态消息解析如果没有做出响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一些处理,否则就会 Crash

整体分析

总体可以被分为三部分:

  1. 刚调用 objc_msgSend 函数后,内部的一些处理逻辑。
  2. 复杂的查找 IMP 的过程,会涉及到 cache listmethod list 等。
  3. 进入消息转发阶段。

cache list 中找不到方法的情况下,会通过 MethodTableLookup 宏定义从类的方法列表中,查找对应的方法。在 MethodTableLookup 中本质上也是调用 _class_lookupMethodAndLoadCache3 函数,只是在传参时 cache 字段传 NO ,表示不从 cache list 中查找。

cache3 函数中,是直接调用的 lookUpImpOrForward 函数,这个函数内部实现很复杂,可以看一下 Runtime Analyze 。在这个里面直接搜 lookUpImpOrForward 函数名即可,可以详细看一下内部实现逻辑。

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

探秘Runtime - Runtime消息发送机制

下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

走进搜索引擎

走进搜索引擎

梁斌 / 电子工业出版社 / 2007-1 / 49.80元

《走进搜索引擎》由搜索引擎开发研究领域年轻而有活力的科学家精心编写,作者将自己对搜索引擎的深刻理解和实际应用巧妙地结合,使得从未接触过搜索引擎原理的读者也能够轻松地在搜索引擎的大厦中邀游一番。《走进搜索引擎》作为搜索引擎原理与技术的入门书籍,面向那些有志从事搜索引擎行业的青年学生、需要完整理解并优化搜索引擎的专业技术人员、搜索引擎的营销人员,以及网站的负责人等。《走进搜索引擎》是从事搜索引擎开发的......一起来看看 《走进搜索引擎》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具