iOS 消息轉發

2021-03-02 iOS開發

作者丨 WYW

https://juejin.im/post/5c6e773be51d451b25716d0e

前言:


我們在開發過程中,可能遇到服務端返回數據中有 null的情況,當取到 null值,並且對null發送消息的時候,就可能出現, unrecognized selector sent to instance,應用crash的情況。


針對這種情況,在每次取值的時候去做判斷處理又不大合適,以前筆者在GitHub上發現了一個神奇的文件NullSafe:

https://github.com/nicklockwood/NullSafe。

把這個文件拖到項目中,即使出現 null的情況,也不會報出 unrecognized selector sent to instance的問題。


筆者近期分析了一下NullSafe文件,並且通過做了一個Demo:QiSafeType,筆者將通過介紹 消息轉發流程的方式,揭開NullSafe神秘的面紗。


Demo(QiSafeType)消息轉發部分解讀

筆者將通過演示調用 QiMessage的實例 qiMessage 沒有實現的 length方法,演示消息轉發過程。

QiSafeType消息轉發效果如下:

 

QiSafeType消息轉發效果說明:

1.qiMessage消息轉發的整個過程主要涉及的3個方法:

+(BOOL)resolveInstanceMethod:(SEL)sel

-(id)forwardingTargetForSelector:(SEL)aSelector

-(void)forwardInvocation:(NSInvocation*)anInvocation

2.其中在 +(BOOL)resolveInstanceMethod:(SEL)sel的時候,會有相應的方法緩存操作,這個操作是系統幫我們做的。

QiSafeType消息轉發部分解析

1.首先貼一張消息轉發的圖,筆者聊到的內容會圍繞著這張圖展開。

2.下邊筆者依次分析消息轉發的過程

下文還是以 qiMessage調用 length方法為例,分析消息轉發的過程。

(1)首先 qiMessage在調用 length方法後,會先進行動態方法解析,調用 +(BOOL)resolveInstanceMethod:(SEL)sel,我們可以在這裡動態添加方法,而且如果在這裡動態添加方法成功後,系統會把動態添加的 length方法進行緩存,當 qiMessage再次調用 length方法的時候,將不會調用 +(BOOL)resolveInstanceMethod:(SEL)sel。會直接調用動態添加成功的 length方法。

(2)如果動態方法解析部分我們沒有做操作,或者動態添加方法失敗了的話,會進行 尋找備援接收者的過程 -(id)forwardingTargetForSelector:(SEL)aSelector,這個過程用於尋找一個接收者,可以響應 未知的方法aSelector。

(3)如果尋找備援接收者的過程中返回值為nil的話,那麼會進入到完整的消息轉發流程中。 完整的消息轉發流程:首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封於其中,此對象包含選擇子、目標(target)及參數。在出發NSInvocation對象時,「消息派發系統」(message-dispatch system)將親自出馬,把消息指派給目標對象。(摘抄自Effective Objective-C 2.0編寫高質量iOS與OS X的52個有效方法)

1.結合 QiMessage中的代碼對消息轉發流程進一步分析

(1)先看第一部分 qiMessage在調用 length方法後,會先進行動態方法解析,調用 +(BOOL)resolveInstanceMethod:(SEL)sel,如果我們在這裡為 qiMessage動態添加方法。那麼也能處理消息。 相關代碼如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    printf("%s:%s \n", __func__ ,NSStringFromSelector(sel).UTF8String);

    if (sel == @selector(length)) {
        BOOL addSuc = class_addMethod([self class], sel, (IMP)(length), "q@:");
        if (addSuc) {
            return addSuc;
        }
    }
    return [super resolveInstanceMethod:sel];
}

class_addMethod(Class_Nullablecls,SEL_Nonnullname,IMP_Nonnullimp,constchar*_Nullabletypes)OBJC_AVAILABLE(10.5,2.0,9.0,1.0,2.0); 參數types傳入的"q@:"分別代表:

」q「:返回值long long ;
」@「:調用方法的的實例為對象類型
「:」:表示方法

如有其它需要,看下圖應該會更直觀一些

(2) qiMessage在調用 length方法後,動態方法解析部分如果返回值為NO的時候,會尋找備援接收者,調用 -(id)forwardingTargetForSelector:(SEL)aSelector,如果我們在這裡為返回可以處理 length的接收者。那麼也能處理消息。

相關代碼如下:

static NSArray *respondClasses;

