余额不足.

为什么要设计metaclass

字数统计: 1.9k阅读时长: 8 min
2020/02/28 Share

今早起床打开微信,发现知识小集推送了一篇文章《阿里、字节:一套高效的iOS面试题》,打开瞅了眼,看到第二题就给我看懵圈了,为什么要设计metaclass?在我的知识体系中关于元类的认知是类对象的isa指向元类对象,元类对象存储着类方法列表,然后就没有然后了。

带着这个疑问我边开始google了,找到一文Why is MetaClass in Objective-C?,该文很好的解释了OC面向对象能力的部分师承于Smalltalk,通过类的划分和消息传递两个亮点解释了为什么要有metaclass,但是我想仅仅通过设计层面解释恐怕打动不了面试官,如果面试官反问为什么OC要借鉴Smalltalk这门语言呢?毕竟咱对Smalltalk也不了解。

OK,既然元类的存在跟方法有关,那么我们就从方法的调用阶段入手。

源代码来自objc-750

__objc_msgSend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	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

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

源码中给了部分注释,不愿看代码的直接看下面的流程吧
1、进入_objc_msgSend后首先判断消息的接受者是否为nil或者是否使用了tagPointer技术,由于本文是为了探究META-CLASS存在的意义,所以关于tagPointer的东西就直接忽略了。
2、根据消息接受者的isa指针找到metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
3、进入CacheLookup流程,这一步会去寻找方法缓存,如果缓存命中则直接调用方法的实现,如果缓存不存在则进入objc_msgSend_uncached流程。

CacheLookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/********************************************************************
*
* 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
*
********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro

.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

.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.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

之前的_objc_msgSend代码中我们知道CacheLookup走的是NORMAL流程,别的支线代码就忽略了
从上述代码中可见得知当缓存命中时会调用TailCallCachedImp验证方法IMP的有效性并调用改方法的实现,如果缓存没有命中则进入__objc_msgSend_uncached流程。

关于缓存是如何缓存和寻找缓存的,后续会写篇blog进行详解。

__objc_msgSend_uncached

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

// MethodTableLookup
.macro MethodTableLookup

*这里忽略寄存器的操作*

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

*这里忽略寄存器的操作*

.endmacro

一通操作后从后面调用到了_class_lookupMethodAndLoadCache3这个方法,该方法在objc_runtim_new.mm文件中,终于从汇编代码中走了出来!

__class_lookupMethodAndLoadCache3

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

该方法会去调用lookUpImpOrForward,由于lookUpImpOrForward方法篇幅有点长,这里简述一下该方法的流程。
1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
4、如果到基类依然没有找到该方法则触发动态方法解析流程。
5、还是找不到就触发消息转发流程

走到这里一套方法发送的流程就都走完了,那这跟元类的存在有啥关系?我们都知道类方法是存储在元类中的,那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?

答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。

倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, …)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的…就是各式各样的参数。

通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。

总结

本文从OC的消息机制分析了元类存在的意义,元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。

CATALOG
  1. 1.
    1. 1.1. __objc_msgSend
    2. 1.2. CacheLookup
    3. 1.3. __objc_msgSend_uncached
    4. 1.4. __class_lookupMethodAndLoadCache3
  2. 2. 总结