小碼哥iOS學習筆記第十三天:訊息傳送

冰凌天發表於2019-03-12

一、objc_msgSend

  • 建立命令列專案, 定義Person類, 並新增personTest方法

小碼哥iOS學習筆記第十三天:訊息傳送

  • main函式中使用Person

小碼哥iOS學習筆記第十三天:訊息傳送

  • 使用終端, 執行下面的命令, 生成main.m在底層的main.cpp檔案
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製程式碼
  • 開啟main.cpp, 可以看到底層程式碼如下

小碼哥iOS學習筆記第十三天:訊息傳送

  • 其中[person personTest][Person initialize]轉化為了下面的程式碼
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personTest"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("initialize"));
複製程式碼
  • 去掉型別轉換後, 底層程式碼如下
objc_msgSend(person, sel_registerName("personTest"));
objc_msgSend(objc_getClass("Person"), sel_registerName("initialize"));
複製程式碼
  • OC中呼叫方法, 會使用objc_msgSend函式給訊息接收者傳送訊息
  • 所以OC呼叫方法的過程也被稱為訊息機制
  • 上面兩個方法呼叫的訊息接收者和訊息名稱如下
objc_msgSend(person, sel_registerName("personTest"));
訊息接收者: person
訊息名稱: personTest

objc_msgSend(objc_getClass("Person"), sel_registerName("initialize"));
訊息接收者: objc_getClass("Person") == [Person class]
訊息名稱: initialize
複製程式碼

二、objc_msgSend底層原始碼

  • 可以在官方提供的原始碼中找到objc_msgSend的底層實現, 可以看到objc_msgSend底層是由彙編實現的

小碼哥iOS學習筆記第十三天:訊息傳送

1、當訊息接收者為空時, 直接返回, 結束objc_msgSend函式的呼叫

  • 通過原始碼可以看到, objc_msgSend執行一開始, 會先判斷訊息接收者是否為空, 如果為空直接返回

小碼哥iOS學習筆記第十三天:訊息傳送

  • 可以寫出模擬程式碼如下
void objc_msgSend(id receiver, SEL selector) {
    // 當 訊息接收者 為空時, 直接返回, 結束函式呼叫
    if (receiver == nil) return;
}
複製程式碼

2、當訊息接收者有值時, 檢視快取

  • 當訊息接收者有值時, 會呼叫CacheLookup

小碼哥iOS學習筆記第十三天:訊息傳送

  • CacheLookup, 首先會查詢cache快取, 查詢是否已經快取了方法, 如果已經快取會直接呼叫

小碼哥iOS學習筆記第十三天:訊息傳送

3、如果方法沒有被快取過, 就會查詢方法列表

  • 當方法還沒有快取, 會從方法列表中查詢, 此時呼叫CheckMiss, 傳入NORMAL

小碼哥iOS學習筆記第十三天:訊息傳送

  • 找到CheckMissNORMAL部分, 呼叫__objc_msgSend_uncached

小碼哥iOS學習筆記第十三天:訊息傳送

  • 接著呼叫MethodTableLookup

小碼哥iOS學習筆記第十三天:訊息傳送

  • 接著可以看到呼叫了__class_lookupMethodAndLoadCache3函式

小碼哥iOS學習筆記第十三天:訊息傳送

  • 搜尋__class_lookupMethodAndLoadCache3函式, 可以發現並沒有__class_lookupMethodAndLoadCache3的實現部分

小碼哥iOS學習筆記第十三天:訊息傳送

  • 這主要是因為彙編中的__class_lookupMethodAndLoadCache3函式, 在C中的函式名需要去掉一個下劃線_, 所以函式名應該是_class_lookupMethodAndLoadCache3
  • 查詢_class_lookupMethodAndLoadCache3, 可以發現_class_lookupMethodAndLoadCache3函式的實現部分

小碼哥iOS學習筆記第十三天:訊息傳送

  • 繼續進入lookUpImpOrForward函式, 可以看到當傳入cacheYES時, 會查詢快取中的方法
  • 此時傳入cache的是NO, 所以不會在呼叫cache_getImp函式從快取中查詢方法

小碼哥iOS學習筆記第十三天:訊息傳送

  • 然而我們還能看到下面還有從快取中查詢方法的程式碼, 如果找到直接跳轉返回

小碼哥iOS學習筆記第十三天:訊息傳送

  • 這主要是因為在下圖過程中, 開發者有可能會動態新增一些方法到快取中, 所以才會再次查詢一次

小碼哥iOS學習筆記第十三天:訊息傳送

  • 當快取中沒有找到需要呼叫的方法時, 就會在方法列表中查詢, 如果找到就會存到快取cache

小碼哥iOS學習筆記第十三天:訊息傳送

  • 已經知道, 方法存在cls->rw->methods中, 而methods是個二位陣列, 所以需要進行遍歷查詢, 這裡先拿到一維陣列呼叫search_method_list函式查詢

小碼哥iOS學習筆記第十三天:訊息傳送

  • 在一維陣列中查詢方法, 這裡有兩種情況
    • 第一種: 方法列表已經排好序, 會通過findMethodInSortedMethodList函式查詢
    • 第二種: 方法列表沒有排好序, 會一個一個遍歷查詢

小碼哥iOS學習筆記第十三天:訊息傳送

  • findMethodInSortedMethodList函式使用的是二分查詢的方式查詢方法

小碼哥iOS學習筆記第十三天:訊息傳送

  • 當找到方法後, 會先將方法儲存到cache中, 呼叫log_and_fill_cache函式進行儲存

小碼哥iOS學習筆記第十三天:訊息傳送

  • log_and_fill_cache中呼叫cache_fill函式

小碼哥iOS學習筆記第十三天:訊息傳送

  • cache_fill函式中呼叫cache_fill_nolock函式

小碼哥iOS學習筆記第十三天:訊息傳送

  • cache_fill_nolock函式中,可以看到方法儲存到了buckets

小碼哥iOS學習筆記第十三天:訊息傳送

  • 如果在自己的類物件中沒有找到需要呼叫的方法, 就會去查詢父類中是否有該方法
    • 1.查詢時會一層一層遍歷所有父類, 只要某個父類中找到方法, 就會結束查詢
    • 2.先從父類的快取中找, 如果找到, 會先存到自己的cache
    • 3.如果父類的快取中沒有該方法, 就會從父類的方法列表中查詢, 如果找到就會存入到自己的cache中, 並不會存入到父類的cache
    • 4.如果沒找到, 就會通過for迴圈檢視父類的父類中有沒有方法, 依次類推, 只要找到就會結束查詢, 並存到自己的cache

小碼哥iOS學習筆記第十三天:訊息傳送

  • 如果最後還是沒找到, 就會進入下一個階段, 動態解析階段

4、訊息機制流程圖

小碼哥iOS學習筆記第十三天:訊息傳送

相關文章