- (id)forwardingTargetForSelector:(SEL)aSelector {

    printf("%s:%s \n", __func__ ,NSStringFromSelector(aSelector).UTF8String);

    id forwardTarget = [super forwardingTargetForSelector:aSelector];
    if (forwardTarget) {
        return forwardTarget;
    }

    Class someClass = [self qiResponedClassForSelector:aSelector];
    if (someClass) {
        forwardTarget = [someClass new];
    }

    return forwardTarget;
}


- (Class)qiResponedClassForSelector:(SEL)selector {

    respondClasses = @[
                       [NSMutableArray class],
                       [NSMutableDictionary class],
                       [NSMutableString class],
                       [NSNumber class],
                       [NSDate class],
                       [NSData class]
                       ];
    for (Class someClass in respondClasses) {
        if ([someClass instancesRespondToSelector:selector]) {
            return someClass;
        }
    }
    return nil;
}

這裡有一個不常用的API: +(BOOL)instancesRespondToSelector:(SEL)aSelector;,這個API用於返回Class對應的實例能否相應aSelector。

(3) qiMessage在調用 length方法後,動態方法解析部分如果返回值為NO的時候,尋找備援接收者的返回值為nil的時候,會進行完整的消息轉發流程。調用 -(void)forwardInvocation:(NSInvocation*)anInvocation,這個過程會有一個插曲, -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector,只有我們在 -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector中返回了相應地NSMethodSignature實例的時候,完整地消息轉發流程才能得以順利完成。

先聊下插曲 -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector。

摘抄自文檔: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.

加粗部分就是適用我們當前場景的部分。

這個方法也會用於消息轉發的時候,當NSInvocation對象必須創建的時候,如果我們的對象能夠處理沒有直接實現的方法,我們應該重寫這個方法,返回一個合適的方法籤名。

相關代碼

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    printf("%s:%s \n\n\n\n", __func__ ,NSStringFromSelector(anInvocation.selector).UTF8String);

    anInvocation.target = nil;
    [anInvocation invoke];
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {

    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        Class responededClass = [self qiResponedClassForSelector:selector];
        if (responededClass) {
            @try {
                signature = [responededClass instanceMethodSignatureForSelector:selector];
            } @catch (NSException *exception) {

            }@finally {

            }
        }
    }
    return signature;
}

- (Class)qiResponedClassForSelector:(SEL)selector {

    respondClasses = @[
                       [NSMutableArray class],
                       [NSMutableDictionary class],
                       [NSMutableString class],
                       [NSNumber class],
                       [NSDate class],
                       [NSData class]
                       ];
    for (Class someClass in respondClasses) {
        if ([someClass instancesRespondToSelector:selector]) {
            return someClass;
        }
    }
    return nil;
}

這裡有一個不常用的API: +(NSMethodSignature*)instanceMethodSignatureForSelector:(SEL)aSelector;,這個API通過Class及給定的aSelector返回一個包含實例方法標識描述的方法籤名實例。

> 此外對於NSInvocation的筆者發現一個很好玩的點。
仍然以`qiMessage`調用`length`方法為例。
- (void)forwardInvocation:(NSInvocation *)anInvocation中的 anInvocation的信息如下:

<NSInvocation: 0x6000025b8140>
return value: {Q} 0
target: {@} 0x60000322c360
selector: {:} length

> return value指返回值,「Q」表示返回值類型為long long類型;
> target 指的是消息的接收者,「@「標識對象類型;
> selector指的是方法,「:」 表示是方法,後邊的length為方法名。

更多內容可見下圖NSInvocation的types:


尚存疑點

細心的讀者可能會發現在首次消息轉發的時候流程並不是

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
-[QiMessage forwardInvocation:]:length 

而是

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
+[QiMessage resolveInstanceMethod:]:length 
+[QiMessage resolveInstanceMethod:]:_forwardStackInvocation: 
-[QiMessage forwardInvocation:]:length 

這裡的第三行 +[QiMessageresolveInstanceMethod:]:length 第四行 +[QiMessageresolveInstanceMethod:]:_forwardStackInvocation: 筆者查看了開源源碼:NSObject.mm相關源碼如下:

https://opensource.apple.com/source/objc4/objc4-750.1/runtime/NSObject.mm.auto.html


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}


- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

筆者尚未搞清楚原因。讀者有知道的敬請指教。

QiSafeType之消息轉發相關代碼

QiSafeType消息轉發相關的代碼在QiMessage中

NSNull+QiNullSafe.m

筆者結合NullSafe:https://github.com/nicklockwood/NullSafe仿寫了一個NSNull+QiNullSafe.m。

