在寫這篇文章之前,我關于方法調(diào)用的知識是比較零散的,甚至一度以為消息轉(zhuǎn)發(fā)就是方法調(diào)用的過程。現(xiàn)有的文章大多根據(jù)蘋果的官方文檔Runtime Programming Guide進行分析,一般包含這些內(nèi)容:
- 方法的調(diào)用會被轉(zhuǎn)換成objc_msgSend()
- 如果找不到方法的實現(xiàn),會開始執(zhí)行動態(tài)方法解析
- 如果動態(tài)方法解析失敗了,會啟動消息轉(zhuǎn)發(fā)
所以消息轉(zhuǎn)發(fā)應該只是方法調(diào)用中的一個步驟。這中間似乎缺了點什么,那就是:
- 在啟動消息轉(zhuǎn)發(fā)之前,objc_msgSend()做了什么?
這也就是本文將要解答的:方法究竟是如何被調(diào)用的?
方法的調(diào)用棧
在上一篇講方法加載的過程時,用過這么一張圖來講realizeClass()的調(diào)用棧:
當時調(diào)用的是類的class方法,在調(diào)用棧里有這么一個關鍵的方法:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
方法名字就是查找實現(xiàn)或者轉(zhuǎn)發(fā),看起來這就是我們要找的方法了。
沿用之前的TestObject類,再修改一下main函數(shù)的內(nèi)容,現(xiàn)在看起來是這個樣子的:
// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
- (void)hello;
@end
// TestObject.m
#import "TestObject.h"
@implementation TestObject
- (void)hello {
NSLog(@"hello");
}
@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestObject *testObj = [TestObject new];
[testObj hello];
}
return 0;
}
在[testObj hello]這一行添加一個斷點,運行程序進入斷點,這時候在lookUpImpOrForward()方法中添加斷點,繼續(xù)運行進入此方法:
左側的調(diào)用棧里面供包含了3層,按照調(diào)用的順序依次是:
- _objc_msgSend_uncached
- _class_lookupMethodAndLoadCache3(id, SEL, Class)
- lookUpImpOrForward(Class, SEL, id, bool, bool, bool)
一步步來看:
- _objc_msgSend_uncached
不對啊,官方文檔中說的是調(diào)用objc_msgSend,這個uncached是怎么回事。看看objc_msgSend:
...
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
CacheLookup NORMAL, CALL // calls IMP on success
NilTestReturnZero NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r10
MESSENGER_END_SLOW
jmp __objc_msgSend_uncached
END_ENTRY _objc_msgSend
...
源碼是匯編,說實話我是不太懂的,但沒關系,關注一下這一行:
jmp __objc_msgSend_uncached。
從注釋可以看到當cache miss的時候,會跳轉(zhuǎn)到uncached方法中,到底是不是這樣呢?重新運行程序,加個斷點測試一下:
(注意,這里也需要先運行進入main函數(shù)中[testObj hello]這一行之后再激活斷點)
沒有問題,調(diào)用棧顯示先進入了objc_msgSend,單步調(diào)試的圖我就不放了,感興趣的同學可以自己試一下,下面是過程:
- 先進入:CacheLookup NORMAL, CALL
- cache miss,跳到這里:jmp __objc_msgSend_uncached
- 進入:__objc_msgSend_uncached
這個時候調(diào)用棧的objc_msgSend已經(jīng)看不到了,取而代之的就是__objc_msgSend_uncached:
所以之前調(diào)用棧中的結果就可以理解了,這里也告訴了我們一個很重要的信息:在objc_msgSend最開始的地方就已經(jīng)通過cache進行過一次查找。
- _class_lookupMethodAndLoadCache3(id, SEL, Class)
現(xiàn)在斷點所在的行是這么一個方法:MethodTableLookup。看起來像是在方法列表里進行查找。沿著斷點繼續(xù)走,就會走到現(xiàn)在這個方面里面,這個方法的實現(xiàn)非常簡單:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
就是完善了一下lookUpImpOrForward()的參數(shù)。話不多說,看看最關鍵的一步。
- lookUpImpOrForward(Class, SEL, id, bool, bool, bool)
這個方法的實現(xiàn)有點長,我就不一起展示了,一步一步來分析:
part1
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
還記得前面說到的關鍵信息嗎,之所以傳入cache=NO就是因為在objc_msgSend()初期就已經(jīng)查找過cache了,不需要在這里再查找一次。這部分代碼主要做的是初始化的相關工作,這里不做擴展。接著往下:
part2
retry:
runtimeLock.read();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
加鎖這一部分只有一行簡單的代碼,其主要目的保證方法查找以及緩存填充(cache-fill)的原子性,保證在運行以下代碼時不會有新方法添加導致緩存被沖洗(flush)。
這里又一次使用cache進行查找。這里我是有點疑問的,在這個時候cache有可能會命中嗎?或者說在什么情況下才能在這里命中cache?
在上一篇方法加載的過程中提到,在realizeClass()方法深處會拷貝編譯期確定的方法同時添加category中的方法,難道這個過程改變了cache的內(nèi)容,所以需要在這里查一下cache?先不深究,等研究category的時候看看能不能有所進展。
cache_getImp()方法同樣是用匯編實現(xiàn)的:
STATIC_ENTRY _cache_getImp
// do lookup
movq %a1, %r10 // move class to r10 for CacheLookup
CacheLookup NORMAL, GETIMP // returns IMP on success
LCacheMiss:
// cache miss, return nil
xorl %eax, %eax
ret
END_ENTRY _cache_getImp
CacheLookup應該就是用來查找cache的,這里是首次調(diào)用hello()方法,所以肯定不會命中,繼續(xù)向下。
part3
// Try this class's method lists.
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
在當前類的方法列表中查找,因為hello()就是當前類的方法,所以在這一步會命中,命中時候的調(diào)用棧是這樣的:
中間的方法都比較簡單,我就不把源代碼一一貼上來了,稍微說一下每個方法做了些什么:
- getMethodNoSuper_nolock(Class cls, SEL sel)
遍歷class的methods列表,依次調(diào)用下一個方法 - search_method_list(const method_list_t *mlist, SEL sel)
如果是無序列表,直接匹配名字,成功則返回
如果是有序列表,調(diào)用下一個方法 - findMethodInSortedMethodList(SEL key, const method_list_t *list)
匹配方法名,成功就直接返回
這些做完之后,會調(diào)用log_and_fill_cache()把方法加入緩存,這個方法的調(diào)用棧是這樣的:
在cache_fill_nolock()方法中把當前調(diào)用的方法加入到cache中:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
if (!cls->isInitialized()) return;
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
注釋還是很清楚的,在cache已經(jīng)3/4滿的時候,就會調(diào)用expand()方法擴充,這樣可以保證cache一直都是有空位的:
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
中間的if判斷是對溢出情況的處理。正常情況下,expand方法會將容量翻倍,通過調(diào)用reallocate方法給cache重新分配內(nèi)存,但出于性能考慮不會將老cache中的內(nèi)容拷貝到新cache中。
這里插一點題外話,如果對swift沒興趣就跳過吧。這里的操作讓我想起了swift中map的實現(xiàn):
public func map<T>(
_ transform: (Iterator.Element) throws -> T
) rethrows -> [T] {
let initialCapacity = underestimatedCount
var result = ContiguousArray<T>()
result.reserveCapacity(initialCapacity)
var iterator = self.makeIterator()
// Add elements up to the initial capacity without checking for regrowth.
for _ in 0..<initialCapacity {
result.append(try transform(iterator.next()!))
}
// Add remaining elements, if any.
while let element = iterator.next() {
result.append(try transform(element))
}
return Array(result)
}
里面有這么一行:
result.reserveCapacity(initialCapacity)
就是先直接申請了一段空間用來存放結果,滿了之后才需要檢查是否需要擴充,所以result.append()操作才會分成兩部分來做,應該也是出于性能的考慮。
part4
因為hello()方法已經(jīng)在上一步找到了,所以走不到下面的代碼了,但還是可以看一看:
// Try superclass caches and method lists.
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
這一塊還是很好理解的,就是在父類的緩存和方法列表中查找,邏輯跟前面兩步基本一樣,就不再細說了。只需要注意一點,在父類中找到的方法,也會被添加到當前類的cache中。
part5
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
如果當前類和父類都找不到方法的實現(xiàn),就進入了動態(tài)方法解析。這里面調(diào)用了_class_resolveMethod()方法,看看是怎么實現(xiàn)的:
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
還是很清楚的,如果類不是元類,調(diào)用_class_resolveInstanceMethod(),是元類則調(diào)用_class_resolveClassMethod()。這兩個方法很類似,就以第一個為例,注意看我添加的注釋:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 查找類是否實現(xiàn)了+ (BOOL)resolveInstanceMethod:(SEL)sel方法
// 如果沒有實現(xiàn)就直接返回
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
// 調(diào)用類里面實現(xiàn)的+ (BOOL)resolveInstanceMethod:(SEL)sel
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
...(略去了一些代碼,主要是驗證是否添加成功)
}
關于+ (BOOL)resolveInstanceMethod:(SEL)sel方法,這里就不細說了,有非常多的文章講解了這個方法該怎么寫,如果曾經(jīng)看過,就會知道在這個方面里面通常都會調(diào)用:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
通過這個方法來給某個方法添加新的實現(xiàn)。在這個方法內(nèi)部,有這么一行:
cls->data()->methods.attachLists(&newlist, 1);
將新的方法實現(xiàn)添加到了方法列表里面。這就完成了整個動態(tài)方法解析的過程。
這個時候回到part5最開始的地方,在調(diào)用完_class_resolveMethod()方法之后,有一步goto retry,就是回到part2重新開始,只不過這個時候在類的方法列表里面就可以找到這個方法了。
part6
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
如果上一步依然沒有解決問題,還有最后一個辦法:消息轉(zhuǎn)發(fā)。這個過程實在是太復雜,簡單一點來說,如果你的類實現(xiàn)了這個方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這個時候就會進到這個方法里面,在這里可以轉(zhuǎn)發(fā)給其他對象進行處理。如果消息轉(zhuǎn)發(fā)也失敗了,那么這次方法的調(diào)用就失敗了。
如果想要對消息轉(zhuǎn)發(fā)的全部過程有更深刻的理解,可以參考這篇文章,講的很詳細:
緩存命中
上面講了那么多,前提是objc_msgSend匯編代碼中的的緩存沒有命中,如果在最開始緩存就命中了,會怎么樣呢?
想要測試命中緩存很簡單,把方法連續(xù)調(diào)用兩次就可以了,第二次調(diào)用的時候上面那些方法都不會被調(diào)用到,直接就把hello()方法的log打印出來了。
總結
最后匯總一下正常方法調(diào)用的過程,總的來看還是很合情合理的:
- 查找當前類的緩存和方法列表
- 查找父類的緩存和方法列表
- 動態(tài)方法解析
- 消息轉(zhuǎn)發(fā)