在xcode中使用快捷鍵command+shift+0
打開官方文檔,搜索Objective-C Runtime
可以看到有這樣一段描述:
Overview
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.
...
譯文:Objective-C運行時是一個運行時庫,它為Objective-C語言的動態屬性提供支持,因此所有Objective-C應用程序都鏈接到它。Objective-C運行時庫支持在在/usr/lib/libobjc.A.dylib的共享庫中實現的函數。
我們都知道OC是一門動態性語言,主要是因為蘋果公司基于Runtime,在C語言的基礎上,加上編譯器
和運行時環境
組成了OC語言的基本框架。編譯器clang
可以將OC語言轉換成C++語言,而c
和c++
都是靜態語言,一經編輯之后就不能再添加新的類、新的方法或者其他結構上的一些變化,而Runtime
則可以實現這些動態性。
關于Runtime的基本概念以及簡單應用,我在之前的文章已經寫過傳送門,本文主要想通過源碼進行分析,分析過程比較長,可直接在文章末尾看總結。蘋果開源官網下載objc4-818.2源碼,如果想要調試源碼可以下載大神編譯好的objc4_debug
Objective-C從三種不同的層級與Runtime進行交互
- 通過Objective-C源代碼:大部分情況下運行時系統在后臺自動運行,我們只需要編輯Objective-C源代碼,當編譯OC類和方法時,編譯器為了實現語言動態特性將自動創建一些數據結構和函數。
-
通過NSObject的方法間接調用:Cocoa程序中大部分的類都是繼承自NSObject,繼承了NSObject的行為。某些情況下NSObject類僅僅是定義了方法模板而沒有提供所有需要的代碼,如 description方法會返回該類內容的字符串表示,然后NSObject并不知道子類中的內容,所以它只是返回類的名字和對象的地址,子類可以重新實現該方法以提供更多的信息。有些方法只是簡單的從Runtime系統中獲取信息,從而對允許對象進行一定程序的自我檢查,如
class
,superclass
,isMemberOfClass
,isKindOfClass
,isSubclassOfClass
,isAncestorOfObject
,respondsToSelector
,conformsToProtocol
,methodForSelector
這些方法的實現都調用了runtime
中定義的方法。 -
直接通過Runtime提供的API:Runtime提供的API很多,大致可以總結為幾大類
- objc_xxx函數:這些函數是最頂層的操作,類或協議開配空間創建、注冊,類、元類對象、協議獲取,操作關聯對象,發送objc消息
objc_msgSend
。 - class_xxx函數:這些函數是對類的內部進行操作,如創建類的實例,添加實例變量、屬性、方法、協議,獲取、拷貝類包含的信息,檢查判斷。
- object_xxx函數:針對對象進行操作,如獲取/設置對象的類、實例變量的值。
- method_xxx函數:針對方法進行操作,獲取方法的參數及返回值,方法的實現以及交換等。
- property_xxx函數:獲取屬性名稱、特性列表,拷貝屬性列表、特性值。
- protocol_xxx函數:與協議相關操作
- ivar_xxx函數:獲取實例變量的名稱、類型編碼、偏移量
- sel_xxx函數:方法編號相關
- imp_xxx函數:方法實現相關的block創建、獲取、刪除
- objc_xxx函數:這些函數是最頂層的操作,類或協議開配空間創建、注冊,類、元類對象、協議獲取,操作關聯對象,發送objc消息
clang分析
創建一個類繼承自NSObject例如CLAnimal
,添加一個方法-(void)eat
,在main.m中初始化并調用CLAnimal *ani = [[CLAnimal alloc] init]; [ani eat];
,使用clang命令將main.m
轉換為main.cpp
文件
#終端命令 -rewrite-objc Rewrite Objective-C source to C++
clang -rewrite-objc main.m
查看clang
生成的main.cpp
文件最后面如下
...此處省略十萬八千行...
typedef struct objc_object CLAnimal;
typedef struct {} _objc_exc_CLAnimal;
#endif
struct CLAnimal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
// -(void)eat;
/* @end */
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2__ts8l68y17cncks73d_vjz4vh0000gn_T_main_bf6255_mi_0);
CLAnimal *animal = ((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CLAnimal"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
我們同樣可以直接使用使用這種寫法,需要導入頭文件#import <objc/message.h>
,另外往父類發送消息有些不同objc_msgSendSuper(id super, SEL _cmd)
, 其中super
是一個結構體objc_super{ id receiver,Class super_class}
, 從super_class中查找方法,消息接受者super->receiver
仍然是self
從上面的代碼可以很直觀的得出兩個結論:
- OC對象的本質是就是一個結構體
objc_object
- 方法的本質就是消息發送
objc_msgSend(id self, SEL _cmd)
,id
類型參數表示調用者,_cmd
表示方法編號
objc_msgSend的實現過程
從源碼中查找c函數objc_msgSend,發現并沒有它的實現,因為它是使用匯編來實現的,搜索對應的匯編代碼ENTRY _objc_msgSend
。為什么要使用匯編呢?原因有兩個,一是因為C函數不能直接保留未知的參數,然后跳轉到任意的指針,但是匯編有寄存器可以保存x0~x31,二是因為使用匯編效率高,匯編語言是二進制指令的文本形式,與指令是一一對應的關系,是最底層的低級語言。
ENTRY _objc_msgSend //方法入口
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
匯編部分我們可以大致分析一下:
- 首先檢查是否為
nil
或tagged pointer
類型(NSNumber、NSDate、NSString
等小對象),是執行LNilOrTagged
,再次檢查是nil
則跳轉到LReturnZero
結束,tagged pointer類型執行GetTaggedClass ---> LGetIsaDone
- 第一步檢查不是
nil或tagged pointer類型
將會繼續執行GetClassFromIsa_p16 ---> LGetIsaDone
-
LGetIsaDone
對isa
操作之后會調用CacheLookup
在緩存中查找imp
,找到則調用imp
,找不到調用objc_msgSend_uncached
-
LGetIsaDone
中只是對CacheLookup
這個宏的調用CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
// calls imp or objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
LLookupStart\Function:
1. ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
2. CacheHit \Mode // hit: call or return imp
3. cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
#if CACHE_MASK_STORAGE == ...
add ...
4. ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
在LLookupStart\Function:
可以看到查找的4種情況(通過注釋以及單詞大致推測):
sel
與_cmd
不匹配,繼續- 執行或返回
imp
- 找不到則調用
MissLabelDynamic
即CacheLookup
的第三個參數__objc_msgSend_uncached
,隨后添加到緩存中- 根據sel循環查找,查找到執行2
- 先看下
CacheHit
這個方法,它也是個宏,這里進入時為NORMAL,直接調用IMPTailCallCachedImp x17, x10, x1, x16
; - 接下來我們需要看一下
__objc_msgSend_uncached
的實現 - 繼續搜索跟蹤發現它的實現里面又調用了
MethodTableLookup
,翻譯一下就是方法列表查找
,而它下面TailCallFunctionPointer x17
翻譯一下就是調用函數指針,很明顯得出結論是在MethodTableLookup
這個方法中進行的IMP查找,不然下面x17
里面的地址從哪來呢。 - 查看
MethodTableLookup
實現,bl
跳轉到_lookUpImpOrForward
,繼續搜索_lookUpImpOrForward
,咦?全是call
和bl
,沒有實現??仔細看匯編實現里面有兩行注釋,原來是在C
函數lookUpImpOrForward
里面
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
最終在objc_runtime_new.mm
文件中找到它的實現方法,看到這里終于舒服一些了,這一刻我忽然覺得C語言
竟然也是這么的親切~,這個方法就是漫長的IMP查找流程,我在里面加了一些中文注釋,最新的源碼與之前的版本有些不一樣,但是大體的邏輯都是一樣的
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {//判斷類是否已經被初始化通過元類中的class_rw_t中的flag字段
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
checkIsKnownClass(cls);//檢查是否為runtime中已知的類,包括共享緩存,加載的image或使用api注冊的
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.獲取當前類的方法列表中查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done; //若找到,則跳轉到done位置
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
//將curClass的父類 賦值給curClass
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;//若當前類是NSObject,那么不需要再繼續查找了,跳出循環 給imp賦值為forward_imp
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
//當前類不是NSObject,查找父類緩存,這里又是匯編查找:ENTRY _cache_getImp --> CacheLookUp
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
//找到的是一個轉發方法,停止搜索,且不進行緩存
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
//在父類中找到了這個方法,緩存它到當前類
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
//最終沒有找到實現方法,那么嘗試一次調用resolver
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//將imp添加到當前類緩存的方法
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
至此IMP查找環節就結束了,若沒有查找到IMP則開始動態方法解析
動態解析
接著上一步IMP查找,如果沒有找到,則會調用resolveMethod_locked
,在此方法中會根據是否為元類分別調用resolveClassMethod
與resolveInstanceMethod
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
以類方法resolveInstanceMethod
為例,我們可以寫個示例測試一下,調用一個未實現的SEL,并重寫resolveInstanceMethod
,然而發現,這個方法竟然被調用了兩次?
仔細分析兩次的調用棧并不一樣,第一次調用確實是與剛才的分析一致,第二次是從
methodSignatureForSelector
調用,具體的流程暫且不清楚,但是可以確認的一點是我們還可以在第一次調用resolveInstanceMethod:
之后的methodSignatureForSelector
方法內去處理消息。
我們嘗試使用runtime
動態添加添加一個IMP
,代碼示例,果然不再崩潰,另外很多人會被那張經典的消息轉發流程圖所誤導,以為只有返回YES消息才是已處理,NO會嘗試消息轉發,其實不然,查看源碼發現方法返回值只跟日志打印有關系
,只有在返回YES并且OBJC_PRINT_RESOLVED_METHODS = YES
時才會打印,可以自己嘗試驗證一下
若沒有處理消息會發生什么呢?通過debug源碼跟蹤發現在動態方法解析之后會再次調用lookUpImpOrForward
方法,并在最后一次for循環也就是NSObject層時,會對IMP進行賦值為forward_imp
,這個forward_imp = _objc_msgForward_impcache
,即消息轉發。這個方法是匯編實現的
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
經過一頓搜索查找。。。跟不下去了,沒有后續的源碼實現了,蘋果對這塊并沒有開源,怎么辦呢?我們只能通過查看log來試試,在源碼內部有一個非常好用的函數
// env NSObjCMessageLoggingEnabled
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
從注釋可以看到我們可以直接添加環境變量NSObjCMessageLoggingEnabled
來使用,它會輸出一個tmp/msgSends-(xxx pid)
的文件,搜索logMessageSend
方法可以看到寫入文件路徑,這里的完整路徑是Macintosh HD --> private --> tmp --> msgSends-xxx
,在818.2源碼添加完可能沒有寫入成功,將源碼中的objcMsgLogLock注釋掉,然后重新編譯一下objc再進行debug即可
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
...
// objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
// objcMsgLogLock.unlock();
}
可以看到在崩潰之前,它內部的方法調用順序,在動態解析之后調用了forwardingTargetForSelector:
方法,然后又調用了methodSignatureForSelector
,哈哈,爽歪歪有沒有?
消息轉發
這部分源碼并沒有開源,我們只能得到它的調用順序,然我們可以在蘋果的官方文檔查看forwardingTargetForSelector
用法和描述,使用command+shift+0
打開官方文檔,只摘重點
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result,
that returned object is used as the new receiver object and the message dispatch resumes to that new object.
如果一個對象實現(或繼承)這個方法,并返回一個非nil(非self)結果,
那么返回的對象將被用作新的接收方對象,消息調度將恢復到這個新對象。
This is useful when you simply want to redirect messages to another object
and can be an order of magnitude faster than regular forwarding.
當您只想將消息重定向到另一個對象時使用此方法比常規轉發快一個數量級。
快速轉發簡單使用示例:
同樣的對于methodSignatureForSelector
也只能通過官方文檔查閱,可以知道該方法用于協議的實現。此方法還用于必須創建NSInvocation對象的情況,例如在消息轉發期間。如果您的對象維護委托或能夠處理它不直接實現的消息,則應該重寫此方法以返回適當的方法簽名。
This method is used in the implementation of protocols.
This method is also used in situations where an NSInvocation object must be created,
such as during message forwarding.
If your object maintains a delegate or is capable of handling messages
that it does not directly implement,
you should override this method to return an appropriate method signature.
慢速轉發流程示例:
如果對消息沒有處理最終會調用doesNotRecognizeSelector
崩潰
總結
最后總結一下整個IMP
查找,動態解析以及消息轉發的完整過程,它是由匯編,c/c++共同完成的
- 首先會進行匯編的快速查找
ENTRY _objc_msgSend
,在當前類的緩存中查找,若有則直接調用IMP
。在runtime
中對應的查詢路徑為objc_class-->cache_t-->buckets-->{sel, imp}
,查找過程使用了哈希算法 - 匯編查找不到則會調用c的方法
_lookUpImpOrForward
-
_lookUpImpOrForward
查找流程為先在自己的方法列表中查找,若找到則返回IMP,并緩存 - 如果在自己的方法列表中查找不到,判斷當前是否為NSObject類,如果是跳出循環;
- 循環查詢父類的緩存,在父類緩存中查詢到的IMP若為
forward_imp
,那么直接調用這個IMP且不做緩存,若不是將它緩存到當前類 - 在循環中繼續查詢父類方法列表,直到根類NSObject
- 循環查詢父類方法列表,直到根類NSObject
- 若最終都沒有找到,則開始動態方法解析
resolveInstanceMethod
,類方法是resolveClassMethod
。這個方法返回值只影響log打印。 - 再次執行
_lookUpImpOrForward
,循環查找是否實現了resolveInstanceMethod直到NSObject,最終會將forward_imp賦值給IMP并返回 -
forward_imp
進入消息轉發流程,這塊是由匯編寫的,可通過instrumentObjcMessageSends
方法開啟寫入日志查看崩潰前的方法調用順序日志,然后查看官方文檔解釋作進一步分析得出,在崩潰前消息轉發有快速轉發和慢速轉發兩種。快速轉發是forwardingTargetForSelector
提供一個對象處理消息。慢速轉發是使用forwardInvocation:
與methodSignatureForSelector:
兩個方法。 - 若消息最后沒有處理,將會拋出異常
doesNotRecognizeSelector
。
PS:本文是對照最新的objc4-818.2源碼進行對照,與舊版本方法以及參數有些出入的地方,另外對于匯編不太了解,基本上都是按照推測來寫,如有錯誤請指正。