NSNull+QiNullSafe.m能夠避免的問題有:

  NSNull *null = [NSNull null];
    [null performSelector:@selector(addObject:) withObject:@"QiShare"];
    [null performSelector:@selector(setValue:forKey:) withObject:@"QiShare"];
    [null performSelector:@selector(valueForKey:) withObject:@"QiShare"];
    [null performSelector:@selector(length) withObject:nil];
    [null performSelector:@selector(integerValue) withObject:nil];
    [null performSelector:@selector(timeIntervalSinceNow) withObject:nil];
    [null performSelector:@selector(bytes) withObject:nil];


NullSafe是怎麼處理null問題

其實NullSafe處理null問題用的是消息轉發的第三部分,走的是完整地消息轉發流程。

不過我們開發過程中,如果可以的話,還是儘可能早地處理消息轉發這部分,比如在動態方法解析的時候,動態添加方法(畢竟這一步系統可以為我們做方法的緩存處理)。 或者是在尋找備援接收對象的時候,返回能夠響應未實現的方法的對象。

注意:相關的使用場景在測試的時候不要用,測試的時候儘可能還是要暴露出問題的。 並且使用的時候,最好結合著異常日誌上報。


 推薦↓↓↓ 

涵蓋:程式設計師大咖、源碼共讀、程式設計師共讀、數據結構與算法、黑客技術和網絡安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程式設計師等。

