深入理解Objective-C:方法缓存

2018-07-26  本文已影响0人  michaelJackDong

简介:本文主要从源码的角度探究了Objective-C在runtime层的方法决议(Method resolveing)过程和方法缓存(Method cache)的实现。内容包括:
1)从消息决议说起 2)缓存为谁而生 3)何为方法缓存 4)缓存和散列 5)十万个为什么 6)缓存-性能优化的万金油? 7)优化永无止境
一、从消息决议说起
我们都知道,在Objective-C里调用一个方法是这样的:
[object methodA];
这表示我们想去调用object的methodA。
但是在Objective-C里面调用一个方法到底意味着什么呢,是否和C++一样,任何一个非虚方法都会被编译成一个唯一的符号,在调用的时候去查找符号表,找到这个方法然后调用呢?
答案是否定的。在Objective-C里面调用一个方法的时候,runtime层会将这个调用翻译成
objc_msgSend(id self, SEL op, ...)
而objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。
在objc-msg-arm.s中,objc_msgSend的代码如下:
(ps:Apple为了高度优化objc_msgSend的性能,这个文件是汇编写成的,不过即使我们不懂汇编,详尽的注释也可以让我们一窥其真面目)
从源码代码中可以看到,objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:
*判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象

define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))

这段代码就是利用了sel的指针地址和mask做了一下简单计算得出的。
而从散列表取缓存则是利用汇编语言写成的(是为了高度优化objc_msgSend而使用汇编的)。我们看objc-msg-arm.mm 里面的CacheLookup方法:
.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 */ ldr1, [a4, r9, LSL #2] /* method = buckets[index] /
teq 1, #0 /* if (method == NULL) */ add r9, r9, #1 /* index++ */ beq2 /
goto cacheMissLabel */

ldr ip, [1, #METHOD_NAME] /* load method->method_name */ teq0, ip /* if (method->method_name != sel) /
bne 1b /
retry */

/* cache hit, 1 == method triplet address */ /* Return triplet in1 and imp in ip /
ldr ip, [$1, #METHOD_IMP] /
imp = method->method_imp */

.endmacro
虽然是汇编,但是注释太详尽了,理解起来并不难,还是求hash,去buckets里找,找不到按照hash冲突的规则继续向下,直到最后。
五、十万个为什么
1)方法缓存在什么地方?
让我们去翻看类的定义,在Objective-C 2.0中,Class的定义大概是这样的(见objc-runtime.mm)
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t ro;
};
我们看到在类的定义里有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。
2)父类方法的缓存只存在父类么,还是子类也会缓存父类的方法?
在第一节对objc_msgSend的追溯中我们可以看到,即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份
3)类的方法缓存大小有没有限制?
要回答这个问题,我们需要再看一下源码,在objc-cache.mm有一个变量定义如下:
/
When _class_slow_grow is non-zero, any given cache is actually grown

六、缓存 - 性能优化的万金油?
非也,就算有了有了Objective-C本身的方法缓存,我们还是有很多调用方法的优化空间,对于这件事情,这篇文章讲的非常详细,大家可以自行移步观摩
http://www.mulle-kybernetik.com/artikel/Optimization/opti-3-imp-deluxe.html(强烈推荐,虽然我们一般不会遇到需要这么强度优化的地方,但是这种精神和思想是值得我们学习的)
七、优化,永无止境
在文章末尾,我们再来回答一下第一节提出的问题:“为什么会有_class_lookupMethodAndLoadCache3这个方法?”
这个方法的实现如下所示:
/***********************************************************************

后记:
本文是Objective-C runtime源码研究的第二篇,主要对Objective-C的方法决议和方法缓存做了剖析。runtime的源代码可以在http://www.opensource.apple.com/tarballs/下载。如有错误,敬请指正。

上一篇 下一篇

猜你喜欢

热点阅读