iOS之武功祕籍⑥:Runtime之方法與消息

iOS之武功祕籍 文章彙總html

寫在前面

上文說到cache_t緩存的是方法,咱們分析了cache的寫入流程,在寫入流程以前,還有一個cache讀取流程,即objc_msgSendcache_getImp.那麼方法又是什麼呢?這一切都要從Runtime開始提及...c++

本節可能用到的祕籍Demogit

1、Runtime

① 什麼是Runtime?

Runtime是一套API,由c、c++、彙編一塊兒寫成的,爲OC提供了運行時.github

  • 運行時:代碼跑起來,將可執行文件裝載到內存
  • 編譯時:正在編譯的時間——翻譯源代碼將高級語言(OC、Swift)翻譯成機器語言(彙編等),最後變成二進制

② Runtime版本

Runtime有兩個版本——LegacyModern蘋果開發者文檔都寫得清清楚楚算法

源碼中-old__OBJC__表明Legacy版本,-new__OBJC2__表明Modern版本,以此作兼容緩存

③ Runtime的做用及調用

Runtime底層通過編譯會提供一套API和供FrameWorkService使用sass

Runtime調用方式:markdown

  • Runtime API,如 sel_registerName(),class_getInstanceSize
  • NSObject API,如 isKindOf()
  • OC上層方式,如 @selector()

原來日常在用的這麼多方法都是Runtime啊,那麼方法到底是什麼呢?app

2、方法的本質

① 研究方法

經過clang編譯成cpp文件能夠看到底層代碼,獲得方法的本質iphone

  • 兼容編譯(代碼少):clang -rewrite-objc main.m -o main.cpp
  • 完整編譯(不報錯):xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cppxcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

