iOS-底層原理 18:類的載入(下)
在上一篇文章iOS-底層原理 17:類的載入(上)中,理解了類是如何從Mach-O載入到記憶體
中,這次我們來解釋下分類
是如何載入
到類
中的,以及分類和類搭配使用
的情況
分類的本質
前提:在main中定義LGperson的分類LG
探索分類的本質,有以下三種方式
- 【方式一】通過
clang
- 【方式二】通過
Xcode
文件搜尋Category
- 【方式三】通過
objc
原始碼搜尋category_t
方式一:通過clang
-
【方式一】
clang -rewrite-objc main.m -o main.cpp
檢視底層編譯,即 main.cpp,- 其中
分類
的 型別是_category_t
- 分類的倒數第二個0,表示的是
沒有協議
,所以賦值為0
- 其中
-
搜尋
struct _category_t
,如下所示- 其中有兩個method_list_t,分別表示
例項方法
和類方法
- 其中有兩個method_list_t,分別表示
-
搜尋
_CATEGORY_INSTANCE_METHODS_LGPerson_
,找到其底層實現
其中有3個方法,格式為:sel+簽名+地址
,是method_t
結構體的屬性即key
-
搜尋
method_t
,其中對應關係如下name
對應sel
type
對應方法簽名
imp
對應函式地址
同時,我們發現了一個問題:檢視看_prop_list_t
,明明分類
中定義了屬性
,但是在底層編譯中並沒有看到屬性,如下圖所示,這是因為分類中定義的屬性沒有相應的set、get方法
關聯物件
來設定(關於如何設定關聯物件
,我們將在後續的擴充套件中進行說明)方式二:通過Xcode文件搜尋 Category
如果不會clang
,可以通過Xcode
文件搜尋 Category
方式三:通過objc原始碼搜尋 category_t
還可以通過objc原始碼搜尋category_t
型別
總結
綜上所述,分類的本質
是一個_category_t
型別
-
有兩個屬性:
name(類的名稱)
和cls(類物件)
-
有兩個
method_list_t
型別的方法列表,表示分類中實現的例項方法+類方法
-
一個
protocol_list_t
型別的協議列表,表示分類中實現的協議 -
一個
prop_list_t
關聯物件
來實現 -
需要注意的是,分類中的
屬性
是沒有set、get
方法
分類的載入
前提:建立LGPerson的兩個分類:LGA、LGB
在上一篇iOS-底層原理 17:類的載入(上)文章中的realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories
中提及了rwe的載入,其中分析了分類的data
資料 時如何 載入到類
中的,且分類的載入順序是:LGA -> LGB
的順序載入到類中,即越晚加進來,越在前面
其中檢視methodizeClass
的原始碼實現,可以發現類的資料
和 分類的資料
是分開處理的,主要是因為在編譯階段
,就已經確定好了方法的歸屬位置
(即例項方法
儲存在類
中,類方法
儲存在元類
中),而分類
是後面才加進來的
其中分類需要通過attatchToClass
新增到類,然後才能在外界進行使用,在此過程,我們已經知道了分類載入三步驟的後面兩個步驟,分類的載入主要分為3步:
-
分類資料
載入時機
:根據類和分類是否實現load方法
來區分不同的時機 -
attachCategories
準備分類資料
-
attachLists
將分類資料新增到主類
中
分類的載入時機
下面我們來探索分類資料的載入時機
,以主類
LGPerson + 分類
LGA、LGB 均實現+load
方法為例
通過第二步資料準備反推第一步的載入時機
-
通過上一篇文章我們瞭解到,在走到
attachCategories
方法時,必然會有分類資料的載入
,可以通過反推法
檢視 在什麼時候呼叫attachCategories
的,通過查詢,有兩個方法中呼叫load_categories_nolock
方法中
addToClass
方法中,這裡經過除錯發現,從來不會進到if流程中,除非載入兩次,一般的類一般只會載入一次
-
不加任何斷點,執行objc程式碼,可以得出以下列印日誌,通過日誌可以發現
addToClass
方法的下一步就是load_categories_nolock
方法就是載入分類資料
-
全域性搜尋
load_categories_nolock
的呼叫,有兩次呼叫- 一次在
loadAllCategories
方法中
- 一次在
_read_images
方法中
- 一次在
-
但是經過除錯發現,是不會走
_read_images
方法中的if流程的,而是走的loadAllCategories
方法中的
-
全域性搜尋檢視
loadAllCategories
的呼叫,發現是在load_images
時呼叫的
通過堆疊資訊分析
- 在
attachCategories
中加自定義邏輯的斷點,bt
檢視堆疊資訊
所以綜上所述,該情況下的分類的資料載入時機
的反推路徑
為:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images
而我們的分類載入正常的流程的路徑為:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories
其中正向和反向的流程如下圖所示:
我們再來看一種情況:主類+分類LGA實現+load,分類LGB不實現+load方法
-
斷點定在
attachCategories
中加自定義邏輯部分,一步步往下執行p entry.cat
p *$0
-
繼續往下執行,會再次來到
attachCategories
方法中斷住p entry.cat
p *$0
總結:只要有一個分類是非懶載入分類,那麼所有的分類都會被標記位非懶載入分類
,意思就是載入一次 已經開闢了rwe
,就不會再次懶載入,重新去處理 LGPerson
分類和類的搭配使用
通過上面的兩個例子,我們可以大致將類 和 分類 是否實現+load的情況分為4種
類+分類 | |||
---|---|---|---|
分類實現+load | 分類未實現+load | ||
類實現+load | 非懶載入類+非懶載入分類 | 非懶載入類+懶載入分類 | |
類未實現+load | 懶載入類+非懶載入分類 | 懶載入類+懶載入分類 |
-
【情況1】
非懶載入類 + 非懶載入分類
-
【情況2】
非懶載入類 + 懶載入分類
-
【情況3】
懶載入類 + 懶載入分類
-
【情況4】
懶載入類 + 非懶載入分類
非懶載入類 與 非懶載入分類
即主類實現了+load方法,分類同樣實現了+load方法
,在前文分類的載入時機時,我們已經分析過這種情況,所以可以直接得出結論,這種情況下
-
類的資料載入是通過
_getObjc2NonlazyClassList
載入,即ro、rw的操作,對rwe
賦值初始化,是在extAlloc
方法中 -
分類的資料載入
是通過load_images
載入到類中的
其呼叫路徑為:
-
map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
,此時的mlists
是一維陣列,然後走到load_images
部分 -
load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists
,此時的mlists
是二維陣列
下面為原始碼中除錯的列印日誌
非懶載入類 與 懶載入分類
即主類實現了+load方法,分類未實現+load方法
-
開啟
realizeClassWithoutSwift
中的自定義斷點,看一下ro- 檢視
kc_ro
- p kc_ro->baseMethodList
- p $1.get(0) ~ p $1.get(4)
- p $1.get(5)、 p $1.get(10)
從上面的列印輸出可以看出,方法的順序是LGB—LGA-LGPerson類
,此時分類已經 載入進來了,但是還沒有排序,說明在沒有進行非懶載入時,通過cls->data
讀取Mach-O
資料時,資料就已經編譯進來了,不需要執行時新增進去
- 檢視
-
來到
methodizeClass
方法中斷點部分p list
p $0->get(0)
-p $0->get(5)
-
來到
prepareMethodLists
的for迴圈部分- p addedLists[0]
- p addedLists[1]
- p *$1
- p *$2
-
來到
fixupMethodList
方法中的if (sort) {
部分- 其中
SortBySELAddress
的原始碼實現如下:根據名字的地址進行排序
- 其中
-
走到
mlist->setFixedUp();
,在讀取listp mlist
p $7->get(0)
~p $7->get(3)
p $7->get(4)
~p $7->get(6)
通過打印發現,僅對同名方法進行了排序
,而分類中的其他方法是不需要排序的,其你imp地址是有序的(從小到大) –fixupMethodList
中的排序只針對 name 地址進行排序
-
不加任何斷點,執行程式,獲取列印日誌
總結:非懶載入類 與 懶載入分類
的資料載入,有如下結論:
-
類 和 分類的載入
是在read_images
就載入資料了 -
其中
data資料
在編譯時期
就已經完成了
懶載入類 與 懶載入分類
即主類和分類均未實現+load方法
-
不加任何斷點,執行程式,獲取列印日誌
其中realizeClassMaybeSwiftMaybeRelock
是訊息流程中慢速查詢中有的函式,即在第一次呼叫訊息時
才有的函式 -
在
readClass
斷住,然後讀取kc_ro
,即讀取整個data
此時的baseMethodList
的count
還是16,說明也是從data中讀取出來
的,所以不需要經過一層緩慢的load_images
載入進來
總結:懶載入類 與 懶載入分類
的資料載入
是在訊息第一次呼叫
時記載
懶載入類 與 非懶載入分類
即主類未實現+load方法, 分類實現了+load方法
-
不加任何斷點,執行程式,獲取列印日誌
-
在列印的日誌中沒有看到
load_categories_nolock
方法,檢視attachCategories -- extAlloc -- attachToClass -- attachCategories
,在attachToClass中加斷點
-
在
readClass
方法中斷住,檢視kc_ro
其中baseMethodList
的count是8個,列印看看:物件方法3個+屬性的setget方法共4個+1個cxx方法 ,即 現在只有主類的資料
- 檢視kc_ro結構
- p kc_ro->baseMethodList
- p $0->get(0) ~ p $0->get(3)、p $0->get(7)
- 檢視kc_ro結構
-
為了除錯分類的資料載入, 繼續往下執行,
bt
檢視堆疊:load_images -> loadAllCategories -> load_categories_nolock
總結:懶載入類 + 非懶載入分類
的資料載入,只要分類實現了load,會迫使主類提前載入
,即 主類 強行轉換為 非懶載入類樣式
總結
類和分類
搭配使用,其資料的載入時機
總結如下:
-
【情況1】
非懶載入類 + 非懶載入分類
,其資料的載入在load_images
方法中,首先對類進行載入,然後把分類的資訊貼到類中 -
【情況2】
非懶載入類 + 懶載入分類
,其資料載入在read_image
就載入資料,資料來自data
,data在編譯時期
就已經完成,即data中除了類的資料,還有分類的資料,與類繫結在一起 -
【情況3】
懶載入類 + 懶載入分類
,其資料載入推遲到 第一次訊息
時,資料同樣來自data
,data在編譯時期
就已經完成 -
【情況4】
懶載入類 + 非懶載入分類
,只要分類實現了load,會迫使主類提前載入
,即在_read_images
中不會對類做實現操作,需要在load_images
方法中觸發類的資料載入,即rwe初始化
,同時載入分類資料
如下圖所示