Runtime之isa詳解

isa 概念

isa是相當于是OC對象的一個標識指針,只要是OC對象就一定會有isa指針,arm64之前isa就是一個指向?qū)ο蠡蛘哳惖闹羔樁?,在arm64之后發(fā)生了一些改進,isa在arm64之后變成了一個共用體(union)結構,同時使用位域的思想來實現(xiàn),達到節(jié)省內(nèi)存的作用;從源碼中,我們可以看到一個OC對象的isa指針并不是直接指向類對象或者元類對象的,而是要通過一個&ISA_MASK才能獲取到真正的類對象或者元類對象,這個是為什么呢?在分析之前,我們有必要先弄懂一下共用體的相關概念和用法;

共用體概念

在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內(nèi)存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內(nèi)存的結構,在C語言中,被稱作“共用體”類型結構,簡稱共用體。

尋找真理

現(xiàn)在假設一個Person對象有3個布爾類型變量tall,rich,handsome,那么如果我們平時定義的時候,肯定都是通過屬性變量來定義,例如下圖:

代碼圖

這個時候你通過終端打印,可以看到結果輸出是16而不是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13,這個為什么是16,前面應該也講過了,是內(nèi)存對齊原則的原因,分配的內(nèi)存小于16的都會直接返回16;這個時候共同體就發(fā)揮作用了,因為共用體中變量可以相互覆蓋,可以使幾個不同的變量存放到同一段內(nèi)存單元中,這樣可以很大程度上節(jié)省內(nèi)存空間,眾所周知BOOL值只有兩種情況0或者1,但是卻占據(jù)了一個字節(jié)的內(nèi)存空間,而一個內(nèi)存空間有8個二進制位(0,1組成的),于是可以嘗試用一個二進制方式來代替bool變量,我們可以定義占用一個字節(jié)的char類型來存儲3個bool值:

char 聲明圖

我們可以在Person對象中直接初始化這個_tallRichHandsome 的值

初始化

我們可以_tallRichHandsome的后三位分別為其賦值0或者1來代表tall、rich、handsome

tall,rich,handsome對應圖

這里只需要搞懂是如何賦值和取值的就可以了,這里我們先看看賦值操作先

二進制的賦值操作

前面說過BOOL變量只有0和1,那么賦值操作就是只要將對應的位置設置為0或者1就可以了,賦值為1呢,我們使用 | (按位或)操作,因為按位或的做法是只要有一個為1,結果為1,所以這里我們?nèi)绻雽⒛骋晃毁x值為1的話,就將原來的值和相應的掩碼進行按位或的操作就可以了,例如現(xiàn)在我想將tall賦值為1

根據(jù)前面的tall,rich,handsome的圖,我們這里需要將第三位賦值為1,如下圖所示

tall賦值操作

那如果想將某一位賦值為0的話,需要將對應的掩碼按位取反(~:按位取反符號),之后再與原本的_tallRichHandsome進行按位與操作,例如下面所示:

handsome賦值為0

那么Person類中的set方法就可以按照下面這樣設置:

set方法

這個時候我們再來看看二進制的取值;

二進制的取值操作

取值操作我們使用按位& 操作來實現(xiàn),相同為1不同為0

取值操作

從圖中的展示結果可以看到這樣的取值操作是沒有問題的,所以我們的get方法實現(xiàn)應該如下:

get方法實現(xiàn)

#define SLThinMask (1<<3)

#define SLTallMask (1<<2)

#define SLRichMask (1<<1)

#define SLHandsomeMask (1<<0)

上面中用到的源碼分別如下,<< 代表左移符號,1 << 0,代表1左移0 也就是000000001,1<<1(00000010),1<<2(00000100)

接下來我們來驗證一下測試結果是否正確

測試源碼
沒有加上!!

從結果中可以看到,這個結果不是我們想要的,我們不能這樣返回一個BOOL變量值的,因為這個值返回去有可能是一個大于1的整數(shù),所以這里我們采取來兩個!!符號來實現(xiàn),如果&的結果之后是0,那!0 代表1,!1代表0,正確,如果&結果返回1,!1是0,!0是1,返回結果也爭取,所以上面的get方法更改成如下:

get方法糾正

雖然測試結果是正確的,但是代碼具有一定的局限性,就是可讀性差,如果需要多添加幾個元素,就得重復上述的操作,這個時候我們可以考慮用位域來操作

位域

位域聲明:位域:位域長度

位域形式大概如下:

位域

set get方法實現(xiàn)如下:

set get方法

測試代碼和結果如下:

測試結果圖

發(fā)現(xiàn)結果居然驚現(xiàn)-1,但是log打斷點顯示賦值已經(jīng)是正確的了

終端調(diào)試圖

上面計算機那里的07 可以看到后面3為都是111,說明已經(jīng)是賦值成功的了,為什么直接拿07,不拿多其他多個字節(jié)呢,是因為_tallRichHandsome只占據(jù)一個內(nèi)存空間,也就是一個字節(jié),可能會有人疑問為什么是1個字節(jié)而不是三個字節(jié),這個應該就是位域的性質(zhì)了,看下圖吧:

結構體
位域

上面的結果都能顯示我們的賦值操作是正確的,那么出錯的可能就應該是取值的時候操作有無導致的,將get方法稍微修改一下先:

修改測試圖

可以發(fā)現(xiàn)的確是get方法錯來,到那時錯在哪里呢?因為BOOL變量占據(jù)一個內(nèi)存空間,也就是8為,這里我們是將一個只占一個內(nèi)存空間的一位0b1賦值到8位,前面的7個都是直接用0b前面的值補充,也就是1,這個時候就相當于符號為用1來填充,就全部變成來11111111,為什么明明顯示是255,答案是-1呢,這里牽涉到有符號位和無符號位的關系,如果有符號位呢,這里11111111 就是-1 因為這里11111111是補碼,需要返回原碼,返回原碼的操作就是減1取反,符號位置不變(原碼--》補碼 取反 再加1),無符號位就是255;

出現(xiàn)這種問題有兩種解決方法,第一種就是將位域的長度擴大到2,也就是占用兩個二進制樹,那么就會變成ob01,這個時候賦值到8位的BOOL值時候就會用0取填充,最終就會變成來00000001;第二種方法就是和之前的一樣,使用!!雙非符號來解決問題,最終實現(xiàn)效果如下:

位域!!

相對來說,結構體的位域則不需要使用掩碼,代碼可讀性增強,但是效力相比直接使用位運算的方式差,因為最終都是要轉(zhuǎn)化成位運算的操作(匯編環(huán)節(jié)),所以我們可以采取用共用體,學習共用體之后,我們就可以看源碼里面的一些設計思路來

共用體

為了提高高效率的同時又能有較強的可讀性,可以使用共用體來增強代碼可讀性,同時使用位運算來提高效率

共用體設計如下:

共用體

get 方法 set方法如下圖:

get set

上面的共用體中_tallRichHandsome1 占用1個字節(jié),tall1,rich1,handsome1,thin1 只占一位二進制空間,所以結構體占用一個字節(jié),前面打印結果有說到,而char類型的bits也只占一個字節(jié),所以可以共用一個字節(jié)的內(nèi)存;從上面的方法中可以看到,get,set方法并沒有使用到結構體,而結構體的目的是為了增強代碼的可讀性,指明共用體中存儲來哪些值,以及這些值各占多少位空間,同時存儲取值還使用位運算來增加效率;好啦,這個時候可以進入查看isa_t 的源碼了。

runtime-isa_t 源碼

isa_t 共用體源碼

有了之前的鋪墊,現(xiàn)在再來看這份源代碼是否有一些似曾相識的感覺呢?源碼中通過共用體的形式存儲來64位的值,這些值在結構體中被展示出來,通過對bits進行位運算而取出相應位置的值;

還記得我們之前在OC對象本質(zhì)中提到過一件事,就是對象的isa指針需要同ISA_MASK經(jīng)過一次& 運算才能得出真正的Class對象地址

isa&ISA_MASK