② 代碼轉換

  • ((TCJPerson *(*)(id, SEL))(void *)是類型強轉
  • (id)objc_getClass("TCJPerson")獲取TCJPerson類對象
  • sel_registerName("alloc")等同於@selector()

便可以理解爲((類型強轉)objc_msgSend)(對象, 方法調用)

③ 方法的本質

方法的本質是經過objc_msgSend發送消息,id是消息接收者,SEL是方法編號.

注意:若是外部定義了C函數並調用如void sayHello() {},在clang編譯以後仍是sayHello()而不是經過objc_msgSend去調用.由於發送消息就是找函數實現的過程,而C函數能夠經過函數名——指針就能夠找到.

爲了驗證,經過objc_msgSend方法來完成[person sayHello]的調用,查看其打印是不是一致. 其打印結果以下,發現是一致的,因此 [person sayHello]等價於objc_msgSend(person,sel_registerName("sayHello"))

這其中須要注意兩點:

  • 一、直接調用objc_msgSend,須要導入頭文件#import <objc/message.h>
  • 二、須要將target --> Build Setting -->搜索msg -- 將enable strict checking of obc_msgSend calls由YES 改成NO,將嚴厲的檢查機制關掉,不然objc_msgSend的參數會報錯

④ 向不一樣對象發送消息

子類TCJTeacher有實例方法sayHellosayNB, 類方法sayNC

父類TCJPerson有實例方法sayHellosayCode, 類方法sayNA

① 發送實例方法

消息接收者——實例對象

② 發送類方法

③ 對象方法調用-實際執行是父類的實現

注意前面的細節:父類TCJPerson中實現了sayHello方法,而子類TCJTeacher沒有實現sayHello方法.如今咱們能夠嘗試讓teacher調用sayHello執行父類中實現,經過objc_msgSendSuper實現.

由於objc_msgSend不能向父類發送消息,須要使用objc_msgSendSuper,並給objc_super結構體賦值(在objc2中只須要賦值receiversuper_class)

receiver——實例對象;super_class——父類類對象 發現不管是[teacher sayHello]仍是objc_msgSendSuper都執行的是父類中sayHello的實現,因此這裏,咱們能夠做一個猜想:方法調用,首先是在類中查找,若是類中沒有找到,會到類的父類中查找.

④ 向父類發送實例方法

receiver——實例對象;super_class——父類類對象

⑤ 向父類發送類方法

receiver——類對象;super_class——父類元類對象

3、消息查找流程

消息查找流程實際上是經過上層的方法編號sel發送消息objc_msgSend找到具體實現imp的過程

objc_msgSend是用匯編寫成的,至於爲何不用C而用匯編寫,是由於:

  • C語言不能經過寫一個函數,保留未知的參數,跳轉到任意的指針,而彙編有寄存器
  • 對於一些調用頻率過高的函數或操做,使用匯編來實現可以提升效率和性能,容易被機器來識別

① 開始查找

打開objc4源碼,因爲主要研究arm64結構的彙編實現,來到objc-msg-arm64.s,先附上其彙編總體執行的流程圖

p0表示0寄存器的指針,x0表示它的值,w0表示低32位的值(不用過多在乎)

  • ①開始objc_msgSend
  • ②判斷消息接收者是否爲空,爲空直接返回
  • ③判斷tagged_pointers(以後會講到)
  • ④取得對象中的isa存一份到p13
  • ⑤根據isa進行mask地址偏移獲得對應的上級對象(類、元類)

查看GetClassFromIsa_p16定義,主要就是進行isa & mask獲得class操做

  • ⑥開始在緩存中查找imp——開始了快速流程

② 快速查找流程

CacheLookup開始了快速查找流程(此時x1selx16class

  • ①經過cache首地址平移16字節(由於在objc_class中,首地址距離cache正好16字節,即isa首地址 佔8字節,superClass8字節),獲取cahcecache中高16位存mask,低48位存buckets,即p11 = cache

  • ②從cache中分別取出bucketsmask,並由mask根據哈希算法計算出哈希下標

    • 經過cache掩碼(即0x0000ffffffffffff)& 運算,將高16位mask抹零,獲得buckets指針地址,即p10 = buckets
    • cache右移48位,獲得mask,即p11 = mask
    • objc_msgSend的參數p1(即第二個參數_cmd)& msak,經過哈希算法,獲得須要查找存儲sel-impbucket下標index,即p12 = index = _cmd & mask,爲何經過這種方式呢?由於在存儲sel-imp時,也是經過一樣哈希算法計算哈希下標進行存儲,因此讀取也須要經過一樣的方式讀取,以下所示
  • ③根據所得的哈希下標indexbuckets首地址,取出哈希下標對應的bucket

    • 其中PTRSHIFT等於3,左移4位(即2^4 = 16字節)的目的是計算出一個bucket實際佔用的大小,結構體bucket_tsel8字節,imp8字節
    • 根據計算的哈希下標index 乘以 單個bucket佔用的內存大小,獲得buckets首地址在實際內存中的偏移量
    • 經過首地址 + 實際偏移量,獲取哈希下標index對應的bucket
  • ④根據獲取的bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel

  • ⑤第一次遞歸循環

    • 比較獲取的bucketselobjc_msgSend的第二個參數的_cmd(即p1)是否相等
    • 若是相等,則直接跳轉至CacheHit,即緩存命中,返回imp
    • 若是不相等,有如下兩種狀況
      • 若是一直都找不到,直接跳轉至CheckMiss,由於$0normal,會跳轉至__objc_msgSend_uncached,即進入慢速查找流程
      • 若是根據index獲取的bucket 等於 buckets的第一個元素,則人爲的將當前bucket設置爲buckets的最後一個元素(經過buckets首地址+mask右移44位(等同於左移4位)直接定位到bucker的最後一個元素),而後繼續進行遞歸循環(第一個遞歸循環嵌套第二個遞歸循環),即⑥
      • 若是當前bucket不等於buckets的第一個元素,則繼續向前查找,進入第一次遞歸循環
  • ⑥第二次遞歸循環:重複⑤的操做,與⑤中惟一區別是,若是當前的bucket仍是等於 buckets的第一個元素,則直接跳轉至JumpMiss,此時的$0normal,也是直接跳轉至__objc_msgSend_uncached,即進入慢速查找流程

如下是整個快速查找過程值的變化過程流程圖

③ 慢速查找流程

① 慢速查找-彙編部分

在快速查找流程中,若是沒有找到方法實現,不管是走到CheckMiss仍是JumpMiss,最終都會走到__objc_msgSend_uncached彙編函數

  • objc-msg-arm64.s文件中查找__objc_msgSend_uncached的彙編實現,其中的核心是MethodTableLookup(即查詢方法列表),其源碼以下

  • 搜索MethodTableLookup的彙編實現,其中的核心是_lookUpImpOrForward,彙編源碼實現以下

驗證 上述彙編的過程,能夠經過彙編調試來驗證

  • main中,例如[person sayHello]對象方法調用處加一個斷點,而且開啓彙編調試【Debug -- Debug worlflow -- 勾選Always show Disassembly】,運行程序
  • 彙編中objc_msgSend加一個斷點,執行斷住,按住control + stepinto,進入objc_msgSend的彙編
  • _objc_msgSend_uncached加一個斷點,執行斷住,按住control + stepinto,進入彙編

從上能夠看出最後走到的就是lookUpImpOrForward,此時並非彙編實現.

注意

  • 一、C/C++中調用 彙編 ,去查找彙編時C/C++調用的方法須要多加一個下劃線
  • 二、彙編 中調用 C/C++方法時,去查找C/C++方法,須要將彙編調用的方法去掉一個下劃線

② 慢速查找-C/C++部分

根據彙編部分的提示,全局續搜索lookUpImpOrForward,最後在objc-runtime-new.mm文件中找到了源碼實現,這是一個c實現的函數 其總體的慢速查找流程如圖所示 慢速流程主要分爲幾個步驟:

  • cache緩存中進行查找,即快速查找,找到則直接返回imp,反之,則進入②
  • ②判斷cls
    • 是不是已知類,若是不是,則報錯
    • 類是否實現,若是沒有,則須要先實現,肯定其父類鏈,此時實例化的目的是爲了肯定父類鏈、ro、以及rw等,方便後續數據的讀取以及查找的循環
    • 是否初始化,若是沒有,則初始化
  • for循環,按照類繼承鏈 或者 元類繼承鏈的順序查找
    • 當前cls的方法列表中使用二分查找算法查找方法,若是找到,則進入cache寫入流程(在iOS之武功祕籍⑤:cache_t分析文章中已經詳述過),並返回imp,若是沒有找到,則返回nil
    • 當前cls被賦值爲父類,若是父類等於nil,則imp = 消息轉發,並終止遞歸,進入④
    • 若是父類鏈中存在循環,則報錯,終止循環
    • 父類緩存中查找方法
      • 若是未找到,則直接返回nil,繼續循環查找
      • 若是找到,則直接返回imp,執行cache寫入流程
  • 判斷是否執行過動態方法解析
    • 若是沒有,執行動態方法解析
    • 若是執行過一次動態方法解析,則走到消息轉發流程

以上就是方法的慢速查找流程,下面在分別詳細解釋二分查找原理 以及 父類緩存查找詳細步驟

getMethodNoSuper_nolock方法:二分查找方法列表

查找方法列表的流程以下所示

其二分查找核心的源碼實現以下 算法原理簡述爲:從第一次查找開始,每次都取中間位置,與想查找的key的value值做比較,若是相等,則須要排除分類方法,而後將查詢到的位置的方法實現返回,若是不相等,則須要繼續二分查找,若是循環至count = 0仍是沒有找到,則直接返回nil,以下所示:

以查找TCJPerson類的sayHello實例方法爲例,其二分查找過程以下

cache_getImp方法:父類緩存查找

cache_getImp方法是經過彙編_cache_getImp實現,傳入的$0GETIMP,以下所示

  • 若是父類緩存中找到了方法實現,則跳轉至CacheHit即命中,則直接返回imp
  • 若是在父類緩存中,沒有找到方法實現,則跳轉至CheckMiss 或者 JumpMiss,經過判斷$0 跳轉至LGetImpMiss,直接返回nil.

總結

  • 對於對象方法(即實例方法),即在類中查找,其慢速查找的父類鏈是:類--父類--根類--nil
  • 對於類方法,即在元類中查找,其慢速查找的父類鏈是:元類--根元類--根類--nil
  • 若是快速查找、慢速查找也沒有找到方法實現,則嘗試動態方法決議
  • 若是動態方法決議仍然沒有找到,則進行消息轉發
常見方法未實現報錯源碼

若是在快速查找、慢速查找、方法解析流程中,均沒有找到實現,則使用消息轉發,其流程以下

消息轉發會實現

  • 其中_objc_msgForward_impcache是彙編實現,會跳轉至__objc_msgForward,其核心是__objc_forward_handler

  • 彙編實現中查找__objc_forward_handler,並無找到,在源碼中去掉一個下劃線進行全局搜索_objc_forward_handler,有以下實現,本質是調用的objc_defaultForwardHandler方法

看着objc_defaultForwardHandler有沒有很眼熟,這就是咱們在平常開發中最多見的錯誤:沒有實現函數,運行程序,崩潰時報的錯誤提示.

🌰:定義TCJPerson父類,其中有sayNB實例方法 和 sayHappay類方法

定義子類:TCJStudent類,有實例方法sayHellosayMaster,類方法sayObjc,其中實例方法sayMaster未實現.

main中 調用TCJStudend的實例方法sayMaster ,運行程序報錯,提示方法未實現,以下所示

下面,咱們來說講如何在崩潰前,如何操做,能夠防止方法未實現的崩潰.

4、動態方法解析

慢速查找流程未找到方法實現時,首先會嘗試一次動態方法決議,其源碼實現以下: 主要分爲如下幾步

  • 判斷類是不是元類
    • 若是是,執行實例方法的動態方法決議resolveInstanceMethod
    • 若是是元類,執行類方法的動態方法決議resolveClassMethod,若是在元類中沒有找到或者爲,則在元類實例方法的動態方法決議resolveInstanceMethod中查找,主要是由於類方法在元類中是實例方法,因此還須要查找元類中實例方法的動態方法決議
  • 若是動態方法決議中,將其實現指向了其餘方法,則繼續查找指定的imp,即繼續慢速查找lookUpImpOrForward流程

其流程以下

① 實例方法

針對實例方法調用,在快速-慢速查找均沒有找到實例方法的實現時,咱們有一次挽救的機會,即嘗試一次動態方法決議,因爲是實例方法,因此會走到resolveInstanceMethod方法,其源碼以下 主要分爲如下幾個步驟:

  • 在發送resolveInstanceMethod消息前,須要查找cls類中是否有該方法的實現,即經過lookUpImpOrNil方法又會進入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 若是沒有,則直接返回
    • 若是有,則發送resolveInstanceMethod消息
  • 再次慢速查找實例方法的實現,即經過lookUpImpOrNil方法又會進入lookUpImpOrForward慢速查找流程查找實例方法

② 崩潰修改--動態方法決議

針對實例方法say666未實現的報錯崩潰,能夠經過在中重寫resolveInstanceMethod類方法,並將其指向其餘方法的實現,即在TCJPerson中重寫resolveInstanceMethod類方法,將實例方法say666的實現指向sayMaster方法實現,以下所示

假如咱們在resolveInstanceMethod類方法中,不指向其餘方法的實現,它會來兩次,爲何會這樣呢?咱們在後面在解釋...

③ 類方法

針對類方法,與實例方法相似,一樣能夠經過重寫resolveClassMethod類方法來解決前文的崩潰問題,即在TCJPerson類中重寫該方法,並將sayNB類方法的實現指向類方法sayHappy resolveClassMethod類方法的重寫須要注意一點,傳入的cls再也不是類而是元類,能夠經過objc_getMetaClass方法獲取類的元類,緣由是由於類方法在元類中是實例方法.

④ 優化方案

上面的這種方式是單獨在每一個類中重寫,有沒有更好的,一勞永逸的方法呢?其實經過方法慢速查找流程能夠發現其查找路徑有兩條

  • 實例方法:類 -- 父類 -- 根類 -- nil
  • 類方法:元類 -- 根元類 -- 根類 -- nil

它們的共同點是若是前面沒找到,都會來到根類即NSObject中查找,因此咱們是否能夠將上述的兩個方法統一整合在一塊兒呢?答案是能夠的,能夠經過NSObject添加分類的方式來實現統一處理,並且因爲類方法的查找,在其繼承鏈,查找的也是實例方法,因此能夠將實例方法 和 類方法的統一處理放在resolveInstanceMethod方法中,以下所示 這種方式的實現,正好與源碼中針對類方法的處理邏輯是一致的,即完美闡述爲何調用了類方法動態方法決議,還要調用對象方法動態方法決議,其根本緣由仍是類方法在元類中是實例方法.

固然,上面這種寫法仍是會有其餘的問題,好比系統方法也會被更改,針對這一點,是能夠優化的,即咱們能夠針對自定義類中方法統一方法名的前綴,根據前綴來判斷是不是自定義方法,而後統一處理自定義方法,例如能夠在崩潰前pop到首頁,主要是用於app線上防崩潰的處理,提高用戶的體驗.

⑤ 動態方法決議總結

  • 實例方法能夠重寫resolveInstanceMethod添加imp
  • 類方法能夠在本類重寫resolveClassMethod元類添加imp,或者在NSObject分類重寫resolveInstanceMethod添加imp
  • 動態方法解析只要在任意一步lookUpImpOrNil查找到imp就不會查找下去——即本類作了動態方法決議,不會走到NSObjct分類的動態方法決議
  • 全部方法均可以經過在NSObject分類重寫resolveInstanceMethod添加imp解決崩潰

那麼把全部崩潰都在NSObjct分類中處理,加之前綴區分業務邏輯,豈不是美滋滋?錯!

  • 統一處理起來耦合度高
  • 邏輯判斷多
  • 可能在NSObjct分類動態方法決議以前已經作了處理
  • SDK封裝的時候須要給一個容錯空間

所以前面的 ④ 優化方案 也不是一個最完美的解決方案.那麼,這也不行,那也不行,那該怎麼辦?放心,蘋果爸爸已經給咱們準備好後路了!

5、消息轉發機制

在慢速查找的流程(lookUpImpOrForward)中,咱們瞭解到,若是快速+慢速沒有找到方法實現,動態方法決議也不行,就使用消息轉發,可是,咱們找遍了源碼也沒有發現消息轉發的相關源碼,能夠經過如下方式來了解,方法調用崩潰前都走了哪些方法

  • 經過instrumentObjcMessageSends方式打印發送消息的日誌

instrumentObjcMessageSends

經過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源碼下方找到instrumentObjcMessageSends的源碼實現,因此,在main中調用instrumentObjcMessageSends打印方法調用的日誌信息,有如下兩點準備工做

  • 一、打開 objcMsgLogEnabled 開關,即調用instrumentObjcMessageSends方法時,傳入YES

  • 二、在main中經過extern 聲明instrumentObjcMessageSends方法

  • 經過logMessageSend源碼,瞭解到消息發送打印信息存儲在/tmp/msgSends 目錄,以下所示

  • 運行代碼,並前往/tmp/msgSends 目錄,發現有msgSends開頭的日誌文件,打開發如今崩潰前,執行了如下方法

    • 兩次動態方法決議:resolveInstanceMethod方法
    • 兩次消息快速轉發:forwardingTargetForSelector方法
    • 兩次消息慢速轉發:methodSignatureForSelector + resolveInvocation

快速轉發流程

forwardingTargetForSelector在源碼中只有一個聲明,並無其它描述,好在幫助文檔中提到了關於它的解釋:

  • 該方法的返回對象是執行sel的新對象,也就是本身處理不了會將消息轉發給別的對象進行相關方法的處理,可是不能返回self,不然會一直找不到
  • 該方法的效率較高,若是不實現,會走到forwardInvocation:方法進行處理
  • 底層會調用objc_msgSend(forwardingTarget, sel, ...);來實現消息的發送
  • 被轉發消息的接受者參數、返回值等應和原方法相同

快速轉發流程解決崩潰

以下代碼就是經過快速轉發解決崩潰——即TCJPerson實現不了的方法,轉發給TCJStudent去實現(轉發給已經實現該方法的對象)

也能夠直接不指定消息接收者,直接調用父類的該方法,若是仍是沒有找到,則直接報錯

慢速轉發流程

在快速轉發流程找不到轉發的對象後,會來到慢速轉發流程methodSignatureForSelector 依葫蘆畫瓢,在幫助文檔中找到methodSignatureForSelector 點擊查看forwardInvocation

  • forwardInvocationmethodSignatureForSelector必須是同時存在的,底層會經過方法簽名,生成一個NSInvocation,將其做爲參數傳遞調用
  • 查找能夠響應NSInvocation中編碼的消息的對象(對於全部消息,此對象沒必要相同)
  • 使用anInvocation將消息發送到該對象.anInvocation將保存結果,運行時系統將提取結果並將其傳遞給原始發送者

慢速轉發流程解決崩潰

慢速轉發流程就是先methodSignatureForSelector提供一個方法簽名,而後forwardInvocation經過對NSInvocation來實現消息的轉發

其實也能夠對forwardInvocation方法中的invocation不進行處理,也不會崩潰報錯

因此,由上述可知,不管在forwardInvocation方法中是否處理invocation事務,程序都不會崩潰.

經過hopper/IDA反彙編消息轉發機制

Hopper和IDA是一個能夠幫助咱們靜態分析可視性文件的工具,能夠將可執行文件反彙編成僞代碼、控制流程圖等,下面以Hopper爲例.

  • 運行程序崩潰,查看堆棧信息

  • 發現___forwarding___來自CoreFoundation

  • 經過image list,讀取整個鏡像文件,而後搜索CoreFoundation,查看其可執行文件的路徑

  • 經過文件路徑,找到CoreFoundation的可執行文件

  • 打開hopper,選擇Try the Demo,而後將上一步的可執行文件拖入hopper進行反彙編,選擇x86(64 bits)

  • 如下是反彙編後的界面,主要使用上面的三個功能,分別是 彙編、流程圖、僞代碼

  • 經過左側的搜索框搜索__forwarding_prep_0___,而後選擇僞代碼

  • 如下是__forwarding_prep_0___的彙編僞代碼,跳轉至___forwarding___

  • 如下是___forwarding___的僞代碼實現,首先是查看是否實現forwardingTargetForSelector方法,若是沒有響應,跳轉至loc_6459b即快速轉發沒有響應,進入慢速轉發流程

  • 跳轉至loc_6459b,在其下方判斷是否響應methodSignatureForSelector方法

  • 若是沒有響應,跳轉至loc_6490b,則直接報錯

  • 若是獲取methodSignatureForSelector的方法簽名爲nil,也是直接報錯

  • 若是methodSignatureForSelector返回值不爲空,則在forwardInvocation方法中對invocation進行處理

經過上面兩種查找方式能夠驗證,消息轉發的方法有3個

  • 【快速轉發】forwardingTargetForSelector
  • 【慢速轉發】
    • methodSignatureForSelector
    • forwardInvocation

消息轉發總體的流程以下

消息轉發的處理主要分爲兩部分:

  • 【快速轉發】當慢速查找,以及動態方法決議均沒有找到實現時,進行消息轉發,首先是進行快速消息轉發,即走到forwardingTargetForSelector方法
    • 若是返回消息接收者,在消息接收者中仍是沒有找到方法實現,則進入另外一個方法的查找流程
    • 若是返回nil,則進入慢速消息轉發
  • 【慢速轉發】執行到methodSignatureForSelector方法
    • 若是返回的方法簽名nil,則直接崩潰報錯
    • 若是返回的方法簽名不爲nil,走到forwardInvocation方法中,對invocation事務進行處理,若是不處理也不會報錯

6、動態方法決議爲何執行兩次?

在前文中說起了動態方法決議方法執行了兩次,有如下兩種分析方式

啓用上帝視角的探索

在慢速查找流程中,咱們瞭解到resolveInstanceMethod方法的執行是經過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod來到resolveInstanceMethod源碼,在源碼中經過發送resolve_sel消息觸發,以下所示 因此能夠在resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個斷點,經過bt打印堆棧信息來看到底發生了什麼

  • resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個斷點,運行程序,直到第一次「來了」,經過bt查看第一次動態方法決議的堆棧信息,此時的selsay666

  • 繼續往下執行,直到第二次「來了」打印,查看堆棧信息,在第二次中,咱們能夠看到是經過CoreFoundation-[NSObject(NSObject) methodSignatureForSelector:]方法,而後經過class_getInstanceMethod再次進入動態方法決議

  • 經過上一步的堆棧信息,咱們須要去看看CoreFoundation中到底作了什麼?經過Hopper反彙編CoreFoundation的可執行文件,查看methodSignatureForSelector方法的僞代碼

  • 經過methodSignatureForSelector僞代碼進入___methodDescriptionForSelector的實現

  • 進入 ___methodDescriptionForSelector的僞代碼實現,結合彙編的堆棧打印,能夠看到,在___methodDescriptionForSelector這個方法中調用了objc4源碼class_getInstanceMethod

  • objc4-818.2源碼中搜索class_getInstanceMethod,其源碼實現以下所示

這一點能夠經過代碼調試來驗證,以下所示,在class_getInstanceMethod方法處加一個斷點,在執行了methodSignatureForSelector方法後,返回了簽名,說明方法簽名是生效的,蘋果在走到invocation以前,給了開發者一次機會再去查詢,因此走到class_getInstanceMethod這裏,又去走了一遍方法查詢say666,而後會再次走到動態方法決議

因此,上述的分析也印證了前文中resolveInstanceMethod方法執行了兩次的緣由

無上帝視角的探索

若是在沒有上帝視角的狀況下,咱們也能夠經過代碼來推導在哪裏再次調用了動態方法決議

  • TCJPerson類中重寫resolveInstanceMethod方法,並加上class_addMethod操做即賦值IMP,此時resolveInstanceMethod會走兩次嗎?

經過運行發現,若是賦值了IMP,動態方法決議只會走一次,說明不是在這裏走第二次動態方法決議

繼續往下探索

  • 去掉resolveInstanceMethod方法中的賦值IMP,在TCJPerson類中重寫forwardingTargetForSelector方法,並指定返回值爲[TCJStudent alloc],從新運行,若是resolveInstanceMethod打印了兩次,說明是在forwardingTargetForSelector方法以前執行了動態方法決議,反之,在forwardingTargetForSelector方法以後

結果發現resolveInstanceMethod中的打印仍是隻打印了一次,那說明第二次動態方法決議 在forwardingTargetForSelector方法後

  • TCJPerson類中重寫 methodSignatureForSelectorforwardInvocation,運行

結果發現第二次動態方法決議在 methodSignatureForSelectorforwardInvocation方法之間.

第二種分析一樣能夠論證前文中resolveInstanceMethod執行了兩次的緣由. 通過上面的論證,咱們瞭解到其實在慢速消息轉發流程中,在methodSignatureForSelectorforwardInvocation方法之間還有一次動態方法決議,即蘋果再次給的一個機會,以下圖所示

寫在後面

到目前爲止,objc_msgSend發送消息的流程就分析完成了,在這裏簡單總結下

  • 【快速查找流程】首先,在類的緩存cache中查找指定方法的實現
  • 【慢速查找流程】若是緩存中沒有找到,則在類的方法列表中查找,若是仍是沒找到,則去父類鏈的緩存和方法列表中查找
  • 【動態方法決議】若是慢速查找仍是沒有找到時,第一次補救機會就是嘗試一次動態方法決議,即重寫resolveInstanceMethod/resolveClassMethod 方法
  • 【消息轉發】若是動態方法決議仍是沒有找到,則進行消息轉發,消息轉發中有兩次補救機會:快速轉發+慢速轉發
  • 若是轉發以後也沒有,則程序直接報錯崩潰unrecognized selector sent to instance

最後,和諧學習,不急不躁.我仍是我,顏色不同的煙火.

相關文章
相關標籤/搜索