Objective-C 本质上是一种基于 C 语言领域特定语言。C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间层实现的。

一、消息传递(方法调用

在 Objective-C 中,消息直到运行时才绑定方法实现编译器会将消息表达式转化为一个消息函数的调用。

OC中的消息表达式如下方法调用):

id returnValue = [someObject messageName:parameter];

这里someObject叫做接收者(receiver)messageName:叫做选择子(selector)选择子和参数合起来称为“消息”。编译器看到消息后,将其转换一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend编译看到上述这条消息转换一条标准的 C 语言函数调用:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数,这个函数将消息接收者和方法名作为主要参数,其原型如下所示

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

objc_msgSend通过以下几个步骤实现动态绑定机制:

消息传递的关键在于上一篇博客记录过的objc_class结构体,其有三个关键的字段

创建一个对象时,先为其分配内存,并初始化成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承链。

下图所示为消息传递过程的示意图:
5435345

二、消息转发

一个对象能接收一个消息时,会走正常的消息传递流程。当一个对象无法接收某一消息时,会发生什么呢?

对于后者,当不确定一个对象是否能接收某个消息时,可以调用respondsToSelector: 来进行判断

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发message forwarding”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。

消息转发机制大致可分为三个步骤

下图为消息转发过程的示意图:
423423423

1.动态方法解析

Objective-C 运行时会调用+ (BOOL)resolveInstanceMethod:(SEL)sel 或者+ (BOOL)resolveClassMethod:(SEL)sel,让你有机会提供一个函数实现。前者在对象方法未找到调用,后者在类方法未找到调用。我们可以通过重写两个方法,添加其他函数实现,并返回YES, 那运行系统就会重新启动一次消息发送过程

主要用的的方法如下

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法

返回值@return表示能否新增一个方法来处理,一般使用@dynamic属性来实现,若方法返回YES,则表示可以处理该消息,在这个过程中,可以动态的给消息增加方法。若方法返回NO,则进行消息转发的第二步,查找是否有其他的接收者。

这里简单说一下class_addMethod方法:

BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);
/** 
 * class_addMethod具有给定名称和实现的类中添加新方法
 * @param cls添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */

其中types编码类型为:
4535345
另外一定要记得每一个方法会默认隐藏两个参数,self_cmd

举例测试

新建一个类Person

//  Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
id getterName(id self, SEL cmd);
//- (NSString *)getName; OC的方法实现get函数
void setterName(id self, SEL cmd, NSString *value);
+ (BOOL)resolveInstanceMethod:(SEL)sel;  //动态方法解析
- (void)eat;
- (void)sleep;
@end


//  Person.m
#import "Person.h"
#import <objc/runtime.h>