從源碼中如果是arm64位的話,可以知道這個ISA_MASK的值是define ISA_MASK? 0x0000000ffffffff8ULL,轉(zhuǎn)為二進制表現(xiàn)形式是:0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000,可以看到有33位為1:

0x0000000ffffffff8ULL 二進制

所以共用體里面的shifcls中存儲的Class,Meta_Class 對象的內(nèi)存地址信息,其他的對應字段相應對應源碼字段,這里有一個特殊點就是,因為這個ISA_MASK最后三位的值都為0,所以可以得出一個結論就是任何類對象或者猿類對象的內(nèi)存地址最后三位必定為0,那也就是說明轉(zhuǎn)為16進制之后,類對象或者元類對象的內(nèi)存地址最后一位要不就是8要不就是0;

實例證明

運行環(huán)境:__arm64__位架構

實例代碼圖和調(diào)試圖如下:

代碼原圖
終端輸出圖
二進制


isa&ISA_MASK

isa 二進制:? ??????????????0000 0000 0000 0000?

? ??????????????????????????????????0000 0001 1010??0001?

? ??????????????????????????????????0000 0000 0000 1010?

? ??????????????????????????????????1000 1110 0010 0001

isa&ISA_MASK二進制:0000 0000 0000 0000

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?0000 0000 0000 0001

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?0000 0000 0000 1010?

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?1000 1110 0010 0000

上述二進制的加粗部分說明的是取出shiftcls33 位數(shù)據(jù),可以發(fā)現(xiàn)對象isa的33 為和isa&ISA_MASK的加粗33位相同的,代表這個shiftcls存儲的就是類對象地址或者元類對象地址? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

相信上面講了這么多,大家對isa指針應該有了全新的認識,在__arm64__架構之后,isa指針不單單只存儲了Class或者Meta-Class的地址,而是使用了共用體的方式存儲了更多的信息,

其中shiftcls存儲了Class或Meta-Class的地址,需要和ISA_MASK進行按位&操作才可以取出其內(nèi)存地址值,通過上面的演示結果可以發(fā)現(xiàn)shiftcls和類對象地址存儲的33位二進制完全相同,

extra_rc中的19位存儲引用計數(shù)減1,實例當中person的引用計數(shù)位1,因此此時extra_rc的19位二進制存儲的是0

magic中的6位在于判斷對象是否未完成初始化,因為magic是(isa.magic is part of ISA_MAGIC_VALUE)的一部分? ?#define ISA_MAGIC_VALUE 0x000001a000000001ULL(二進制:0000 0000 0000 0000 0000 00001 1010 0000 0000 0000 0000 0000 0000 0000 0000 0001)粗色標注的6位就是magic的值:而例子中的person已經(jīng)初始化了,所以magic存儲的就是里面的6位011010

isa 初始化

nonpointer :0 代表普通指針,存儲這Class,Meta-Class對象的內(nèi)存地址,1代表表示優(yōu)化后使用位域存儲這更多的信息,這里肯定是使用優(yōu)化后的isa,因此nonpointer的值肯定是1

這個時候可以看到另外一些字段has_assoc 和 weakly_referenced 值都為0,接著我們繼續(xù)測試這兩個字段,添加弱引用和關聯(lián)對象,來觀察一下has_assoc 和 weakly_referenced 變化

代碼圖
二進制圖

二進制:0000 0000 0000 0000

? ? ? ? ? ? ? 0000 0101 1010 0001

? ? ? ? ? ? ? 0000 0000 0000 0000?

? ? ? ? ? ? ? 1000 1110 0110? 1111

可以看到后面3個都是1,說明測試結果正確

注意:只要設置過關聯(lián)對象或者弱引用引用過的對象has_assoc和weakly_referenced 的值就會變成1,不論之后是否將關聯(lián)對象置為nil或者斷開弱引用

如果沒有設置過關聯(lián)對象,對象釋放時候會更快,這是因為對象在銷毀時會判斷是否有關聯(lián)對象進而對對象釋放,上對象釋放的源碼圖:

對象銷毀

可以添加微信一起交流學習:fslskz

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容