前言
相關文章
iOS底層探索九(方法的本質下objc_msgSend慢速及方法轉發初探)
相關代碼:
objc4_752源碼 消息轉發
上篇文章講述了方法的查找流程進行了詳細的探索,並對方法的轉發進行了初步探索,這篇文章,我們對消息的轉發流程進行詳細的探索。
消息轉發
上篇文章我們介紹了,如果在OC中如果調用一個沒有實現的方法,在方法查找過程中因爲找不到會造成崩潰,但是蘋果大大給我們提供了另一個解決思路,就是在動態解析(resolveMethod)的過程中有進行消息轉發,並且我們在上篇文章中也實現了初步的防止崩潰方法:
static void resolveMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
if (! cls->isMetaClass()) {//不是元類
//判斷類不是元類,那sel就是實例方法,那就先轉發resolveInstanceMethod方法,判斷有沒有實現resolveInstanceMethod,沒實現就不做處理
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//先轉發resolveClassMethod,會先查找下resolveClassMethod,如果沒實現就不做處理
resolveClassMethod(cls, sel, inst);
//再次查找下方法,如果沒有的話,就再轉發一下resolveInstanceMethod方法
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
}
}
實例方法動態消息決議
我們在main函數中調用teacher 中一個沒有實現的方法;
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma clang diagnostic push
// // 讓編譯器忽略錯誤,不然調用沒有的方法,編譯器編譯完語法分析會報錯,導致不能運行
#pragma clang diagnostic ignored "-Wundeclared-selector"
XZTeacher *teacher = [[XZTeacher alloc] init];
// 消息轉發流程這個方法.h中之聲明,不實現
[teacher saySomthing];
#pragma clang diagnostic pop
}
return 0;
}
因爲調用的是實力方法,我們來查看resolveInstanceMethod 方法,這裏傳入3個參數 cls 類,sel 方法編號, inst 對象
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
//查找系統方法中是否有實現resolveInstanceMethod這個方法,這裏會根據繼承鏈進行查找
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
//發送SEL_resolveInstanceMethod消息,系統給你一次機會-->你是不是要針對這個沒有實現的sel進行操作一下
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//發送消息SEL_resolveInstanceMethod 傳參爲我們的方法編號
//崩潰的方法不是這個方法,說明再NSObject方法中有實現這個方法
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//再次查找類中是否sel方法,因爲resolveInstanceMethod方法執行後可能動態進行添加了,resolver是不要進行消息轉發了
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 {
// Method resolver didn't add anything?
_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));
}
}
}
我們可以看到首先lookUpImpOrNil查找類中方法中是否有實現resolveInstanceMethod這個方法,這裏會根據繼承鏈進行查找,因爲是根據繼承鏈查找,也就是說我們類中如果沒有實現的話,就需要查找系統類,如果都找不到,這裏就直接return,
我們可以在類中查找到這個方法(resolveInstanceMethod),有這個方法後,代碼繼續執行,然後使用
msg(cls, SEL_resolveInstanceMethod, sel),方法給resolveInstanceMethod發送消息從而繼續lookUpImpOrNil查詢方法IMP有沒有實現,所以我們可以在我們自己的類中實現resolveInstanceMethod方法並對IMP進行綁定,所以在teacher中實現+resolveInstanceMethod 方法,並對未實現的方法進行IMP實現及綁定,使得,本身調用saySomthing 方法,經過IMP綁定後,調用了SayHello方法。從而防止了崩潰,
#import <objc/message.h>
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(saySomthing)) {
NSLog(@"進入方法了");
IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayType = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, saySomeIMP, sayType);
}
return [super resolveInstanceMethod:sel];
}
打印結果:
根據打印結果,我們可以看出,系統調用我們在XZTeacher 類中的resolveInstanceMethod方法,並根據我們的綁定,執行了sayHello方法。
實例方法我們看到了可以這麼處理,那麼類方法呢,我們也來看一下,我們在Main 函數中調用[XZTeacher sayLove],方法,sayLove 方法在XZTeacher類中只聲明不實現,運行崩潰,這是肯定的就不進行演示了,這裏根據源碼,我們可以看到會進入resolveMethod方法,並進入resolveClassMethod方法;
static void resolveClassMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
assert(cls->isMetaClass());
//查找下類是否實現了resolveClassMethod方法,NSObject類已經實現了
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//切記,此處是向元類發送resolveClassMethod消息,也就是調用resolveClassMethod方法
bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
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 {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%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));
}
}
}
這裏我們需要注意的是傳入的參數cls 爲元類,sel方法編號,inst爲類;我們可以看到這裏和resolveInstanceMethod方法中的邏輯基本一樣,不過這裏檢查的系統方法爲resolveClassMethod方法,這個方法我們可以看到上面的截圖裏面在NSObject類中也是有實現這個方法的;
類方法動態消息決議
我們在main函數中調用一個類方法 [XZTeacher sayLove] 在XZTeacher類中實現resolveClassMethod方法並運行程序:
可以看到確實來了resolveClassMethod方法,所以我們可以在這個方法進行類似處理來進行防止崩潰;在方法內加入代碼:
+(BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"來了:resolveClassMethod");
if (sel == @selector(sayLove)) {
NSLog(@"進入方法了sayLove");
IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
Method sayObjMethod = class_getClassMethod(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
const char *sayObjType = method_getTypeEncoding(sayObjMethod);
// 類方法在元類 objc_getMetaClass("XZTeacher")
BOOL isAdd = class_addMethod(objc_getMetaClass("XZTeacher"), sel, sayObjIMP, sayObjType);
return isAdd;
}
return [super resolveClassMethod:sel];
}
運行程序,查看運行結果,可以看出方法正常運行
在resolveMethod方法中類方法在調用resolveClassMethod完成後還進行了一次調用
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
在這裏我們可以看出,因爲這裏是的cls是元類,所以在元類中lookUpImpOrNil查找sayLove方法肯定也是找不到的,這裏就會調用resolveInstanceMethod,那我們是不是可以考慮將所有的處理都放到這個方法中呢resolveInstanceMethod,嘗試將resolveInstanceMethod重寫,並調用sayLove方法
可以看出還是隻來了resolveClassMethod方法,這是我們考慮,因爲是在元類職工查找resolveInstanceMethod方法,所以找不到,這個時候會想到,查找方法就是根據繼承鏈來進行查找方法的,也就是如下圖:
根據繼承鏈關係,可以看到根元類也是繼承與NSObject類,那就可以在NSObject類中只處理resolveInstanceMethod方法,先寫上這個方法添加處理方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"來了,%s",__func__);
if (sel == @selector(saySomthing)) {
NSLog(@"進入方法了saySomthing");
IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *sayType = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, saySomeIMP, sayType);
}
if (sel == @selector(sayLove)) {
NSLog(@"進入方法了sayLove");
IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(sayEasy));
Method sayObjMethod = class_getClassMethod(objc_getMetaClass("NSObject"), @selector(sayEasy));
const char *sayObjType = method_getTypeEncoding(sayObjMethod);
// 類方法在元類 objc_getMetaClass("XZTeacher")
return class_addMethod(objc_getMetaClass("NSObject"), sel, sayObjIMP, sayObjType);;
}
// [super resolveInstanceMethod:sel],因爲這裏是根類不能調用super了,直接返回NO就行
return NO;
}
運行查看下運行結果
調用類方法sayLove方法可以調用到SayEasy方法,調用實例方法saySomething根據IMP轉換調用了SayMaster方法;說民這裏處理確實可以一勞永逸
崩潰優化策略
-
可以在自己的項目工程中每個模塊進行命名規範,例如首頁的所有方法都以home_開頭,我的都有me_開頭
-
實現一個NSObject分類,進行bug替換,當檢測到是home_開始,沒有方法時,直接跳轉到首頁,並上報服務器,這樣的簡單操作就可以實現一個優化策略了。
這個策略可以發小是有漏洞的,如果在上層類中實現了resolveInstanceMethod方法那麼實例方法在防止的時候就不會走到NSObject類中了,可能就會導致上報不全,等問題。所以我們再進行消息處理的時候,一般會放到消息轉發的最後一步,那麼我們繼續來看消息轉發中的流程,在源碼中我們可以看到resolveMethod調用之後就是cache_fill 就會導致崩潰了,那麼中件流程我們根本沒法探索,這時我們需要回頭看一下方法緩存;
消息轉發流程探索
根據查找到imp後會進行緩存調用方法爲log_and_fill_cache,看到log說明應該是有日誌產生的,進入方法進行查看
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
//如果有這個objcMsgLogEnabled變量就會寫入日誌
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
// 寫入緩存
cache_fill (cls, sel, imp, receiver);
}
需要一個參數爲objcMsgLogEnabled爲true時調用logMessageSend 方法進行日誌書寫,繼續進入
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
可以看到日誌是寫入了本地的"/tmp/msgSends-編號"的目錄下總結下來就是需要2個參數objcMsgLogEnabled爲true 可以看到函數上面默認爲false ,還有objcMsgLogFD<0 函數上這個值默認爲-1,所以就只需要看objcMsgLogEnabled爲true即可;搜索一下這個函數的賦值方法
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;
}
找到這個方法我們需要在外部聲明一下這個函數來看一下在main文件中這枚寫下
#import "XZPerson.h"
#import "XZTeacher.h"
#import "NSObject+XZ.h"
#import <objc/runtime.h>
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
XZTeacher *teacher = [XZTeacher alloc];
// 方法調用日誌打開
instrumentObjcMessageSends(true);
[teacher saySomthing];
// [XZTeacher sayLove];
instrumentObjcMessageSends(false);
}
return 0;
}
將之前處理崩潰的都先註釋掉,然後調用日誌打開,運行並查看/tmp/路徑下以msgSends開頭的文件進行查看
+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject class
+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
+ OS_xpc_payload NSObject class
。。。根據名稱可以判斷下面都是系統方法
可以看到都是調用了2次分別爲resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector方法,這個方法resolveInstanceMethod已經研究過了,下面我們對後面2個方法研究一下
forwardingTargetForSelector 方法研究
點擊xcode 工具中的help 搜索
自己沒有處理的方法,會使用快速轉發到forwardingTargetForSelector方法中,使用這個方法,返回一個對象,讓別人進行處理這個方法,那麼我們新定義一個類XZStudent類,在這個類實現saySomething 和sayLove 方法並實現
#import "XZStudent.h"
@implementation XZStudent
- (void)saySomthing
{
NSLog(@"%s",__func__);
}
+ (void)sayLove{
NSLog(@"%s",__func__);
}
@end
在XZTeacher類中實現forwardingTargetForSelector方法
//實例方法調用這個轉發
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(saySomthing)) {
return [XZStudent alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
//類方法調用這個轉發
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(sayLove)) {
return [XZStudent class];
}
return [super forwardingTargetForSelector:aSelector];
}
並在main中調用saySomething方法查看打印效果
正常打印,可以看出這樣也是能完美解決崩潰
methodSignatureForSelector方法研究
根據xcode中的help 搜索,methodSignatureForSelector搜索
這個方法methodSignatureForSelector 方法關聯使用的就是forwardInvocation方法,將之前的處理先去掉,在XZTeacher類中加入這個最新處理:
//方法簽名的下層慢速處理,返回一個方法簽名 但是隻有這個方法肯定是不夠的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomthing)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
}
這裏的方法簽名是有固定格式的,標示不同的類型具體可查看官方文檔
查看一下運行結果:
可以正常執行到方法,而且不崩潰了 ,但是也沒有對這個方法進行處理,我們繼續查看文檔中的這個方法
看到可以這麼進行處理,
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
按照官方文檔我們進行處理
if ([[XZStudent alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent alloc]];
else
[super forwardInvocation:anInvocation];
運行查看結果:
也可以正常運行,同樣的這個如果是類方法處理方式和實例方法處理方式不同,如下代碼:
//方法簽名的下層慢速處理,返回一個方法簽名 但是隻有這個方法肯定是不夠的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomthing)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
// 在這裏進行處理方法,如果處理的話就可以處理,不處理就會失效
SEL aSelector = [anInvocation selector];
// 查看XZStudent 對象能否進行處理,如果能,就交給他處理不能就不處理了
if ([[XZStudent alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent alloc]];
else
[super forwardInvocation:anInvocation];
}
//類方法處理
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(sayLove)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
// 在這裏進行處理方法,如果處理的話就可以處理,不處理就會失效
SEL aSelector = [anInvocation selector];
// 查看XZStudent 類能否進行處理,如果能,就交給他處理不能就不處理了
if ([[XZStudent class] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[XZStudent class]];
else
[super forwardInvocation:anInvocation];
}
如果這幾個方法都沒有實現的話,系統就會調用 doesNotRecognizeSelector崩潰信息出來
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
總結
這篇文章我們就對方法的本質進行了完整的探索:
-
方法的底層就是調用objc_msgSend 方法
-
進行彙編在緩存中快速查找
-
調用_class_lookupMethodAndCache3進行慢速查找
-
找不到方法後進行動態解析
4.1 :resolveInstanceMethod:爲發送消息的對象的添加一個IMP,然後再讓該對象去處理
4.2:forwardingTargetForSelector:將該消息轉發給能處理該消息的對象
4.3:methodSignatureForSelector和forwardInvocation:第一個方法生成方法簽名,然後創建NSInvocation對象作爲參數給第二個方法,
4.3.2 然後在第二個方法(forwardInvocation)裏面做消息處理,只要在第二個方法裏面不執行父類的方法,即使不處理也不會崩潰
5.如果不進行動態解析就會導致崩潰
最後附帶一份蘋果的官方消息轉發圖:
寫給自己:
喋喋不休不如觀心自省,埋怨他人不如即聽即忘。能干擾你的,往往是自己的太在意,能傷害你的,往往是自己的想不開,未完待續。。。。