@implementation Person
@dynamic name;
//动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(name)) {
        //@@: 此为签名符号
        class_addMethod(self, sel, (IMP)getterName, "@@:");
        //获取OC的函数指针
        //IMP getIMP = class_getMethodImplementation(self, @selector(getName));
        //获取函数方法数据
        //Method nameMethod = class_getInstanceMethod(self, @selector(getName));
        //自动获取函数参数类型
        //const char *nameType = method_getTypeEncoding(nameMethod);
        //添加方法
        //class_addMethod(self, sel, getIMP, nameType);
        return YES;
    }
    if (sel == @selector(setName:)) {
        class_addMethod(self, sel, (IMP)setterName, "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}
id getterName(id self, SEL cmd) {
    NSLog(@"%@, %s", [self class], sel_getName(cmd));
    
    return @"Getter called";
}
//OC的方法实现get
//- (NSString *)getName {
//    NSLog(@"%@", self);
//    return @"Getter called";
//}
void setterName(id self, SEL cmd, NSString *value) {
    NSLog(@"%@, %s, %@", [self class], sel_getName(cmd), value);
    
    NSLog(@"SetterName called");
}
@end

然后创建Person类的对象,并调用其name属性的setget方法:

//  main.m
Person *person = [[Person alloc] init];
person.name = @"Jake";
NSLog(@"%@", person.name);

423424
输出信息我们刚才定义的函数相同,说明我们在动态方法解析过程中拯救成功了。

2.备援接收者

如果上一步+ (BOOL)resolveClassMethod:(SEL)sel或者+ (BOOL)resolveInstanceMethod:(SEL)sel没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向

如果当前对象实现了- (id)forwardingTargetForSelector:(SEL)aSelector方法,Runtime就会调用这个方法,允许我们将消息的接受者转发给其他对象。
其调用的方法如下

//传入参数aSelector同样为无法处理的方法
//返回值为当前找到的备援接受者,如果没有找到则返回nil进入下一阶段
- (id)forwardingTargetForSelector:(SEL)aSelector;

很明显:forwardingTargetForSelector不能返回self,否则会陷入循环,因为返回self又回去当前实例对象身上走一遍消息查找流程,显然又会来到forwardingTargetForSelector

举例测试

我们创建一个毫不相关的Child类,其中实现Personeat函数:

//  Child.h
#import <Foundation/Foundation.h>

@interface Child : NSObject
@end

//  Child.m
#import "Child.h"

@implementation Child
- (void)eat {
    NSLog(@"Child method eat called");
}
@end

然后Person.m中实现- (id)forwardingTargetForSelector:(SEL)aSelector使用Person类对象调用eat函数:

//  Person.m
//备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selStr = NSStringFromSelector(aSelector);
    
    if ([selStr isEqualTo:@"eat"]) {
        //这里创建一个child的类对象,所以就算你没有在.h中实现该方法,也是会调用的,它就代表着用这个child对象来调用其eat私有方法一样
        return [[Child alloc] init];
        //通过返回一个对象,让这个对象调用其相关的函数,来实现响应
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

// main.m
Person *person = [[Person alloc] init];
[person eat];

4234234
输出信息我们刚才在Child类中定义的函数相同,说明我们在备援接收者过程中拯救成功了。

这样的方式被叫做伪多继承。消息转发实现的伪多继承,对应功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。

3.完整消息转发

如果上述的步骤返回的不是一个对象,而是nil或者self系统将会执行消息转发的最后一步:完整消息转发。

首先会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法创建NSInvocation对象,把尚未处理的那条消息的全部信息细节装在里边,在触发NSInvocation对象时,信息派发系统messagedispatch system)将会把消息指派给目标对象。这时会调用该方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

举例测试

Child类中实现Person类的sleep函数:

// Child.m
- (void)sleep {
    NSLog(@"Child method sleep called");
}

然后在Person类中实现- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector- (void)forwardInvocation:(NSInvocation *)anInvocation函数,并使用Person对象调用sleep函数:

//  Person.m
//生成方法签名
//我们必须重写该方法 消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因为我们要给selector提供一个合适的方法签名,所以我们必须重写这个方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString *sel = NSStringFromSelector(aSelector);
    // 判断要转发的SEL(方法)
    if ([sel isEqualToString:@"sleep"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        //"v@:"解释一下:每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd
    }
    
    return [super methodSignatureForSelector:aSelector];
}

//NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常
//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    //拿到消息(methodSignatureForSelector中生成的方法签名)
    SEL selector = [anInvocation selector];
    //新建需要转发消息的对象,即新建一个可以实现该消息的对象
    Child *child = [[Child alloc] init];
    if ([child respondsToSelector:selector]) {
        //转发 唤醒这个方法,交给这个可以实现该消息的对象来处理该消息
        [anInvocation invokeWithTarget:child];
        //这里将该方法给一个child的类对象来调用,就算你没有在.h中实现该方法,也是会调用的,它就意味着用这个child对象来调用其sleep私有方法一样
    } else {
        [super forwardInvocation:anInvocation];
    }
}
//从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。

// main.m
Person *person = [[Person alloc] init];
[person sleep];

4234234
这样就实现了消息的第三次拯救。

在消息转发机制中,虽有三层顺序关系(先执行第一种方法,前一套方案实现后一套就不会执行),但是在消息转发的过程中这三种办法都可以用来解决接受消息找不到对应的方法的情况。如果这三套方案都没有得以处理问题,那么程序就会crash

三、相关源码解析

1.消息发送的快速查imp过程汇编环节)

