歡迎閱讀iOS探索系列(按序閱讀食用效果更加)
寫在前面
書接上文說到cache_t
快取的是方法,那麼方法又是什麼呢?這一切都要從Runtime
開始說起
一、Runtime
1.什麼是Runtime?
Runtime
是一套API
,由c、c++、彙編
一起寫成的,為OC
提供了執行時
- 執行時:程式碼跑起來,將可執行檔案裝載到記憶體
- 編譯時:正在編譯的時間——翻譯原始碼將高階語言(OC、Swift)翻譯成機器語言(彙編等),最後變成二進位制
2.Runtime版本
Runtime有兩個版本——Legacy
和Modern
,蘋果開發者文件都寫得清清楚楚
原始碼中-old
、__OBJC__
代表Legacy
版本,-new
、__OBJC2__
代表Modern
版本,以此做相容
3.Runtime的作用及呼叫
Runtime
底層經過編譯會提供一套API和供FrameWork
、Service
使用
Runtime
呼叫方式:
- Runtime API,如 sel_registerName()
- NSObject API,如 isKindOf()
- OC上層方式,如 @selector()
原來平常在用的這麼多方法都是Runtime啊,那麼方法究竟是什麼呢?
二、方法的本質
1.研究方法
通過clang編譯成cpp檔案
可以看到底層程式碼,得到方法的本質
- 相容編譯(程式碼少):clang -rewrite-objc main.m -o main.cpp
- 完整編譯(不報錯):xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
2.程式碼轉換
FXPerson *p = [FXPerson alloc];
[p fly];
複製程式碼
FXPerson *p = ((FXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FXPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("fly"));
複製程式碼
((FXPerson *(*)(id, SEL))(void *)
是型別強轉(id)objc_getClass("FXPerson")
獲取FXPerson類物件sel_registerName("alloc")
等同於@selector()
那麼可以理解為((型別強轉)objc_msgSend)(物件, 方法呼叫)
3.方法的本質
方法的本質是通過objc_msgSend
傳送訊息,id
是訊息接收者,SEL
是方法編號
如果外部定義了C函式並呼叫如
void fly() {}
,在clang編譯之後還是fly()
而不是通過objc_msgSend
去呼叫。因為傳送訊息就是找函式實現的過程,而C函式可以通過函式名
——指標
就可以找到
4.向不同物件傳送訊息
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface FXFather: NSObject
- (void)walk;
+ (void)run;
@end
@implementation FXFather
- (void)walk { NSLog(@"%s",__func__); }
+ (void)run { NSLog(@"%s",__func__); }
@end
@interface FXSon: FXFather
- (void)jump;
+ (void)swim;
@end
複製程式碼
子類FXSon
有例項方法jump
、類方法swim
父類FXFather
有例項方法walk
、類方法run
①傳送例項方法
訊息接收者——例項物件
FXSon *s = [FXSon new];
objc_msgSend(s, sel_registerName("jump"));
複製程式碼
②傳送類方法
訊息接收者——類物件
objc_msgSend(objc_getClass("FXSon"), sel_registerName("swim"));
複製程式碼
objc_msgSend
不能向父類傳送訊息,需要使用objc_msgSendSuper
,並給objc_super
結構體賦值(在objc2中只需要賦值receiver
、super_class)
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
複製程式碼
③向父類傳送例項方法
receiver——例項物件
;super_class——父類類物件
struct objc_super superInstanceMethod;
superInstanceMethod.receiver = s;
superInstanceMethod.super_class = objc_getClass("FXFather");
objc_msgSendSuper(&superInstanceMethod, sel_registerName("walk"));
複製程式碼
④向父類傳送類方法
receiver——類物件
;super_class——父類元類物件
struct objc_super superClassMethod;
superClassMethod.receiver = [s class];
superClassMethod.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&superClassMethod, sel_registerName("run"));
複製程式碼
如果出現Too many arguments to function call, expected 0, have 2
問題,來到BuildSetting
把配置修改成如下圖
三、訊息查詢流程
訊息查詢流程其實是通過上層的
方法編號sel
傳送訊息objc_msgSend
找到具體實現imp
的過程
objc_msgSend
是用匯編寫成的,至於為什麼不用C而是用匯編寫,是因為:
- C語言不能通過寫一個函式,保留未知的引數,跳轉到任意的指標,而彙編有暫存器
- 對於一些呼叫頻率太高的函式或操作,使用匯編來實現能夠提高效率和效能,容易被機器來識別
1.開始查詢
開啟objc
原始碼,由於主要研究arm64結構
的彙編實現,來到objc-msg-arm64.s
①開始objc_msgSend
②判斷訊息接收者
是否為空,為空直接返回
③判斷tagged_pointers
(之後會講到)
④取得物件中的isa
存一份到p13
中(暫存器指令在逆向篇中會講到)
⑤根據isa
進行mask
地址偏移得到對應的上級物件
(類、元類)
GetClassFromIsa_p16
定義,主要就是進行isa & mask
得到class
操作
(其定義方式與iOS探索 isa初始化&指向分析一文中提到的shiftcls
異曲同工)
⑥開始在快取中查詢imp
——開始了快速流程
2.快速流程
從CacheLookup
開始了快速查詢流程(此時x0是sel
,x16是class
)
#CACHE
是個巨集定義表示16個位元組,[x16, #CACHE]
表示類物件
記憶體地址偏移16位元組
得到cache
。cache
一分為二——8位元組的buckets
存放在p10,兩個4位元組的occupied
和mask
存放在p11
#define CLASS __SIZEOF_POINTER__
#define CACHE (2 * __SIZEOF_POINTER__)
複製程式碼
②x1是sel即cmd
,取出p11中的低32位(w11)——mask
,兩者進行與運算得到hash下標
存放在x12
③p12先左移動(1+PTRSHIFT)
,再與p10buckets
相加得到新的p12——bucket
④拿出p12bucket
地址所在的值,放在p17imp
和p9sel
中,這點可以從bucket_t
的結構中看出(sel強轉成key)用bucket
中的sel
與x1cmd
作對比,如果相同則快取命中CacheHit
得到其中的imp
;如果不等就跳轉⑤
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
...
}
複製程式碼
⑤如果bucket->sel == 0
則CheckMiss
;比較p12bucket
和p10buckets
,如果不相等就將x12bucket
的值進行自減操作(查詢上一個bucket
),跳轉回④重新迴圈,直到bucket == buckets
遍歷結束跳轉⑥
.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
複製程式碼
⑥平移雜湊使得p12 = first bucket
,再重複進行一下類似④⑤⑥的操作——
防止不斷迴圈的過程中多執行緒併發,正好快取更新了。如果bucket->sel == 0
走CheckMiss
,如果bucket == buckets
走JumpMiss
,本質是一樣的
.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
複製程式碼
當NORMAL
時,CheckMiss
和JumpMiss
都走__objc_msgSend_uncached
⑦__objc_msgSend_uncached
呼叫MethodTableLookup
⑧儲存引數呼叫c++方法進入慢速流程(準備好裝備和藥水打BOSS)
總結:訊息查詢的快速流程
可以和cache_t::find
方法對比加深理解
3.慢速流程
彙編
__class_lookupMethodAndLoadCache3
與c++中_class_lookupMethodAndLoadCache3
相對應
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製程式碼
// initialize = YES , cache = NO , resolver = YES
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 快取查詢,cache為NO直接跳過
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
// lock是為了防止多執行緒操作; 類是否被編譯
runtimeLock.lock();
checkIsKnownClass(cls);
// 為查詢方法做準備條件,如果類沒有初始化時,初始化類和父類、元類等
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
// 從快取裡面查詢一遍,若有直接goto done
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
// 形成區域性作用域,避免區域性變數命名重複
{
// 在類的方法列表中查詢方法,若有直接cache_fill
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
// 遍歷父類進行查詢
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 在父類快取中查詢,若有直接cache_fill
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.
// 在父類的方法列表中查詢方法,若有直接cache_fill
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
// 如果方法仍然沒找到,就開始做動態方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// 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;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 開始訊息轉發
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
複製程式碼
慢速流程主要分為幾個步驟:
①_class_lookupMethodAndLoadCache3
呼叫lookUpImpOrForward
,此時引數initialize=YES cache=NO resolver=YES
②runtimeLock.lock()
為了防止多執行緒操作
③realizeClass(cls)
為查詢方法做準備條件,如果類沒有初始化時,初始化類和父類、元類等
④imp = cache_getImp(cls, sel)
為了容錯從快取
中再找一遍,若有goto done⑨
⑤// Try this class's method lists
區域性作用域中,在類的方法列表
中查詢方法,若有直接log_and_fill_cache
並goto done⑨
⑥// Try superclass caches and method lists
區域性作用域中,遍歷父類:先在父類快取
中查詢,若有直接log_and_fill_cache
並goto done
;沒有再去父類的方法列表中
查詢方法,若有直接log_and_fill_cache
並goto done⑨
⑦如果還沒找到就動態方法解析_class_resolveMethod
,標記為triedResolver = YES(已自我拯救過)
,動態方法解析結束後跳轉慢速流程④
⑧如果動態方法解析之後再找一遍仍然沒找到imp
,就丟擲錯誤_objc_msgForward_impcache
得到imp
並cache_fill
⑨done
:多執行緒解鎖,返回imp
接下來拆解步驟進行說明:
cache_getImp
這個方法後續會解釋getMethodNoSuper_nolock
遍歷呼叫search_method_list
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
複製程式碼
search_method_list
利用二分查詢尋找方法
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
// 如果方法列表已經排序好了,則通過二分查詢法查詢方法,以節省時間
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
// 如果方法列表沒有排序好就遍歷查詢
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
複製程式碼
findMethodInSortedMethodList
二分查詢演算法的具體實現(瞭解即可)
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// >>1 表示將變數n的各個二進位制位順序右移1位,最高位補二進位制0
// count >>= 1 如果count為偶數則值變為(count / 2);如果count為奇數則值變為(count-1) / 2
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
// 取出中間method_t的name,也就是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 繼續向前二分查詢
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 取出 probe
return (method_t *)probe;
}
// 如果keyValue > probeValue 則折半向後查詢
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
複製程式碼
log_and_fill_cache
->cache_fill
->cache_fill_nolock
進行快取
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
複製程式碼
_class_resolveMethod
動態方法解析——在找不到imp時的自我拯救操作cls
是元類的話說明呼叫類方法,走_class_resolveInstanceMethod
;非元類的話呼叫了例項方法,走_class_resolveInstanceMethod
- 兩者邏輯大同小異,主要邏輯是是objc_msgSend函式傳送
SEL_resolveInstanceMethod
訊息,系統呼叫resolveInstanceMethod
- 傳送訊息後,系統會再查詢一下sel方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
複製程式碼
_objc_msgForward_impcache
在彙編中呼叫了_objc_msgForward
,然後又進入_objc_forward_handler
,它在c++呼叫了objc_defaultForwardHandler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
複製程式碼
原來unrecognized selector sent to instance xxx
是這麼來的啊...
4.訊息查詢流程圖
寫在後面
OC的訊息機制分為三個階段:
- 訊息查詢階段:從類及父類的方法快取列表及方法列表查詢方法
- 動態解析階段:如果訊息傳送階段沒有找到方法,則會進入動態解析階段,負責動態的新增方法實現
- 訊息轉發階段:如果沒有實現動態解析方法,則會進行訊息轉發階段,將訊息轉發給可以處理訊息的接受者來處理
本文主要講了訊息查詢流程
,順帶提了幾句動態方法解析
,下一篇文章將通過案例來詳細解讀動態方法解析
並著重介紹訊息轉發機制
最後準備了一份動態方法決議的Demo,有興趣的小夥伴們可以自己下斷點看看方法查詢流程
和研究下動態方法決議