面试遇到Runtime的第三天-消息转发

825 阅读10分钟

如果阅读过前面的两篇文章,我相信你一定对runtime有了一些自己的理解。本文就要切入正题,所谓Objective-C是一个动态的语言,他的主要核心就是消息转发和传递。所以我们了解runtime,也一定要明白他的核心(消息转发)的实现原理。

消息

对于消息这个概念呢,我们可以简单理解为:调用一个对象的方法,就是给这个对象发消息。
比如下面的两行代码是等价的,array调用insert方法,并不会立即执行insert,而是在运行时给array发送了一条insert消息,至于这个消息是否会由array执行,或者是否能执行,都要在运行的时候才能决定。

[array insertObject:foo atIndex:5];

objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

所以,消息传递的关键就在于objc_msgSend这个方法的定义

消息传递

在message.h文件中找到objc_msgSend的定义,这是一个可变参数函数,但是有两个默认的参数,这两参数是在编译期自动生成的。

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
  • self: 调用当前方法的对象
  • _cmd 被调用方法的SEL

他的第二个参数类型是SEL,SEL在OC中就是selector方法选择器

typedef struct objc_selector *SEL;

所有消息传递中的消息都会被转换成一个objc_selector作为objc_msgSend的参数,所以,OC中方法调用的过程,就是利用objc_msgSend向对象发送消息,然后根据发送的SEL找到具体的函数实现IMP,最后调用。

这里有几个知识点需要注意:

  1. 实例方法:objc_msgSend(对象,sel)
  1. 类方法:objc_msgSend(类,sel)
  1. 父类:则是使用objc_msgSendSuper ,super和self指向一个对象,但是self是类隐藏参数,而super只是预编译指令符号,作用就是不经过本类的方法列表,直接通过superClass的方法列表去查找,然后利用本身(objc_super->receiver)去调用

关于objc_msgSendSuper的调用在runtime的第一篇文章中的实战中就分析过,可以再回头重温一下

IMP

IMP本质上就是一个函数指针,在IMP中有两个默认的参数id和SEL,和objc_msgSend()函数的参数一样。

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

runtime提供了很多对于IMP的操作API,比如我们比较熟悉的method swizzling就是通过这些API实现的。

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
OBJC_EXPORT void
method_getArgumentType(Method _Nonnull m, unsigned int index, 
                       char * _Nullable dst, size_t dst_len) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
...

获取IMP

通过定义在NSObject中的方法,可以获取IMP,不仅实例对象可以调用,类对象也同样可以调用。

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

SEL(objc_selector)

objc_selector是一个映射到方法的C字符串。需要注意的是@selector()选择子只与函数名有关。即使方法名字相同而参数类型不同或是参数个数不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了OC不支持函数重载
在runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL都会被看作是一个,所以,不同类中同名的方法也是同一个SEL

method_t

源码中查看method的定义,同样也是一个结构体,其中包含了IMP,可以看到只记录了方法名字并没有方法参数,也证实了上面说的选择子只与函数名有关

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

这里还可能会被问到一个面试题:
问:申明在.h文件中,而在.m中没有实现中的方法,会被编译到method list中嘛?
答:不会,编译时只会将 Xcode中Compile Sources中.m文件声明的方法编译到method list中。

objc_msgSend

objc_msgSend函数究竟会干什么事情呢?

总结一下objc_msgSend会做以下几件事情:

  1. 检测这个 selector是不是要忽略的。
  2. 检查target是不是为nil。

如果这里有相应的nil的处理函数,就跳转到相应的函数中。 如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因

  1. 确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。

如果找到,就跳转进去执行。 如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止。

  1. 如果还没有找到,那就需要开始消息转发阶段了。至此,消息传递阶段完成。这一阶段主要完成的是通过SEL快速查找IMP的过程。

objc_msgSend函数源码解析

我们以objc-msg-arm64.s文件为例,他的实现其实是一段汇编代码,一眼看过去懵懵的,这里其实没有必要完全看懂(可能完全看不懂的概率更大),所以我们就看几个关键词理解它的大概流程就好了。
搜索一下msgSend这个关键词,就可以找到我们想要的那部分代码(这里注释写的还是比较好的)

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************/

#if SUPPORT_TAGGED_POINTERS
	.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