进入objc_msgSend源码的入口处:

	//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

1.1 当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend

再到LNilOrTagged都做了些什么:

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    //nil check判空处理,直接退出
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

我们先看LReturnZero

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend

等于说是结束了消息转发_objc_msgSend

1.2 检测指针如果为空,就立马返回。结论:给nil发送消息不会做处理

接着回来看看LGetIsaDone做了什么(LGetIsaDone入口在获取isaclass的后面):

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

进入CacheLookup,是一个宏定义,在cache中查找imp

//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStartFunction label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEndFunction,
	//   then our PC will be reset to LLookupRecoverFunction which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

    //从x16中取出class移到x15中
	mov	x15, x16			// stash the original isa
//开始查找
LLookupStartFunction:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //ldr表示将一个值存入到p10寄存器中
    //x16表示p16寄存器存储的值,当前是Class
    //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd &amp; mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreoptFunction
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
	tbnz	p11, #0, LLookupPreoptFunction
#endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) &amp; mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标哈希地址
    //整句表示_cmd &amp; mask并保存到p12
	and	p12, p1, p11, LSR #48		// x12 = _cmd &amp; mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
	and	p12, p1, p11			// x12 = _cmd &amp; mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//ldp表示出栈取出bucket中的imp和sel分别存放到p17和p9
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
	cmp	p9, p1				//     if (sel != _cmd) {
    //b.ne表示如果不相同则跳转到3f
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit Mode				// hit:    call or return imp
						//     }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3:	cbz	p9, MissLabelDynamic		//     if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEndFunction:
LLookupRecoverFunction:
	b	MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreoptFunction:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
	// keep the remaining 38 bits for the IMP offset, which may need to reach
	// across the shared cache. This offset needs to be shifted << 2. We did this
	// to give it even more reach, given the alignment of source (the class data)
	// and destination (the IMP)
	ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs << 38) | imp_offs
	cmp	x12, x17, LSR #38

.if Mode == GETIMP
	b.ne	MissLabelConstant		// cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub	x0, x16, x17        		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				        // cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub x17, x16, x17               // imp = isa - imp_offs
.if Mode == NORMAL
	br	x17
.elseif Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStartFunction		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

1.3 通过 类对象/元类 (objc_class) 通过内存平移得到cache,获取buckets,通过内存平移的方式获取对应的方法(对比sel)。

如果找到sel就会进入CacheHit,去return or call imp

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    //编码查找imp,并且返回x17,也就是imp
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

如果没有找到sel就会进入__objc_msgSend_uncached,下面是上述判断跳转代码

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入到缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

__objc_msgSend_uncached源码汇编

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

其中调用了MethodTableLookup宏:

.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

MethodTableLookup跳转到了_lookUpImpOrForward处。

1.4 如果对比sel找到了imp,就会return or call imp,如果没有sel,则去调用_lookUpImpOrForward

接下来_lookUpImpOrForward汇编代码里就找不到了。
至此快速查imp汇编部分就结束了,接下来到了漫长查找过程:c/c++环节。

总结消息发送快速查找imp(汇编):

objc_msgSend(receiver, sel, ...)

  • 1.检查消息接收者receiver是否存在,为nil则不做任何处理
  • 2.如果不为nil,通过receiverisa指针找到对应的class类对象
  • 3.找到class类对象进行内存平移,找到cache
  • 4.从cache中获取buckets
  • 5.从buckets中对比参数sel,看在缓存里有没有同名方法
  • 6.如果buckets中有对应sel –> cacheHit –> 调用imp
  • 7.如果buckets中没有对应sel –> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++慢速查找)

2.消息发送的慢速查找imp过程(c/c++环节)

