在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
只要用到Objective-C,我们每天都会跟方法调用打交道。我们都知道Objective-C的方法决议是动态的,但是在底层一个方法究竟是怎么找到的,方法缓存又是怎么运作的却鲜为人知。 本文主要从源码角度探究了Objective-C在runtime层的方法决议(Method resolving)过程和方法缓存(Method cache)的实现。 介绍本文系学习Objective-C的runtime源码时整理所成,主要剖析了Objective-C在runtime层的方法决议过程和方法缓存。 我们都知道,在Objective-C里调用一个方法是这样的: [object methodA];
这表示我们想去调用object的methodA。 objc_msgSend(id self, SEL op, ...) 而objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。 ENTRY objc_msgSend # check whether receiver is nil teq a1, #0 beq LMsgSendNilReceiver # save registers and load receiver's class for CacheLookup stmfd sp!, {a4,v1} ldr v1, [a1, #ISA] # receiver is non-nil: search the cache CacheLookup a2, v1, LMsgSendCacheMiss # cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call ldmfd sp!, {a4,v1} bx ip # cache miss: go search the method lists LMsgSendCacheMiss: ldmfd sp!, {a4,v1} b _objc_msgSend_uncached LMsgSendNilReceiver: mov a2, #0 bx lr LMsgSendExit: END_ENTRY objc_msgSend STATIC_ENTRY objc_msgSend_uncached # Push stack frame stmfd sp!, {a1-a4,r7,lr} add r7, sp, #16 # Load class and selector ldr a3, [a1, #ISA] /* class = receiver->isa */ /* selector already in a2 */ /* receiver already in a1 */ # Do the lookup MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3) MOVE ip, a1 # Prep for forwarding, Pop stack frame and call imp teq v1, v1 /* set nonstret (eq) */ ldmfd sp!, {a1-a4,r7,lr} bx ip 从上述代码中可以看到,objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:
缓存从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。 for ( int i = 0; i < 100000; ++i) { MyClass *myObject = myObjects[i]; [myObject methodA]; } 当我们需要去调用一个方法数十万次甚至更多地时候,查找方法的消耗会变的非常显著。 追本溯源,何为方法缓存本着源码面前,了无秘密的原则,我们看下源码中的方法缓存到底是什么,在objc-cache.mm中,objc_cache的定义如下: struct objc_cache { uintptr_t mask; /* total = mask + 1 */ uintptr_t occupied; cache_entry *buckets[1]; }; 嗯,objc_cache的定义看起来很简单,它包含了下面三个变量: 而cache_entry的定义如下: typedef struct { SEL name; // same layout as struct old_method void *unused; IMP imp; // same layout as struct old_method } cache_entry; cache_entry定义也包含了三个字段,分别是: 缓存和散列缓存的存储使用了散列表。 // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. buckets = (cache_entry **)cache->buckets; for (index = CACHE_HASH(sel, cache->mask); buckets[index] != NULL; index = (index+1) & cache->mask) { // empty } buckets[index] = entry; 这是往方法缓存里存放一个方法的代码片段,我们可以看到sel被散列后找到一个空槽放在buckets中,而CACHE_HASH的定义如下: #define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
这段代码就是利用了sel的指针地址和mask做了一下简单计算得出的。 .macro CacheLookup /* selReg, classReg, missLabel */ MOVE r9, $0, LSR #2 /* index = (sel >> 2) */ ldr a4, [$1, #CACHE] /* cache = class->cache */ add a4, a4, #BUCKETS /* buckets = &cache->buckets */ /* search the cache */ /* a1=receiver, a2 or a3=sel, r9=index, a4=buckets, $1=method */ 1: ldr ip, [a4, #NEGMASK] /* mask = cache->mask */ and r9, r9, ip /* index &= mask */ ldr $1, [a4, r9, LSL #2] /* method = buckets[index] */ teq $1, #0 /* if (method == NULL) */ add r9, r9, #1 /* index++ */ beq $2 /* goto cacheMissLabel */ ldr ip, [$1, #METHOD_NAME] /* load method->method_name */ teq $0, ip /* if (method->method_name != sel) */ bne 1b /* retry */ /* cache hit, $1 == method triplet address */ /* Return triplet in $1 and imp in ip */ ldr ip, [$1, #METHOD_IMP] /* imp = method->method_imp */ .endmacro 虽然是汇编,但是注释太详尽了,理解起来并不难,还是求hash,去buckets里找,找不到按照hash冲突的规则继续向下,直到最后。 为什么了解了方法缓存的定义之后,我们提出几个问题并一一解答
缓存 - 性能优化的万金油?非也,就算有了有了Objective-C本身的方法缓存,我们还是有很多调用方法的优化空间,对于这件事情,这篇文章讲的非常详细,大家可以自行移步观摩http://www.mulle-kybernetik.com/artikel/Optimization/opti-3-imp-deluxe.html (强烈推荐,虽然我们一般不会遇到需要这么强度优化的地方,但是这种精神和思想是值得我们学习的) |
请发表评论