iOS底層原理之類的底層探索(下)
本文主要內容
1.探索ivar的儲存位置
2.ro、rw、rwe解析
3.類方法的儲存位置
4.關於元類的解釋
5.通過runtime的API探索類的資料結構
一、探索ivar的儲存位置
上一篇研究了類的部分底層
,瞭解到isa的走點陣圖以及類物件和元類物件的繼承關係,並且知道類的本質是一個叫作objc_class的結構體
,其中包含了ISA
指標、(類物件的)父類、cache
、bits
等,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
結構體中。
我們來驗證類的成員變數是否真的儲存在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個成員變數)如下圖:
知識小亮點
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修改
,其中分類和本類必須是非懶載入類
。
分析原始碼,會判斷rw
是否存在rwe
,如果存在rwe
,就會在其中找需要修改的內容(方法、協議等),如果不存在rwe
就會去ro
中找。
三、類方法的儲存位置
2個疑問
的另一個疑問是:類方法存在在哪裡?
猜測:上一篇文章中發現,類方法並不在類的bits
資料中,那類方法是否會在類的元類的bits
中呢?我們帶著疑問進行探索。首先通過類的isa指標指向
找到元類的記憶體地址,再使用獲取bits中方法列表的方法
找到元類中的方法列表即可(詳細分析過程此處省略,如有不清楚的地方,請檢視上一篇文章),分析如下圖:
知識小亮點
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等內容。
通過runtime中的class_copyIvarList
函式拿到成員變數列表ivars
,遍歷即可獲取所有的成員變數。
注意⚠️:
class_copyIvarList
方法返回的ivar *
型別,該方法中使用malloc 開闢了記憶體空間,而ARC進行記憶體管理只會管理OC物件,所以需要用free
釋放ivars
。
2.獲取類的屬性
通過runtime中的class_copyPropertyList
函式拿到屬性列表properties
,遍歷即可獲取類的所有屬性name、age、height。具體實現如下圖:
列印屬性型別解析:
顯示如屬性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
2.獲取例項方法和類方法
(1)例項方法
通過runtime中的class_copyMethodList
函式拿到方法列表methods
,遍歷即可獲取例項物件的所有方法。具體實現如下圖:
列印屬性型別解析:
顯示如方法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個長度.
(2)類方法
通過runtime中的class_getInstanceMethod
函式傳入元類也能得到類方法的記憶體地址。為什麼?根據我們瞭解,獲取類方法可以使用class_getClassMethod
函式,所以objc底層並沒有例項方法和類方法之分
。
檢視原始碼發現:class_getClassMethod
實際上呼叫class_getInstance
Method
函式。
由此進一步說明,蘋果設計元類的目的並不是存放類方法而是為了複用訊息機制!!!
3.獲取方法的實現imp
通過runtime中的class_getMethodImplementation
函式獲取方法的實現。觀察發現,此函式既能通過類找到例項方法的實現,也能通過元類找到。為什麼呢?
檢視class_getMethodImplementation
函式的原始碼發現,如果找不到方法的實現,會返回_objc_msgForward
來進行訊息轉發
。
本文總結
1.類的ro中儲存成員變數,例項方法、屬性、協議等也儲存在類物件中;
2.類方法儲存在元類中;
3.元類的設計是為了複用訊息機制,並非為了存放類方法;
4.objc底層並沒有例項方法和類方法之分。
有任何問題,歡迎👏各位評論指出!覺得博主寫的還不錯的麻煩點個贊嘍👍