相關焦點

  • 《英雄聯盟手遊》IOS測試怎麼預約 ios內測資格申請方法
    自從官方放出了ios測試開啟的消息以後,就有大批小夥伴在網上尋找資格申請攻略,本期我們就來看看英雄聯盟手遊ios內測資格申請教程,感興趣的小夥伴可以來看看! 英雄... 英雄聯盟手遊IOS測試怎麼預約?
  • 深入理解Object-C消息轉發機制
    你可能早就遇到過經由消息轉發流程所處理的消息了,只是未加留意。如果在控制臺中看到下面這種提示信息,那就說明你曾向某個對象發送過一條無法解讀的消息,從而啟動了消息轉發機制,並將次消息轉發給了NSObject得默認實現。
  • 騰訊孤島行動ios在哪玩?ios試玩體驗方法[多圖]
    孤島行動ios玩家可以玩嗎?騰訊的新遊已經曝光,很多玩家都在期待這款遊戲的上線,而大多數ios玩家則在但有,ios系統能玩嗎?針對ios用戶,其實也是有方法可以玩的,由於目前遊戲暫未上線,不過對ios用戶試玩體驗的方法小編已經為大家準備好。
  • 看到反常消息,先別急著轉發,很可能是騙局
    原創,請勿使用某天,我在一個微信群看到一條消息。大致是說孩子走丟了,希望大家轉發之類的。結果因為轉發這條消息被幾個朋友嘲笑,認為我無腦,貪財。天知道,我真的只是想讓大家知道這個事啊!仔細看了幾遍信息。發現編造這條信息的人,傳播學學的不錯。大體上通順,讓人看了沒什麼疑慮。操作簡便。直接轉發信息,就是動動手指的事情,不需要付出什麼代價。
  • 輕鬆學習之 Objective-C消息轉發 乾貨
    我們要通過一個小例子來簡單、通俗的理解一下什麼是消息轉發以及如何消息轉發,希望看完這篇文章時大家會徹底的明白OC的消息。
  • 咪嚕ios版遊戲下載安裝_咪嚕ios版手遊下載安裝_18183手機遊戲下載
    咪嚕ios版下載安裝擁有豐富的玩法模式任你選擇,讓你的遊戲過程更加的精彩! 咪嚕ios客戶端是一款破解遊戲盒子,為用戶提供各類優質熱門破解遊戲,綠色安全,無需付費,各種裝備道具隨意用,怎麼爽怎麼來,哪個好玩就玩哪個,輕鬆自在。喜歡的朋友歡迎下載使用!
  • 谷歌在其ios輸入法添加了比特幣符號
    圖片來源圖蟲:已授站長之家使用今日塊訊(ChinaZ.com) 2 月 20 日消息:據bitcoinist消息,谷歌已在其ios行動裝置鍵盤上添加了比特幣符號,這標著這家IT當然,ios用戶想要使用這個比特幣符號的話,需要先安裝谷歌Gboard輸入法而不是默認的蘋果鍵盤。安裝完之後,按住美元符號,就能在彈出的多個主流貨幣符號裡找到這一符號。業內分析稱,谷歌的這一舉動可能無助於提高比特幣的使用率,不過表現了對比特幣的認可。 2018 年 9 月,谷歌解除了四個月前對加密貨幣廣告的禁令。
  • 咪嚕ios遊戲下載不了怎麼辦
    咪嚕ios遊戲下載不了怎麼辦?咪嚕盒子遊戲打不開怎麼辦?咪嚕盒子ios遊戲玩不了怎麼辦?今天很多玩家問咪嚕盒子ios打不開、ios遊戲信任不了、遊戲打不開,以上情況是因為什麼呢?今天小編就來告訴大家。
  • iOS逆向分析和注入-微信防撤回
    蘋果電腦 (macOS High Sierra 10.13.3) 並安裝了以下軟體:frida-ios-dump: 砸殼利器。class_dump: dump 目標對象的 class 信息的工具。Hopper_Disassembler: 靜態分析工具。usbmuxd: 埠轉發,可以讓我們通過 usb 連接手機進行 ssh、lldb 調試等。
  • 對比iOS12,這續航看不懂
    大家好,這裡是那可愛的凌春的小編,數碼伴隨我們左右,每天給大家推薦精緻的文章,大家可以收藏跟轉發哦! ios13.2.3正式版軟體是今天蘋果凌晨剛剛更新的最新的ios的手機系統,部分以後在前期手機的使用當中出現了,app 在後臺使用當中無法下載的的問題哦,現在這些問題已經都統統解決了哦
  • QQ閱讀ios怎麼充值 ios最優惠充值教程
    QQ閱讀是騰訊下面的一款手機閱讀軟體,在安卓充值比較方便,那麼ios充值要怎麼樣?QQ閱讀ios要怎麼充值?就一起來看看吧!
  • 蘋果ios14支持分屏功能嗎
    ios14 怎麼分屏?蘋果ios14 支持分屏功能嗎?蘋果今年的 WWDC 採用了全新的線上模式,面向大眾的發布會也在北京時間本周二(6.23)凌晨 01:00 舉行。
  • bt盒子ios
    bt盒子ios下載!對於遊戲玩家來說是必備的,有了變態版手遊平臺基本上可以解決你在遊戲中遇到的所有難題,為了讓各位玩家能玩到最新的變態版手遊,本文為大家帶來變態版手遊平臺排行榜大全,涵蓋了大型單機、手機遊戲、網頁遊戲等資源,喜歡的小夥伴記得下載體驗哦!
  • ios蘋果變態手遊盒子官網 ios變態手遊盒子哪家好
    18183首頁 ios蘋果變態手遊盒子官網 ios變態手遊盒子哪家好 點擊領取
  • ios破解遊戲助手
    ios破解遊戲助手哪個好?今天18183小編就給大家推薦一款破解遊戲盒子:ios破解遊戲助手。ios破解遊戲助手包含好玩的ios破解遊戲、修仙、修真、角色扮演、模擬經營等各種類型破解手機遊戲!喜歡破解遊戲的玩家趕緊來下載吧!
  • 微信遵守ios深色模式沒被下架,QQ HD卻被蘋果下架
    之前蘋果發出公告稱今後不支持ios深色模式的APP都會被下架,在這條公告出來之前,微信官方說為了保證用戶夜間睡眠,不會開發深色模式,然而蘋果公告出來之後,微信官方立刻發聲明宣布會很快開發出ios版深色模式。所以微信應蘋果要求加入深色模式被吐槽是妥協,這次還上了熱搜。
  • ios描述文件怎麼刪除?刪除iOS12公測版描述文件的方法
    通過ios描述文件來升級相應的系統版本,是許多蘋果手機用戶都會選擇的系統升級方式,不過,在升級最新系統之後,需要把手機中原有系統的ios描述文件刪除掉,那麼,ios描述文件怎麼刪除?我們就以最近發布的iOS12公測版描述文件為例子進行講解,下面,給大家帶來刪除iOS12公測版描述文件的方法!
  • ios越獄已成過去式,ios籤名將迎來新轉機
    ios系統越獄至今依然是被一群資深刷機玩家頗受追捧的話題。不難理解的是,ios越獄和Android ROOT的原理如出一轍,都是通過破壞原生系統的底層獲取到最高權限,從而繞過官方安全系統的限制達到安裝第三方應用的目的。
  • 英雄聯盟手遊ios安裝教程 lol手遊ios怎麼安裝
    英雄聯盟手遊終於迎來了大規模測試,這次測試加入了IOS版本,很多沒能參加測試的小夥伴也都非常激動,安卓用戶可以直接在谷歌商店進行預約下載,但是IOS的玩家由於國內蘋果商店並沒有上架,所以無法下載,為了能夠讓更多的玩家體驗到英雄聯盟手遊,下面由我遊小編為大家介紹一下英雄聯盟手遊ios安裝教程。
  • 王者榮耀ios版可以用Q幣充值嗎?王者榮耀ios怎麼充值?
    導 讀 王者榮耀ios版可以用Q幣充值嗎?