iOS底層原理之類的底層探索(下)

語言: CN / TW / HK

本文主要內容

1.探索ivar的儲存位置
2.ro、rw、rwe解析
3.類方法的儲存位置
4.關於元類的解釋
5.通過runtime的API探索類的資料結構

一、探索ivar的儲存位置

上一篇研究了類的部分底層,瞭解到isa的走點陣圖以及類物件和元類物件的繼承關係,並且知道類的本質是一個叫作objc_class的結構體,其中包含了ISA指標、(類物件的)父類、cachebits等,bits中儲存了類的屬性例項方法協議等內容,而這些內容都放在一個叫class_rw_t的結構體中。同時還留下2個疑問,第1個疑問是:成員變數並沒有在bits中屬性列表中展示。現在我們來研究類的成員變數到底儲存在什麼位置? 通過分析並檢視objc4-838.1原始碼發現,class_rw_t結構體中有1個叫作ro的結構體,返回class_ro_t結構體型別的資料,而class_ro_t結構體中包含ivars,即代表成員變數,也就是說類的成員變數就儲存在class_ro_t結構體中。

image.png

image.png

我們來驗證類的成員變數是否真的儲存在class_ro_t結構體中。方法同上一篇獲取bits裡面的methodlists和propertylist類似。讀取bits的返回class_data_bits_t資料內容,通過data函式讀取得到class_rw_t結構體型別資料,通過*取值,再呼叫ro函式獲取到class_ro_t結構體型別資料,其中ivars就是成員變數!呼叫ivars即可返回ovar_list_t型別資料,此時同前面屬性和方法的獲取一樣了。通過get函式獲取其中每個ivar型別元素的資料,從而找到累的成員變數(包含5個成員變數)如下圖:

image.png

image.png

知識小亮點
A:為何真正的成員變數放在類中,而成員變數的值卻放在例項物件中(前面已經知道,例項物件包含isa指標和成員變數的值)? 
Q:類的本質是一個結構體,這個結構體相當於一個模版,這個模版中有成員變數、屬性、方法、協議等記憶體,例項物件就是根據類的模版生成的,在建立例項物件時,不同的例項物件成員變數的值是不一樣的,所以就把不一樣的成員變數的值存放在不同的例項物件中。

二.ro、rw、rwe解析

ro是在編譯時生成,當類進行編譯時,類的屬性例項方法協議等就會存在於ro結構體中,它是一塊純淨的、只讀(read only)的記憶體空間,不允許被修改,即clean memory。rw是在執行時生成的,類一經使用ro就會變成rw,即rw會把ro中的內容"剪下"到rw中,可讀可寫(read&write)。而runtime提供了動態為類新增方法和屬性的API,這些方法和屬性存在於只讀不允許修改的ro中,如果想要修改方法和屬性(一般修改比例為10%左右),需要把ro中的內容"拷貝"到rw中,但這樣就會存在兩份ro,增加記憶體消耗。所以蘋果通過class_rw_ext_t(rwe)結構體來解決這10%左右的修改,這些修改主要指分類或者runtime API修改,其中分類和本類必須是非懶載入類

image.png

image.png

分析原始碼,會判斷rw是否存在rwe,如果存在rwe,就會在其中找需要修改的內容(方法、協議等),如果不存在rwe就會去ro中找。

image.png

三、類方法的儲存位置

2個疑問的另一個疑問是:類方法存在在哪裡?
猜測:上一篇文章中發現,類方法並不在類的bits資料中,那類方法是否會在類的元類的bits中呢?我們帶著疑問進行探索。首先通過類的isa指標指向找到元類的記憶體地址,再使用獲取bits中方法列表的方法找到元類中的方法列表即可(詳細分析過程此處省略,如有不清楚的地方,請檢視上一篇文章),分析如下圖:

image.png

知識小亮點
ro存在磁碟中,使用時記憶體載入。只要APP執行rw就會一直存在,APP殺死才會釋放。

結論類方法確實儲存在元類中!

四、關於元類的解釋