来看看lookUpImpOrForward函数的实现:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // 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.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    // 检查当前类是个已知类
    checkIsKnownClass(cls);
    // 确定当前类的继承关系
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); 
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel); //cache中找IMP
            if (imp) goto done_unlock; //找到就直接返回了
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else { //如果不是常量优化缓存
            // 当前类的方法列表
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

    // 没有实现,尝试一次方法解析器
	// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

下面来慢慢讲解

2.1 检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass方法列表
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // 没有找到实现,方法解析器没有帮助。
                // 使用转发。
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 超类缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 在超类中找到forward::条目。
            // 停止搜索,但不要缓存;调用方法
            // 首先为这个类解析器
            break;
        }
        if (fastpath(imp)) {
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

进入了一个循环逻辑:

  • a.从本类的method list查找imp(查找的方式getMethodNoSuper_nolock,一会分析);
  • b.从本类的父类cache查找impcache_getImp编写的)
  • c.从本类的父类method list查找imp
    …继承链遍历…(父类->…->根父类
  • d.若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cachelog_and_fill_cache);
  • e.直到查找到nil指定imp为消息转发,跳出循环。

跳出循环后的逻辑:

done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;

如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver); // 插入缓存
}

2.2 在类和父类中查找imp

看看在类和父类继承链中查找imp是怎样查找的(getMethodNoSuper_nolock):

/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

	// 找到方法列表
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

search_method_list_inline里找到了method_t就会返回出去了(search_method_list_inline):

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

这里就是使用findMethodInSortedMethodListfindMethodInUnsortedMethodList通过sel找到method_t的。这两个函数的区别就是:

总结消息发送慢速查找imp(c/c++):

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  • 1.从本类的method list(二分查找/遍历查找)查找imp
  • 2.从本类的父类的cache查找imp(汇编)
  • 3.从本类的父类的method list (二分查找/遍历查找)查找imp
    …继承链遍历…(父类->…->根父类)里找cachemethod listimp
  • 4.若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
  • 5.直到查找到nil指定imp为消息转发,跳出循环,执行动态方法解析resolveMethod_locked

3.动态方法解析流程分析

到此就说明之前的查找方法都没有找到selimp,所以我们在运行期再进行动态方法解析。

我们先来到_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);
        //检查cls中sel的IMP是否存在
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
        	//没找到,则在进行一次实例方法解析
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

大概的流程如下:

  • 判断进行解析的是否是元类
  • 如果不是元类,则调用_class_resolveInstanceMethod进行对象方法动态解析
  • 如果是元类,则调用_class_resolveClassMethod进行类方法动态解析
  • 完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析

3.1 对象方法动态解析

