(3)OC中消息和消息转发-01

前一段时间有朋友问了下面的这个问题,我给出的回答是这样的:

其实上面回答的方法调用也都是基于以前对runtime的理解,和自己试验出来的结果,但是,回答完这个问题之后,抱着探究到底的精神(其实是这几天产品没提什么需求,有点儿闲),问了自己一个问题:你怎么知道是底层调用的是这几个方法??。。。又是一番查资料,验证问题。。。下面正式开始分析:

  • 首先新建一个Person类,代码如下:

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)eat;

@end

Person.m

#import "Person.h"

@implementation Person

- (void)eat{
    NSLog(@"Person eat=======");
}

@end

很简单的一个类

在main函数里面调用eat方法:

Person *p = [[Person alloc] init];
        
[p eat];

main.m代码转换成c++代码:

 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

main.cpp文件当中查看底层实现:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));

    }
    return 0;
}

通过这些代码我们发现,OC中方法调用本质上就是给对象发消息,上面给对象发消息的代码可以简写成:

objc_msgSend(p, @selector(eat));

上面方法调用的意思就是:给p对象发送名为eat的消息,所以OC中给对象发消息本质上都是调用objc_msgSend方法,接着看下苹果官方文档对这个方法的定义(我是用的Dash查看的):

  • self :指向接收消息的类实例的指针。简单来说就是消息的接收者。
  • op:处理消息的方法选择器,也就是我们常见的@selector()
  • ...:包含方法参数的可变参数列表,

翻译如下:

当遇到方法调用时,编译器生成对其中一个函数的调用。当向superclass发消息的时候调用的是objc_msgSendSuper,向其他对象发消息的时候调用objc_msgSend,方法返回值是一个结构体的时候调用的是objc_msgSendSuper_stretobjc_msgSend_stret

接下来我们再看下objc_msgSend的底层实现,objc 源码,发现底层是用汇编代码实现的(表示很蛋疼):

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 lists
LCacheMiss:
	// isa still in r10
	MESSENGER_END_SLOW
	jmp	__objc_msgSend_uncached

	END_ENTRY _objc_msgSend

由于本人汇编就懂几个简单的指令,所以就做简单分析:

首先,GetIsaFast获取对象的isa指针,接着通过CacheLookup从缓存查找方法的实现,会调用cache_getImp(Class cls, SEL sel),如果缓存中没有查到也就是cache miss,会跳到__objc_msgSend_uncached方法:

STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves
	
	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band r10 is the searched class

	// r10 is already the class to search
	MethodTableLookup NORMAL	// r11 = IMP
	jmp	*%r11			// goto *imp

	END_ENTRY __objc_msgSend_uncached

在这个方法里面又会调用MethodTableLookup查找方法列表:

.macro MethodTableLookup

	push	%rbp
	mov	%rsp, %rbp
	
	sub	$$0x80+8, %rsp		// +8 for alignment

	movdqa	%xmm0, -0x80(%rbp)
	push	%rax			// might be xmm parameter count
	movdqa	%xmm1, -0x70(%rbp)
	push	%a1
	movdqa	%xmm2, -0x60(%rbp)
	push	%a2
	movdqa	%xmm3, -0x50(%rbp)
	push	%a3
	movdqa	%xmm4, -0x40(%rbp)
	push	%a4
	movdqa	%xmm5, -0x30(%rbp)
	push	%a5
	movdqa	%xmm6, -0x20(%rbp)
	push	%a6
	movdqa	%xmm7, -0x10(%rbp)

	// _class_lookupMethodAndLoadCache3(receiver, selector, class)
   ...
   ...
   省略

MethodTableLookup里面又调用了_class_lookupMethodAndLoadCache3方法:

/***********************************************************************
* _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*/);
}

接下来看一下lookUpImpOrForward的实现

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search 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.

    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }

    
 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    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.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

我们重点看// No implementation found. Try method resolver once.下面的代码,如果依然没有找到方法的实现,会调用_class_resolveMethod方法:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

在这个方法里面,我们可以清楚地看到,首先会判断该对象是否是元类对象,如果不是,会调用_class_resolveInstanceMethod方法,否则会调用_class_resolveClassMethod方法。如果还没有找到对应的IMP方法实现:

// No implementation found, and method resolver didn't help. 
// Use forwarding.

接下来就会用到消息转发,调用这个方法_objc_msgForward_impcache

