方法查找 , 動態方法解析以及消息轉發已是面試常客了 , 也是咱們瞭解 Aspects 或者餓了麼的 AOP
- Stinger 等等優秀的三方庫必不可少的基礎知識 .html
上篇文章 手把手帶你探索OC方法的本質 咱們講到 objc_msgSend
函數彙編查找方法緩存的流程 , 直到緩存沒有命中 , 由 JumpMiss
調用了 bl __class_lookupMethodAndLoadCache3
, 由此找到了 lookUpImpOrForward
回到了 c 函數中 , 開啓方法查找與消息轉發的流程 .git
開啓本篇文章探索前 , 請對 OC類對象/實例對象/元類 各自存儲着什麼內容 , 以及 isa 的走位有詳細瞭解 .github
繼續 上篇文章 , 咱們 objc_msgSend
彙編查找緩存 , 緩存 miss
時來到 _class_lookupMethodAndLoadCache3
這個函數 ,面試
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
/* cls:若是是實例方法那就是類,若是是類方法那就是元類 sel:方法名 obj:方法調用者 */
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
首先須要明白的是 :緩存
注意 : runtime
有兩個版本 , 找源碼的時候不要找到 objc-class-old.mm 中去了 .bash
源碼以下 :app
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//若是須要從緩存裏面查找,那須要先從緩存裏面找
// 第一次進入爲 false , 由於彙編快速查找流程沒找到進入.
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp; // 找到就直接返回 imp
}
// 加把鎖
runtimeLock.lock();
checkIsKnownClass(cls);
/** - 爲查找方法作準備條件,判斷類有沒有加載好 - 若是沒有加載好,那就先加載一下類信息,準備好父類、元類 - 只會加載一次. - 具體能夠參考 realizeClass 具體實現. */
if (!cls->isRealized()) {
realizeClass(cls);
}
// 當 sel == initialize, _class_initialize 將會調用 +initialize
// 確保對象已經初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
/** 這部分代碼過長先省略 , 後面詳細分析...*/
done:
runtimeLock.unlock();
return imp;
}
複製代碼
具體操做都加了註釋.ide
第一次進入該函數是由
objc_msgSend
彙編快速查找完cache
, 而後cache miss
纔會來到 , 所以傳入cache
爲false
.函數
return
, 找不到繼續下面步驟 ( 第一次進來爲不查找緩存 , 由於沒有緩存 ) .// 查找本類緩存
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 查找本類方法列表 class_data_bits_t -> bits.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
複製代碼
done
, 找不到繼續下面步驟.class_data_bits_t
中 , 找到後進行緩存並打印 , 直接跳轉到 done
, 找不到繼續下面步驟 .{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 查找父類緩存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 找到 -> 把方法緩存,跳轉到 done
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 查找父類方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
複製代碼
done
, 找不到繼續下面步驟.done
, 找不到繼續遍歷 .nil
, 繼續下面流程 .retry
部分通過 本類 , 父類方法緩存和方法列表查找都沒找到時來到此處. 代碼以下:源碼分析
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;
}
複製代碼
咱們能夠看到,在通過
_class_resolveMethod
後,會進行一遍retry
操做,從新進行一遍方法的查找流程,而且只有一次動態方法解析的機會 .
官方給出註釋以下 :
Don't cache the result ; we don't hold the lock so it may have changed already. Re-do the search from scratch instead .
咱們先來看下 _class_resolveMethod
的實現 , 再來看這個問題 .
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);
}
}
}
複製代碼
源碼流程分析 :
若是本類爲元類 : 說明方法爲類方法 . 調用
_class_resolveInstanceMethod
若是本類不是元類 : 說明方法爲實例方法 . 調用
_class_resolveClassMethod
- 調用
lookUpImpOrNil
若是沒找到 , 調用 _class_resolveInstanceMethod
源碼以下 :
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
複製代碼
源碼流程分析 :
SEL_resolveInstanceMethod
方法,即 +(BOOL)resolveInstanceMethod:(SEL)sel , 提示 , NSObject
已經實現這個方法 , 默認返回爲 NO
SEL_resolveInstanceMethod
消息 , 即調用這個方法 .sel
的 imp
.lookUpImpOrForward
中 , 進行 retry
, triedResolver
設置爲 YES
.講到這裏 , 是否是就對咱們剛剛所說的 : 在通過 _class_resolveMethod
後,會進行一遍 retry
操做,從新進行一遍方法的查找流程,而且只有一次動態方法解析的機會 豁然開朗了 .
這個方法和 1.3.1
- _class_resolveInstanceMethod
基本同樣 , 只不過發消息的對象變成元類 , 以下圖. 這裏就不重複贅述了 .
retry
最後一步中源碼以下 :
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼
在 lookUpImpOrForward
方法中,通過對本類 , 父類的緩存,方法列表查找和動態方法解析後,若是以上步驟都沒有進行處理,那麼就會進入,消息處理的最後一步,即消息轉發流程,這也是蘋果預留的補救方法崩潰最後一步。
找到 imp , 返回.
done:
runtimeLock.unlock();
return imp;
複製代碼
爲何當調用類方法時 , 對元類進行了動態方法解析也沒有找到 imp
時 , 還要再對類進行實例方法的解析呢 ? 以下圖 :
答 :
其實這是因爲 isa 走位的緣由 , 根源類的父類是 NSObject ,
類方法和實例方法的區別是存儲地不一樣 , 類方法是在元類中 , 實例方法是在類對象中 , 都是存儲在方法列表裏 , 並沒有
+
/-
之分 .所以當調用類方法時 , 繼承鏈走到根源類並無查找到時 , 是須要繼續走其父類
NSObject
查一次動態方法解析器的 , 而 NSObject 做爲類對象 , 是須要經過實例方法去查找的 .
也就是說 , 當在本類和父類中方法緩存以及方法列表都沒有找到 imp
時 , 蘋果給了咱們一次動態方法解析的機會 , 也就是在 resolveInstanceMethod
中本身給這個方法添加 imp
, return true
便可 .
在上面流程中 , 都沒有找到函數實現地址 , 那麼就進入了消息轉發流程 , 調用入口以下 :
_objc_msgForward_impcache
同
objc_msgSend
同樣 , 是由彙編實現的 , 所以全局搜索 , 直接來到 入口處.
代碼以下 :
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
複製代碼
還記得上篇文章提到的 彙編函數入口和結束符吧 ENTRY
, END_ENTRY
.
這段函數很明顯 只調用了 __objc_msgForward
.
搜索 __objc_msgForward
, 就在下面 , 一樣是彙編函數 .
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
複製代碼
彙編代碼解析 :
_objc_forward_handler
的 imp
放到 x17
寄存器中 .x17
寄存器中將低 32
位的數據放到 p17
裏 .x17
存儲的函數 imp .也就是說咱們須要找 _objc_forward_handler
.
全局搜索 , 在 objc-runtime.mm
中找到以下 :
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製代碼
再搜索 objc_defaultForwardHandler
, 找不到啥時候賦值的了 , 跟不進去了 ..
怎麼辦呢 ?
別急 , 既然前輩們都已經探索出來了 , 必然是有地方能夠尋找點蛛絲馬跡的 . 回顧一下本篇文章方法查找流程裏 , 咱們忽視掉一些細節 .
在整個流程裏 , 都是有一個打印的. 那麼打印到哪兒了呢 ? 點進去看下 .
static void 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);
}
複製代碼
其中 SUPPORT_MESSAGE_LOGGING
爲 :
# define SUPPORT_MESSAGE_LOGGING 1
複製代碼
也就是說 objcMsgLogEnabled 只要爲 true , 就能夠打印了 , 那麼怎麼把它設置爲 true 呢 ?
cmd
+ 點擊 objcMsgLogEnabled
. 來到 objc-class.mm
. 搜索 objcMsgLogEnabled
, 咱們看到有以下函數 .
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;
}
複製代碼
這裏咱們找到 當調用這個函數 , 參數會被賦值給 objcMsgLogEnabled
.
所以來到咱們的代碼中 .
修改代碼以下 :
#import "LBStudent.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LBStudent *student = [[LBStudent alloc] init];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
}
return 0;
}
複製代碼
運行工程 . 來到 /private/tmp
目錄下 , 打開最新的一份 msgSends
文件 :
打開以下 :
這其中 resolveInstanceMethod
動態方法解析咱們已經分析過了 , 來到 forwardingTargetForSelector
, 搜索一下發現其只有在 NSObject
中默認實現了返回爲 nil
, 並沒有其餘的方法實現了 .
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
複製代碼
那麼這時該怎麼辦 ? 不急 , 來找官方文檔 .
小提示 :
主要看 Discussion
文檔說明了該方法的前世此生 , 總結以下 :
- 1️⃣ : 該方法的目的 , 說白了就是你搞不定的方法 , 就交給別人處理 . 可是不能返回
self
,不然會一直找不到 , 陷入死循環 .- 2️⃣ : 若是不實現或者返回
nil
,會走到forwardInvocation
: 方法進行處理 .- 3️⃣ : 被轉發消息的接受者參數和返回值等須要和原方法相同 .
也就是說咱們能夠本身新定義一個類 , 或者選一個現有的類 , 其實現了這個方法 , 就交由其去處理.
寫法以下 :
// 消息轉發流程
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(saySomething)) {
return [LBTeacher new];
}
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
在運行 , 就很愉快的打印出 LBTeacher
的 saySomething
方法了 .
這個轉發給單一方法實現對象的過程 , 咱們也稱之爲 快速消息轉發流程 .
當快速消息轉發流程也並無實現 , 或者返回爲 nil
, 就來到了 慢速消息轉發流程
.
一樣 , 根據咱們的打印來尋求線索 .
找到 methodSignatureForSelector
.
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
複製代碼
查看源碼一樣是沒有發現 . 老辦法 , 找官方文檔 .
其實熟悉方法和函數的同窗應該很清楚 , 這個就是咱們的方法簽名 .
- 該方法是讓咱們根據方法選擇器
SEL
生成一個NSMethodSignature
方法簽名並返回 .- 這個方法簽名裏面其實就是封裝了返回值類型,參數類型等信息。
寫法以下 :
// 方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(saySomething)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
複製代碼
Code | Meaning |
---|---|
c | A char |
i | An int |
s | A short |
l | A long |
l | is treated as a 32-bit quantity on 64-bit programs. |
q | A long long |
C | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
* | A character string (char *) |
@ | An object (whether statically typed or typed id) |
# | A class object (Class) |
: | A method selector (SEL) |
[array type] | An array |
{name=type...} | A structure |
(name=type...) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers) |
很明顯 , 只給蘋果一個方法簽名是確定不行的 , 必需要有方法實現啊 , 別急 , 剛剛咱們在 forwardingTargetForSelector
的官方文檔中看到 , 若是返回 nil
或者不實現 , 則會進入 forwardInvocation:
的流程 .
一樣查找官方文檔後 , 方法解析以下 :
1️⃣ : forwardInvocation
和 methodSignatureForSelector
必須是同時存在的 .
2️⃣ : 該方法能夠自由指派多個對象來接收這個消息 .
3️⃣ : 將消息發送到該對象。保存結果 ,運行時系統將提取此結果並將其傳遞給原始發消息人。
該方法能夠指定多個轉發者 , 並且因爲 NSInvocation
的封裝 , 能夠自由調配 target
, 參數等等 , 自由度較高 , 但與此同時花費也將更高 , 官方文檔稱其爲 more expensive forwardInvocation: machinery . 使用方法以下 :
// 消息轉發
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
NSLog(@"%@",anInvocation);
SEL aSelector = [anInvocation selector];
if ([self respondsToSelector:aSelector]){
[anInvocation invoke];
}else if ([[LBTeacher new] respondsToSelector:aSelector]){
[anInvocation invokeWithTarget:[LBTeacher new]];
}
else
[super forwardInvocation:anInvocation];
}
複製代碼
至此 , 整個方法查找 , 動態方法解析 , 消息轉發的完整流程咱們已經講完了 , 整個流程走完 , 若是仍然沒有找到方法實現 , 那麼蘋果表示 我給過你太多機會了 , 我也沒轍了.
最後 留下兩道面試題 , 以幫助你們檢驗本身是否理解了 方法的本質 , 以及 方法查找和消息轉發的知識 .
#import "LBPerson.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[LBPerson class] isKindOfClass:[LBPerson class]];
BOOL re4 = [(id)[LBPerson class] isMemberOfClass:[LBPerson class]];
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[LBPerson alloc] isKindOfClass:[LBPerson class]];
BOOL re8 = [(id)[LBPerson alloc] isMemberOfClass:[LBPerson class]];
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
return 0;
}
複製代碼
LBPerson 爲一個普通的
OC
類 .
問打印結果.
LBStudent
繼承自 LBPerson
, LBPerson
中有一個方法以下 :
//聲明
- (void)sayHi;
//實現
- (void)sayHi{
NSLog(@"hi");
}
複製代碼
問 [LBStudent performSelector:@selector(sayHi)];
調用結果是什麼 .
LBPerson
繼承自 NSObject
, LBStudent
繼承於 LBPerson
, 項目中有一個 NSObject
的分類以下 :
// NSObject+LB.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LB)
- (void)sayHi;
@end
NS_ASSUME_NONNULL_END
// NSObject+LB.m
#import "NSObject+LG.h"
#import <AppKit/AppKit.h>
@implementation NSObject (LB)
- (void)sayHi{
NSLog(@"%s",__func__);
}
@end
複製代碼
問 :
[LBStudent performSelector:@selector(sayHi)];
複製代碼
打印結果是什麼 .
若是將分類裏 類方法改成實例方法 , 打印結果是什麼 ?
你們能夠在評論區留下本身的答案 , 下期揭曉 .
你們能夠由此來開拓思惟 , 想想若是讓你來作一個防止崩潰的三方庫 , 你會如何思考 , 如何設計 ?