上一篇文章第二部分還有一個疑問:什麼是元類?為什麼要引出元類呢?也就是說蘋果為什麼要設計這個元類呢?
這是為了**複用訊息機制**,用同一套訊息機制。在OC中呼叫方法,如[HGPerson alloc],在蘋果系統中實際上就是給HGPerson傳送某條訊息,呼叫方法編譯時就會編譯為包含2個引數的函式objc_msgSend(訊息接收者 isa,訊息方法名),通過這個函式根據訊息接收者的isa指標找到該方法的實現,如訊息接收者是例項物件,就會到例項物件isa指標指向的類物件中找該方法的實現,如果訊息接收者是類物件,就會到類物件isa指標指向的元類物件中找該方法的實現(所以類方法和例項方法可同名)。
如果沒有元類,只用2個引數無法找到方法的實現,需要修改為:objc_msgSend(訊息接收者,訊息方法名,判斷例項物件/類物件,判斷例項方法/類方法),而訊息的傳送最重要的是快速,如果新增上述這些判斷結構會影響傳送效率!利用當前的訊息機制只通過isa指標可以很快找到方法的實現,例項物件儲存成員變數的值,類物件儲存例項物件的方法,元類物件儲存類物件的方法,也就是單一職責的原則,大大增加訊息傳送的效率,同時維護同一個訊息機制(objc_msgSend函式)也更方便。

五、通過runtime的API探索類的資料結構

1.獲取類的成員變數
成員變數為ivar_t型別的結構體,其中包含name、type、size等內容。

image.png 通過runtime中的class_copyIvarList函式拿到成員變數列表ivars,遍歷即可獲取所有的成員變數。

image.png

注意⚠️:class_copyIvarList方法返回的ivar *型別,該方法中使用malloc 開闢了記憶體空間,而ARC進行記憶體管理只會管理OC物件,所以需要用free釋放ivarsimage.png

2.獲取類的屬性
通過runtime中的class_copyPropertyList函式拿到屬性列表properties,遍歷即可獲取類的所有屬性name、age、height。具體實現如下圖:

image.png

列印屬性型別解析:
顯示如屬性name“[email protected]'NSString',C,N,V_name”。其中"T"代表型別,後面加"@‘NSString’即為字串型別,"C"代表copy,"N"代表"nonatomic","V_name"代表成員變數_name.
再如屬性age”Ti,N,V_age**“,"Ti"整體代表int型別,"N"代表"nonatomic","V_age"代表成員變數_age.

屬性型別編碼說明官網地址:Declared property type encodings

image.png image.png

2.獲取例項方法和類方法
(1)例項方法 通過runtime中的class_copyMethodList函式拿到方法列表methods,遍歷即可獲取例項物件的所有方法。具體實現如下圖:

image.png

列印屬性型別解析:
顯示如方法name型別“@[email protected]:8”.其中"@"代表返回值為物件型別,"16"代表方法引數的長度,第2個"@"代表方法的接收者,8個長度,從0-7;":"代表方法名,8個長度,從8-15,所以總共16個長度.
再如屬性setHeight“[email protected]:8q16”,"v"代表返回值為void型別,"24"代表方法引數的長度,第2個"@"代表方法的接收者,8個長度,從0-7;":"代表方法名,8個長度,從8-15;還有個long型別的引數height,8個長度,從16-23,所以總共24個長度.

image.png

(2)類方法
通過runtime中的class_getInstanceMethod函式傳入元類也能得到類方法的記憶體地址。為什麼?根據我們瞭解,獲取類方法可以使用class_getClassMethod函式,所以objc底層並沒有例項方法和類方法之分

image.png 檢視原始碼發現:class_getClassMethod實際上呼叫class_getInstance Method函式。 由此進一步說明,蘋果設計元類的目的並不是存放類方法而是為了複用訊息機制!!!

image.png

3.獲取方法的實現imp
通過runtime中的class_getMethodImplementation函式獲取方法的實現。觀察發現,此函式既能通過類找到例項方法的實現,也能通過元類找到。為什麼呢?

image.png 檢視class_getMethodImplementation函式的原始碼發現,如果找不到方法的實現,會返回_objc_msgForward來進行訊息轉發

image.png

本文總結

1.類的ro中儲存成員變數,例項方法、屬性、協議等也儲存在類物件中;
2.類方法儲存在元類中;
3.元類的設計是為了複用訊息機制,並非為了存放類方法;
4.objc底層並沒有例項方法和類方法之分。

有任何問題,歡迎👏各位評論指出!覺得博主寫的還不錯的麻煩點個贊嘍👍