/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward and _objc_msgForward_stret are the externally-callable
*   functions returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
*   method caches.
*
********************************************************************/

	STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band condition register is NE for stret, EQ otherwise.

	MESSENGER_START
	nop
	MESSENGER_END_SLOW
	
	jne	__objc_msgForward_stret
	jmp	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache
	
	
	ENTRY __objc_msgForward
	// Non-stret version

	movq	__objc_forward_handler(%rip), %r11
	jmp	*%r11

	END_ENTRY __objc_msgForward


	ENTRY __objc_msgForward_stret
	// Struct-return version

	movq	__objc_forward_stret_handler(%rip), %r11
	jmp	*%r11

	END_ENTRY __objc_msgForward_stret
	...
   ...
   省略

从description可以看到__objc_msgForward_impcache实际上是一个存储在方法缓存当中的函数指针,当某种类型的对象处理消息的过程中,无论怎样都找不到对应的IMP实现时,会将它作为sel对应的imp记入缓存。所以,从严格意义上来讲_class_resolveInstanceMethod_class_resolveClassMethod并不是由__objc_msgForward_impcache触发的,并不能算作消息转发的后续步骤,消息转发后,该对象如果再次遇到同名消息是,会直接从缓存中找到对应的IMP,即_objc_msgForward_impcache,此时我们需要重写- (id)forwardingTargetForSelector:(SEL)aSelector方法,重定向到别的类当中找到方法的实现。

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

官方文档截图:

主要看下官方文档里面的Discussion:

官方文档写的非常清楚:当你只想将消息重定向到另一个类时,用这个方法非常有用,因为它比常规的转发快一个数量级,他转发的目标是捕获NSInvocation。也有人将这种方式称为Fast Forwarding,因为这一步不会创建NSInvocation对象。

今天先写这么多,还没完呢,上网查了好多资料,好多网上资料苹果的源代码都是老的,跟最新代码不太一样,也费了我很多时间,下一篇文章将会介绍如何通过regular forwarding也有人叫Normal Forwarding,动态添加方法实现。。。。


References:

  • https://lpd-ios.github.io/2017/12/19/ObjC-Message/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微信公众号:Java团长

Java生成、解析二维码

目标:借助Google提供的ZXing Core工具包,使用Java语言实现二维码的生成和解析。

25410
来自专栏编程

用Swift写一个响应式编程库

2017年又快过去了,忙了一年感觉没啥收获,感觉是不是应该写点啥,想了好久没想出要写什么。下半年因为工作的原因,狗狗也没养了,吉他上也积满了灰尘,兴致勃勃的学习...

19350
来自专栏一“技”之长

iOS中JSON数据的解析 原

官方为我们提供的解析JSON数据的类是NSJSONSerialization,首先我们先来看下这个类的几个方法:

8750
来自专栏Java成神之路

Java微信开发_Exception_01_The type org.xmlpull.v1.XmlPullParser cannot be resolved. It is indirectly ref

这个异常是在做微信开发时出现的,在引入了XStream的jar包之后,还是出现了如下错误信息:

10930
来自专栏陈满iOS

iOS中Cocoa框架·Runtime及isa指针知识·填坑

是什么因素使一个程序成为Cocoa程序呢?不是编程语言,因为在Cocoa开发中你可以使用各种语言;也不是开发工具,你可以在命令行上就可以创建Cocoa程序。Co...

28020
来自专栏码生

ios KVO 官方文档学习

When an observer is registered for an attribute of an object the isa pointer of ...

13530
来自专栏梧雨北辰的开发录

iOS运行时Runtime应用

18920
来自专栏冰霜之地

iOS 如何实现Aspect Oriented Programming (上)

在“Runtime病院”住院的后两天,分析了一下AOP的实现原理。“出院”后,发现Aspect库还没有详细分析,于是就有了这篇文章,今天就来说说iOS 是如何实...

17820
来自专栏王磊的博客

C#常用代码汇总

1、字符串首字母转为大写。 System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCa...

31450
来自专栏数据结构与算法

BZOJ1269: [AHOI2006]文本编辑器editor

Descriptio 这些日子,可可不和卡卡一起玩了,原来可可正废寝忘食的想做一个简单而高效的文本编辑器。你能帮助他吗? 为了明确任务目标,可可对“文本编辑器...

29870

扫码关注云+社区

领取腾讯云代金券