我们先分析对象方法的动态解析,我们直接来到_class_resolveInstanceMethod方法处:

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
	//在cls->ISA()中检查是否实现了SEL_resolveInstanceMethod方法
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        //没有实现直接返回
        return;
    }

	//到这就说明实现了SEL_resolveInstanceMethod方法
	//那就通过objc_msgSend手动调用该类方法发送消息,这两步操作过后,这个动态方法就应该已经被加到方法列表里了
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 缓存结果(好或坏),这样解析器下次就不会触发
    // +resolveInstanceMethod添加到self,也就是cls
    //调用objc_msgSend完成后,再次查询cls中sel的IMP指针
    //因为上一步的成功,这一步应该就能找到,找不到肯定是有问题
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

	//解析完成并且需要输出日志
    if (resolved  &&  PrintResolving) {
    	//如果IMP找到了
        if (imp) {
        	//输出动态解析对象方法成功的日志
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {  //如果IMP没找到
            // 方法解析器没有添加任何东西?
            //输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel方法,并且返回了 YES,但并没有查找到IMP指针的日志
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

大致的流程如下:

  • 检查是否实现了+(BOOL)resolveInstanceMethod:(SEL)sel类方法,如果没有实现则直接返回(通过cls->ISA()是拿到元类,因为类方法是存储在元类上的对象方法)
  • 如果当前实现了+(BOOL)resolveInstanceMethod:(SEL)sel类方法,则通过objc_msgSend手动调用该类方法
  • 完成调用后,再次查询cls中的imp,并存到方法列表
  • 如果imp找到了,则输出动态解析对象方法成功的日志
  • 如果imp没有找到,则输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel,并且返回了YES,但并没有查找到imp的日志

3.2 类方法动态解析

接着我们分析类方法动态解析,我们直接来到_class_resolveClassMethod方法处:

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
	//断言是不是元类,不是元类就直接退出
    assert(cls->isMetaClass());

	//能到这里就表示该类就是元类
	//检查cls中是否实现了SEL_resolveClassMethod方法
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        //没有实现,就直接返回
        return;
    }
	
	//到这里还没有返回,就说明实现了SEL_resolveClassMethod方法
	//那就通过objc_msgSend手动调用该类方法发送消息,这两步操作过后,这个动态方法就应该已经被加到方法列表里了
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //这里需要通过元类和对象来找到类
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // 缓存结果(好或坏),这样解析器下次就不会触发
    // +resolveInstanceMethod添加到self,也就是cls
    //调用完成之后,再次查找cls中的sel的IMP指针
    //因为上一步的成功,这一步应该就能找到,找不到肯定是有问题
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
	
	//解析完成并且需要输出日志
    if (resolved  &&  PrintResolving) {
    	//如果找到了IMP指针
        if (imp) {
        	//输出动态解析对象方法成功的日志
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {  //没有找到IMP指针
            // 方法解析器没有添加任何东西?
            //输出虽然实现了+(BOOL)resolveClassMethod:(SEL)sel,并且返回了YES,但并没有查找到imp指针的日志
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

大致的流程如下:

  • 断言是否是元类,如果不是,直接退出,因为我们是要找类方法
  • 检查是否实现了+ (BOOL)resolveClassMethod:(SEL)sel类方法,如果没有实现则直接返回(通过cls是因为当前cls就是元类,因为类方法是存储在元类上的对象方法)
  • 如果当前实现了+ (BOOL)resolveClassMethod:(SEL)sel类方法,则通过objc_msgSend手动调用该类方法,注意这里和动态解析对象方法不同,这里需要通过元类和对象来找到类,也就是_class_getNonMetaClass
  • 完成调用后,再次查询cls中的imp,并存到方法列表
  • 如果imp找到了,则输出动态解析对象方法成功的日志
  • 如果imp没有找到,则输出虽然实现了+(BOOL)resolveClassMethod:(SEL)sel,并且返回了YES,但并没有查找到imp的日志

3.3 特殊的NSObject对象方法动态解析

我们再聚焦到_class_resolveMethod方法上,如果cls是元类,也就是说进行的是类方法动态解析的话,有以下源码:

_class_resolveClassMethod(cls, sel, inst); // 已经处理
if (!lookUpImpOrNil(cls, sel, inst, 
                    NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
{
    // 对象方法 决议
    _class_resolveInstanceMethod(cls, sel, inst);
}

对于_class_resolveClassMethod的执行,肯定是没有问题的,只是为什么在判断如果动态解析失败之后,还要再进行一次对象方法解析呢,这个时候就需要一张经典的isa走位图了:
5435345345
由这个流程图我们可以知道,元类最终继承于根元类,而根元类又继承于NSObject,那么也就是说在根元类中存储的类方法等价于在NSObject存储的对象方法。而系统在执行lookUpImpOrNil时,会递归查找元类的父类的方法列表。但是由于元类和根元类都是系统自动生成的,我们是无法直接编写它们,而对于NSObject,我们可以借助分类(Category)来实现统一的类方法动态解析,不过前提是类本身是没有实现resolveClassMethod方法。

这也就解释了为什么_class_resolveClassMethod为什么会多一步对象方法解析的流程了。

就大概理解这么多吧,可以看看这个大哥的iOS 消息发送、动态方法解析和消息转发 objc4-838.1源码最新的源码解析,太强了。

原文地址:https://blog.csdn.net/m0_55124878/article/details/125855155

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_15263.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注