? ? ? ? 在上個章節,我們學習了對象的本質,對isa有了一個籠統的概念,了解到對象的本質其實就是一個包含了變量和isa指針的結構體。并且可以通過實例對象的isa獲取到類對象,然后通過類對象的isa獲取到元類的對象。但是我們并不清楚類對象中的具體結構,我們定義的成員變量,屬性,協議,實例方法、類方法都保存在哪?在這個章節中,我們會對這些進行詳細的分析。
? ? ? ? 在分析對象本質的時候,我們知道OC底層其實是用C++編寫的,所以用下面命令將main.m轉換成了c++代碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main__.cpp
我們打開這個文件看看能不能找到一些有用的信息。
查找信息肯定要有針對性才有結果,我們要分析類的本質,應該從Class這個類型出發,我們全局搜索一下看看有沒有針對Class類型的定義。
如果有讀者在Xcode中通過runtime.h進入到頭文件中應該也會發現下面的這個定義
那么這個objc_class結構體內部包含了哪些成員呢?全局搜索一下,沒有發現定義,不過已經有眉目就好辦了,打開objc源碼,全局搜索objc_class。
不過找到的信息似乎已經過期了,OBJC2_UNAVAILABLE表示在OBJC2下已經失效了,并且最后一行的注釋也說明這個結構體被Class替代了,但是Class不是本來就是struct objc_class的重定義嗎?既然是過期,就應該會有新的定義,我們修改一下搜索關鍵詞,將關鍵詞objc_class換成struct objc_class,得到如下結果
可以看到 objc-runtime 分為 new 和 old 2個頭文件,我們要找的肯定是最新的,所以在new 頭文件中找到了上圖的objc_class定義,objc_class繼承自objc_object(這是C++的寫法,C++中類和結構體都是可以繼承的),那么objc_object又是個什么結構呢?
objc_object 內部其實只有一個成員,isa。剩余的都是結構體內部的函數。
從宏觀來看,我們可以知道類的本質是objc_class結構體,里面包含了以下4個成員以及大量的函數:
1、isa_t isa;
2、Class????superclass;
3、cache_t????cache;? ?
4、class_data_bits_t????bits;? ?
。。。。。。。。其它函數
isa在上一節已經分析過了,不再重復分析。
Class其實就是objc_class *,所以第二個成員又可以寫成objc_class * superclass;
那么我們需要分析的就剩下2個了
cache_t????cache
class_data_bits_t????bits
cache_t????cache結構體比較復雜,我們先挑簡單的分析,先分析class_data_bits_t????bits
class_data_bits_t
首先定義了一個friend objc_class,然后是一個bits 指針
friend代表什么意思呢?
friend關鍵字用于修飾C++中的有元類,被修飾的的類可以訪問當前類的私有成員或者調用私有函數。
大家注意看上圖紅線的私有函數,getBits、setBits 等在objc_class中是有調用的
上圖可以看到objc_calss直接用bits.getBit訪問了私有函數,這就是friend修飾后的效果。
說白了,class_data_bits_t其實就是和isa一樣的指針,通過bits將各種信息存儲到8個字節64位中。可以看到上圖 bits.getBit(XX)傳入的是一個宏,該宏的實際值是:
#define FAST_HAS_DEFAULT_RR? ? (1UL<<2)?? ? 1左移2位
也就是說hasCustomRR() 其實是判斷bits 64位中第3個bit位中的值是0還是1。
和isa取值的原理是完全一樣的,通過 & 上不同的宏,來獲取不同bit位上的值,從而得到相關數據。
那么我們再往下翻,看看bits具體能獲取到什么東西
class_rw_t
通過點語法調用了bits的data()函數,來獲取一個類型為class_rw_t的結構體。其實這個結構體在之前的文章已經分析過了。
用一幅圖來表示class_rw_t、ro_or_rw_ext_t、class_ro_t 這3個結構體的關系更好理解
重點解釋下:ro_or_rw_ext_t = objc::PointerUnion<const?class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
這是個什么呢?這是一個Union聯合體(不懂的讀者自行查找資料)。聯合的是傳入的參數 class_ro_t 或 class_rw_ext_t ,也就是說 rw 結構體中有且僅有一個ro或者一個rwe。如果get_ro_or_rwe()返回的是rwe,則取ro是這樣的 ro = rwe -> ro.
總結下:
ro 是體量最小的,而且不可變的只讀的,里面存儲著類的成員變量,基本的方法、屬性、協議等。
rwe 是可變的,可讀寫的,其中包含了ro,而且還有3個數組用于存儲方法、屬性、協議。
rw 中包含了rwe或者ro,如果該類有category則rw中存儲的則是rwe,因為存儲category需要對內存進行寫入,不管是取ro還是取rwe都是調用同一個函數get_ro_or_rwe()。如果是取ro,rw中還有一個單獨的ro()函數,里面的實現也可以說明rw中存儲的要么是rwe要么是ro。
那么什么情況會返回ro,什么情況下會返回rwe呢?
如果有認真閱讀過重學iOS系列之APP啟動(三)objc(runtime)肯定能回答。
如果類存在category,則需要將category中的所有信息附加到類中,需要對類動態的修改內存,所以這種情況下返回的是rwe;
如果只存在單獨的類,則不需要對內存進行修改,則返回的是ro。
Talk is cheap, show me your code.
我們來寫代碼驗證一下,class_rw_t 的結構是否真的如 圖中一樣。
先在objc源碼的main.mm文件中添加一個Student類、category,然后打好斷點運行起來。
然后通過lldb打印出數據如下
$0 是Student 類對象的地址,我們再看看類的結構體
想通過 $0->data() 拿到?class_rw_t 結構體,發現行不通。那么我們只能通過內存偏移來獲取 bits 的地址,再通過 bits->data() 來拿到class_rw_t數據了。
大家都知道結構體成員在內存中都是緊挨著的,如上圖所示:
Student 內存 前8個字節是 isa 指針,后8個字節是 superclass 指針, 然后是cache_t 結構體,只要算出cache_t占用多少字節就可以算出 bits 在內存中的偏移量了。先通過源碼看看?cache_t 結構體包含什么成員。
cache_t結構體成員? ? ? ? ? ? ? ? ? ? ? ? ? ?類型 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?大小(字節)
_bucketsAndMaybeMask?????????????uintptr_t? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?8字節
_maybeMask??????????????????????????????????mask_t? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 32bits(4字節)
_flags? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?uint16_t ? ? ? ? ? ? ? ? ? ? ? ? ? ?????16bits(2字節)
_occupied ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??uint16_t????????????????????????????????16bits(2字節)???
由上述計算可以得?cache_t 占用了 16個字節
那么 bits 的偏移量 = 8 + 8 + 16 = 32 字節 = 16進制 0x20
那么我們就可以這樣來獲取 bits?
?????0x00000001000086d0 是Person class的地址 + 0x20 就是偏移32個字節,這樣就拿到了 bits?
然后通過 ->data() 拿到class_rw_t
在通過 p *$2 打印出class_rw_t 的內容
再看看class_rw_t 中有什么函數可以獲取到類的相關信息
上圖可以知道class_rw_t結構體中有直接獲取 方法、屬性、協議的成員函數。
3個函數的內部實現非常相似都是通過get_ro_or_rwe() 拿到?ro_or_rw_ext_t 類型的一個結構,ro_or_rw_ext_t 前面分析過是一個聯合體,內部存儲的是ro 或者 rw,我們也可以通過下面的命令來獲取
但是在我們嘗試獲取他的具體類型class_rw_ext_t 時失敗了,目前筆者還未找到方法來打印該聯合體的具體類型,有了解的讀者可以私信,不勝感激!
既然我們拿不到具體類型,就無法確定 是 ro 還是rwe ,我們再從源碼找找是否有其他的函數可以調用
找到可以直接獲取class_ro_t 的 ro() 函數,嘗試調用一下
打印數據非常完整,我們拿到了 ro
ro 里是否真的保存了之前列舉的那些數據呢?我們打印一下看看
那么怎么取ivars中的具體值呢?我們先看看 ivar_list_t 類型定義
發現 property_list_t 和?ivar_list_t 都是繼承自同一個類型entsize_list_tt
entsize_list_tt 內部定義了一個成員函數 get(下標)可以獲取到 list 中的 Element ,那么我們就可以直接調用 get() 函數來打印成員變量 和 屬性
實際上我們只定義了3個成員變量,由于@property 定義的屬性系統會自動幫我們生成帶下劃線的成員變量,所以這里的count 為 6 。
那么我們再看看屬性
Student只定義了3個屬性,但是這里打印卻出現了7個,由此可見,llvm幫我們自動幫我們定義了4個屬性:hash、superclass、description、debugDescription。
所以這也是為什么我們可以用點語法獲取類的superclass、description等,這2個屬性應該是相對來說使用頻率高的。
獲取方法列表
咦,怎么返回值是 void * ,我們再找找是否有函數可以調用來獲取MethodList
找到了,執行下面的命令調用get()函數來獲取具體的方法元素
注意 上面?p $6.baseMethodList ?和?p $6.baseMethods() 返回的地址是一樣的,只有類型是不同的,后面會分析為什么
但是為什么get函數返回的值會是空的,我們看看?method_t 結構體是怎么定義的
從注釋可以得知具體的數據被封裝到了 struct big {} 中,神仙操作啊
修改命令,用點語法調用big再重新獲取
方法列表更離譜,竟然有10個
我們明明才定義了5個方法,2個對象方法,2個類方法,1個協議方法 (包括category)
從上圖可以發現,2個類方法不在里面,多出來的方法是?llvm 自動生成的?property 屬性?get、set 方法。
并且,注意看p $30->get(3).big()的結果
(method_t::big) $38 = {
? name = "categoryFunc"
? types = 0x0000000100003d4f "v16@0:8"
? imp = 0x0000000100003b30 (KCObjcBuild`-[Student(TestCategory) categoryFunc] at main.m:55)
}
category的對象方法竟然也在 ro 的baseMethod里面,結合$6.baseMethodList ?和?p $6.baseMethods() 返回的地址一樣,可以分析出類的方法列表有且只有一份存檔,?ro ?和 rw 的方法列表指針都是指向這一份內存地址的。這點和之前版本的 ro 、 rw 的結構是不一樣的,要注意!!!
最后我們把協議也打印下看看
最終打印的數據竟然不能看,這里肯定有什么誤會,我們再從源碼著手,看看是否是類型錯誤導致的
注意看紅線位置的注釋,protocol_ref_t 其實是指向protocol_t *的指針。那么我們可以直接強制轉換類型,將$49 和 $50 強制轉換成?protocol_t * 類型打印
到此,成員變量、屬性、方法、協議 都已經驗證完畢,最后還剩余2個類方法沒有被發現,類方法是保存在類的元類中的,怎么獲取元類呢?其實在上一個章節已經教過大家怎么獲取元類了,方法和實例對象獲取類對象的地址是一樣的,通過 isa 指針 & 上一個宏常量Mask,就能得到元類的地址。
$0 = 0x00000001000086d0 是 類對象的地址
大家注意2根綠線的位置,0x00000001000086a8 是 Student 類對象的 isa 指針,與 Mask 進行 & 計算后 獲取到 元類對象的地址,0x00000001000086a8,沒錯,就是 isa的地址。這可能是個巧合?
然后繼續使用之前獲取類信息的步驟,一步一步獲取到元類的 ro
注意綠線的位置,這次筆者是直接使用強制類型轉換,將原本是 void * 強制轉換成了?method_list_t * ?, 效果是一樣的。
下面的打印和我們在Student類中定義的是一樣的,并且這次LLVM沒有幫我們生成其他的類方法。
結論 :8.18版本的objc 類的信息都可以通過?class_rw_t 的 ro() 函數來獲取到。
彩蛋:在閱讀objc_class 結構體大量的成員函數中發現了3個有意思的函數實現,相信大家看到了也會很吃驚!
getMeta() 獲取元類, 判斷本身是否是元類,如果是,則返回自己,否則調用ISA()函數進行Mask計算。
證明了元類是類對象通過 isa 計算出來的。
isRootClass() ?內部直接判斷 當前類的superclass 是否為nil
證明了根類NSObjcet的父類 為nil。
isRootMetaclass 判斷通過 isa 計算得到的地址是否就是自己
證明 根元類的 isa 指針其實指向的就是自己
結合下圖,會對 類 、 元類、isa 、superclass 之間的關聯 有更深刻的理解
還沒結束呢,我們還有?catch_t 這個結構體沒有分析呢!!!
catch_t
下面分析 catch_t ,顧名思義,從命名來看,catch_t應該是跟緩存有關的,那么緩存了類的什么信息呢?先從?catch_t 結構體的源碼找找線索
從上圖來看(注意看綠線劃的2個函數的參數),catch_t 內部有這么一個函數
?void? ? insert(SEL? ? sel,?????IMP? ? imp,????id? ? receiver)
我們可以大膽的猜想這個函數應該是將緩存的信息插入到容器中,那么再看具體的參數
SEL ?sel ?這不就是方法的符號信息么
IMP? ? imp 方法的實現,或者說imp里存儲著方法執行的首地址
id? ? receiver ? ?接收者,其實就是調用方法的對象
大家再看另外一個函數????cache_getImp(cls(), sel) == imp,該函數的定義就在 cache_t 的上面,從注釋可以知道?cache_getImp 就是從緩存中查找方法的 imp
從cache中拿到imp,由此可以大概的確定?catch_t 應該就是緩存類的方法的,那么具體緩存的是什么方法呢?什么時候會觸發?insert 的調用呢?
實戰打印catch_t
我們用打印 bits 的方式來打印一下 catch_t ,至于為什么要用偏移量的方式打印,是因為正常打印會報錯失敗,和直接打印bits一樣。
在此之前,既然跟方法有關,那么我們就先在Student類添加幾個對象方法,如下
修改main函數內部代碼如下,打上斷點然后運行工程
注意:
1、Student 類只 alloc 申請了內存空間,沒有調用init方法。
2、student.name 是會觸發 屬性的 setName 方法調用的。
也就是說,目前 student 對象沒有調用任何一個方法。
運行結果如上圖,在沒有調用方法的情況下,_occupied 和?_maybeMask 的值都為0
過掉斷點,執行?student.name=@"ZYYC" 語句,這句代碼會調用 setName 方法
可以發現劃線的_maybeMask由之前的0變為了3,_occupied由原來的0變為了1。這中間的變化就是調用了一個方法。我們繼續過斷點?
_maybeMask 的值不變,_occupied 又加 1 了,再繼續過斷點
_maybeMask 竟然直接變成7了,?_occupied 竟然變成了1,這很不符合我們的預期,說明有問題,我們再繼續過斷點看看會發生什么其他的變化
_maybeMask 還是7 ,?_occupied 又增加了 1 。 值的變化不是很有規律,從調用一次方法變化一次的情況,_maybeMask 和?_occupied 的變化可能和 insert()函數有關,我們查看下 insert() 源碼實現邏輯,源碼比較多,挑重點截下來分析
簡述下 insert() 的 流程 :
1、拿到_occupied,并且將值加 1,驗證了調用一次方法值會加 1 的變化。
2、拿到舊的容量oldCapacity,并且賦值到新創建的capacity 中。
3、判斷緩存是否為空,如果為空,則調用reallocate進行初始化操作,申請空間,稍后分析reallocate函數的具體實現。在調用reallocate之前,會對capacity的值做判斷,如果沒有值,則賦值為默認值 4 。
4、判斷是否達到容量的 3/4 或者 7/8,沒有達到則什么都不做
5、判斷是否支持裝滿容量,如果支持,并且容量還未裝滿,則什么都不做
6、上述3、4、5 步驟都判斷失敗的話,則進行擴容操作,擴充的容量為之前的 2 倍,擴容之前判斷是否擴充的容量超過了最大值 2 的 16 次方,如果超過了,則賦值為最大值。最后調用reallocate進行擴容。
7、mask 的值 為容量?capacity - 1,因為?capacity默認為4,所以_maybeMask第一次的值為 3 。并且擴容后容量為 4*2 = 8,_maybeMask = 7。
但是上述流程并沒有解釋 為什么 ?_occupied 的值會在擴容的時候重新賦值為 1 。
我們看看?reallocate 的實現
buckets() 其實就是返回通過?_bucketsAndMaybeMask 拿到地址,然后再做一次 & 計算得到的地址指針。?_bucketsAndMaybeMask 是cache_t 的第一個成員,不知道大家還記得嗎?
沒有發現?_occupied 相關代碼,繼續進入?setBucketsAndMask 函數內部
發現了 在新建的時候以及擴容的時候?_occupied 的值會被重新初始化為 0 。
并且上面的注釋也透露了一點 objc_msgSend 的秘密。
我們都知道OC的方法調用底層其實就是?objc_msgSend 函數的調用,可見objc_msgSend 在查找方法的時候會先到cache_t 緩存中查詢,如果查詢到的話就直接調用,大大的降低了方法查詢的時間。
從上述代碼可以得出我們的方法緩存存儲的容器其實就是 buckets()
從 bucket_t 的結構體成員 也驗證了存儲的確實就是 方法符號 和 方法地址。
有創建就有回收,我們看看官方是怎么回收緩存內存的
調用?_garbage_make_room 創建了一個垃圾場,用于存放需要回收的緩存數據
garbage_refs 是垃圾場的存儲容器
真正回收內存的函數是?cache_t::collectNolock(false) 這句
先看看?_garbage_make_room 里面是怎么創建垃圾場的
如果是第一次進入就申請內存,創建容器空間。否則判斷容器是否滿,滿了就擴容為 2 倍。
然后看看?collectNolock 的內部實現
看劃綠線的位置,判斷 需要回收的垃圾大小是否達到一個閾值,如果沒有達到閾值則不會回收,直接return了
綠圈內部才是真正進行內存回收的邏輯,調用了非常熟悉的 free 函數進行內存回收。
擴容這部分代碼分析完畢,繼續分析?insert 后半部分插入的邏輯
上述代碼進行真正的插入邏輯,該邏輯會進行檢查防止重復緩存。
再回過頭來分析下?isConstantEmptyCache 內部是怎么判斷為空的
內部調用了emptyBucketsForCapacity ,注意第二個參數傳入了 false?
emptyBucketsList 是一個二維數組,所有空的Buckets都是從這個二維數組中取的。
源碼分析完畢,上述打印的結果得到驗證,又到了實戰環節了,我們這次把緩存的方法符號給打印出來。
之前斷點運行到了下圖所示位置
目前?_occupied = 2 , 說明緩存中有2個方法,以上圖斷點的位置,應該存儲的是 instenceFunc2 和?instenceFunc3 。
打印第一個緩存的方法符號如下,正是????instenceFunc3?
繼續打印
咦,怎么是nil,有點奇怪哦,繼續打印
找到了????instenceFunc2
筆者將剩余的位置打印出來了,都是nil,就不再將截圖傳上來。
結論: ? cache_t ? ?會將對象調用的方法進行緩存, 緩存的方法不是按照順序存儲的,是以hash算法計算位置查詢空插槽進行插入的,并且在每次擴容的時候會將之前存儲的方法全部清空。