#endif

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

还是主要看注释吧。。。(手动尴尬)
可以看到这里做了我们上面说的,检查target是不是为nil,确定不是nil之后,在该class的缓存中查找方法对应的IMP实现,就是 CacheLookup

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *	 x1 = selector
 *	 x16 = class to be searched
 *
 * Kills:
 * 	 x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/
 .macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro

这里就要用到面试遇到Runtime的第二天-isa和meta-Class中说到的objc_class结构体中的cache部分,为了提高方法查找的效率会先在缓存中查找,如果未命中就跳入CheckMiss(都是汇编代码实现,就不展开说了,了解过程就行,根据前面传入的参数NORMAL,知道下一步进入_objc_msgSend_uncached),如果命中就执行CacheHit,返回IMP。

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

而_objc_msgSend_uncached中主要就是调用了MethodTableLookup以及回调一个方法指针

.macro MethodTableLookup
	
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]

	// receiver and selector already in x0 and x1
	mov	x2, x16
	bl	__class_lookupMethodAndLoadCache3

	// IMP in x0
	mov	x17, x0
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro

MethodTableLookup中的主要工作是去调用__class_lookupMethodAndLoadCache3,这个方法的定义在objc-runtime-new.mm中

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

由于实现的查找方法 lookUpImpOrForward 涉及很多函数的调用,所以我们就不一一贴代码了,总结一下流程,捡重要的点说明一下具体调用:

  1. 无锁的缓存查找(因为参数cache传入NO,所以这里是直接跳过的)
  2. 如果类没有实现(isRealized)或者初始化(isInitialized),实现或者初始化类(贴一点儿精简版的代码)

在这个过程中,会把编译器存储在bits(objc_class中定义的bits,还不熟悉的可以看下上一篇文章)中的class_ro_t取出来,然后创建class_rw_t,并把ro赋值给rw,成为rw的一个成员变量,最后把rw设置给bits,替代之前bits中的ro

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;

    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }

    isMeta = ro->flags & RO_META;

    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6

    cls->chooseClassArrayIndex();

    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));

    cls->superclass = supercls;
    cls->initClassIsa(metacls);
    cls->setInstanceSize(ro->instanceSize);

    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    methodizeClass(cls);

    return cls;
}

在上面源码中还有两个函数addSubclass和addRootClass,这两个函数是把当前类的子类串成一个列表,所以,我们是可以通过class_rw_t获取到当前类的所有子类。

初始化rw之后,rw的method list ,protocol list,property list都是空的,需要调用methodizeClass函数进行赋值。函数中会把ro中的list取出赋值给rw,我们在运行时动态修改的就是rw.所以ro中存储的是编译时决定的原数据,rw才是运行时修改的数据。

static void methodizeClass(Class cls)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}
  1. 加锁
  2. 缓存以及当前类中方法的查找
  3. 尝试查找父类的缓存以及方法列表
  4. 没有找到实现,尝试方法解析器
  5. 进行消息转发
  6. 解锁、返回实现

动态方法解析

当一个方法没有实现时,也就是在cache list和继承关系的method list中,都没有找到对应的方法,这时会进入到消息转发阶段。
在进入转发阶段前,会有一次动态添加方法实现的机会:通过重写下面两个方法,动态添加实例方法和类方法,这两方法如果都返回NO,则进入消息转发。

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

在进行消息转发前,还可以通过forwardingTargetForSelector:方法,将消息转发给其他对象。

 - (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorName = NSStringFromSelector(aSelector); if ([selectorName isEqualToString:@"selector"]) {
        return object;
    }
    return [super forwardingTargetForSelector:aSelector]; 
     
 }

消息转发

如果forwardingTargetForSelector:方法未实现,会进入消息转发流程。

  1. 生成方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

2.通过forwardInvocation将消息转发给另外一个对象,该方法接收一个NSInvocation类型的参数, NSInvocation对象中包含原始消息及参数

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:someOtherObject]; 
        
    } else {
        [super forwardInvocation:anInvocation]; }

}

调用invokeWithTarget方法后,原方法的返回值将被返回给调用方。

模拟多继承

可以通过消息转发机制来模拟多继承,分属不同继承系统里面的两个类,通过消息转发,将实现重定向到其他类中,以实现多继承。