在寫這篇文章之前,我關于方法調用的知識是比較零散的拍鲤,甚至一度以為消息轉發(fā)就是方法調用的過程》芍鳎現(xiàn)有的文章大多根據蘋果的官方文檔Runtime Programming Guide進行分析,一般包含這些內容:
- 方法的調用會被轉換成objc_msgSend()
- 如果找不到方法的實現(xiàn)淤袜,會開始執(zhí)行動態(tài)方法解析
- 如果動態(tài)方法解析失敗了毒租,會啟動消息轉發(fā)
所以消息轉發(fā)應該只是方法調用中的一個步驟稚铣。這中間似乎缺了點什么,那就是:
- 在啟動消息轉發(fā)之前墅垮,objc_msgSend()做了什么惕医?
這也就是本文將要解答的:方法究竟是如何被調用的?
方法的調用棧
在上一篇講方法加載的過程時算色,用過這么一張圖來講realizeClass()的調用棧:
當時調用的是類的class方法曹锨,在調用棧里有這么一個關鍵的方法:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
方法名字就是查找實現(xiàn)或者轉發(fā),看起來這就是我們要找的方法了剃允。
沿用之前的TestObject類,再修改一下main函數的內容齐鲤,現(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ù)運行進入此方法:
左側的調用棧里面供包含了3層牡肉,按照調用的順序依次是:
- _objc_msgSend_uncached
- _class_lookupMethodAndLoadCache3(id, SEL, Class)
- lookUpImpOrForward(Class, SEL, id, bool, bool, bool)
一步步來看:
- _objc_msgSend_uncached
不對啊,官方文檔中說的是調用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的時候逾雄,會跳轉到uncached方法中,到底是不是這樣呢腻脏?重新運行程序鸦泳,加個斷點測試一下:
(注意,這里也需要先運行進入main函數中[testObj hello]這一行之后再激活斷點)
沒有問題永品,調用棧顯示先進入了objc_msgSend做鹰,單步調試的圖我就不放了,感興趣的同學可以自己試一下鼎姐,下面是過程:
- 先進入:CacheLookup NORMAL, CALL
- cache miss钾麸,跳到這里:jmp __objc_msgSend_uncached
- 進入:__objc_msgSend_uncached
這個時候調用棧的objc_msgSend已經看不到了更振,取而代之的就是__objc_msgSend_uncached:
所以之前調用棧中的結果就可以理解了,這里也告訴了我們一個很重要的信息:在objc_msgSend最開始的地方就已經通過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()的參數帖池。話不多說奈惑,看看最關鍵的一步。
- 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()初期就已經查找過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的內容搅轿,所以需要在這里查一下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的,這里是首次調用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()就是當前類的方法褐澎,所以在這一步會命中,命中時候的調用棧是這樣的:
中間的方法都比較簡單伐蒋,我就不把源代碼一一貼上來了工三,稍微說一下每個方法做了些什么:
- getMethodNoSuper_nolock(Class cls, SEL sel)
遍歷class的methods列表迁酸,依次調用下一個方法 - search_method_list(const method_list_t *mlist, SEL sel)
如果是無序列表,直接匹配名字俭正,成功則返回
如果是有序列表奸鬓,調用下一個方法 - findMethodInSortedMethodList(SEL key, const method_list_t *list)
匹配方法名,成功就直接返回
這些做完之后掸读,會調用log_and_fill_cache()把方法加入緩存串远,這個方法的調用棧是這樣的:
在cache_fill_nolock()方法中把當前調用的方法加入到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已經3/4滿的時候儿惫,就會調用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方法會將容量翻倍留搔,通過調用reallocate方法給cache重新分配內存,但出于性能考慮不會將老cache中的內容拷貝到新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()方法已經在上一步找到了,所以走不到下面的代碼了捐下,但還是可以看一看:
// 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)方法解析档叔。這里面調用了_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);
}
}
}
還是很清楚的,如果類不是元類衙四,調用_class_resolveInstanceMethod()铃肯,是元類則調用_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;
// 調用類里面實現(xiàn)的+ (BOOL)resolveInstanceMethod:(SEL)sel
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
...(略去了一些代碼步藕,主要是驗證是否添加成功)
}
關于+ (BOOL)resolveInstanceMethod:(SEL)sel方法,這里就不細說了挑格,有非常多的文章講解了這個方法該怎么寫咙冗,如果曾經看過,就會知道在這個方面里面通常都會調用:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
通過這個方法來給某個方法添加新的實現(xiàn)漂彤。在這個方法內部雾消,有這么一行:
cls->data()->methods.attachLists(&newlist, 1);
將新的方法實現(xiàn)添加到了方法列表里面。這就完成了整個動態(tài)方法解析的過程显歧。
這個時候回到part5最開始的地方仪或,在調用完_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);
如果上一步依然沒有解決問題拷肌,還有最后一個辦法:消息轉發(fā)到旦。這個過程實在是太復雜,簡單一點來說巨缘,如果你的類實現(xiàn)了這個方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這個時候就會進到這個方法里面添忘,在這里可以轉發(fā)給其他對象進行處理。如果消息轉發(fā)也失敗了若锁,那么這次方法的調用就失敗了搁骑。
如果想要對消息轉發(fā)的全部過程有更深刻的理解,可以參考這篇文章又固,講的很詳細:
緩存命中
上面講了那么多仲器,前提是objc_msgSend匯編代碼中的的緩存沒有命中,如果在最開始緩存就命中了仰冠,會怎么樣呢乏冀?
想要測試命中緩存很簡單,把方法連續(xù)調用兩次就可以了洋只,第二次調用的時候上面那些方法都不會被調用到辆沦,直接就把hello()方法的log打印出來了。
總結
最后匯總一下正常方法調用的過程识虚,總的來看還是很合情合理的:
- 查找當前類的緩存和方法列表
- 查找父類的緩存和方法列表
- 動態(tài)方法解析
- 消息轉發(fā)