引今早起床打开微信,发现知识小集推送了一篇文章《阿里、字节:一套高效的iOS面试题》,打开瞅了眼,看到第二题就给我看懵圈了,为什么要设计metaclass?在我的知识体系中关于元类的认知是类对象的isa指向元类对象,元类对象存储着类方法列表,然后就没有然后了。
带着这个疑问我边开始google了,找到一文Why is MetaClass in Objective-C?,该文很好的解释了OC面向对象能力的部分师承于Smalltalk,通过类的划分和消息传递两个亮点解释了为什么要有metaclass,但是我想仅仅通过设计层面解释恐怕打动不了面试官,如果面试官反问为什么OC要借鉴Smalltalk这门语言呢?毕竟咱对Smalltalk也不了解。
OK,既然元类的存在跟方法有关,那么我们就从方法的调用阶段入手。
源代码来自objc-750
__objc_msgSend1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 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 = classLGetIsaDone: CacheLookup NORMAL // calls imp or objc_msgSend_uncached#if SUPPORT_TAGGED_POINTERSLNilOrTagged: 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#endifLReturnZero: // 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流程。
CacheLookup123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109/******************************************************************** * * 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} = *bucket1: 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 // loop3: // 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} = *bucket1: 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 // loop3: // double wrap JumpMiss $0 .endmacro
之前的_objc_msgSend代码中我们知道CacheLookup走的是NORMAL流程,别的支线代码就忽略了从上述代码中可见得知当缓存命中时会调用TailCallCachedImp验证方法IMP的有效性并调用改方法的实现,如果缓存没有命中则进入__objc_msgSend_uncached流程。
关于缓存是如何缓存和寻找缓存的,后续会写篇blog进行详解。
__objc_msgSend_uncached1234567891011121314151617181920212223STATIC_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_lookupMethodAndLoadCache312345IMP _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的消息机制分析了元类存在的意义,元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。