isa 概念
isa是相當于是OC對象的一個標識指針,只要是OC對象就一定會有isa指針,arm64之前isa就是一個指向對象或者類的指針而已,在arm64之后發生了一些改進,isa在arm64之后變成了一個共用體(union)結構,同時使用位域的思想來實現,達到節省內存的作用;從源碼中,我們可以看到一個OC對象的isa指針并不是直接指向類對象或者元類對象的,而是要通過一個&ISA_MASK才能獲取到真正的類對象或者元類對象,這個是為什么呢?在分析之前,我們有必要先弄懂一下共用體的相關概念和用法;
共用體概念
在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內存的結構,在C語言中,被稱作“共用體”類型結構,簡稱共用體。
尋找真理
現在假設一個Person對象有3個布爾類型變量tall,rich,handsome,那么如果我們平時定義的時候,肯定都是通過屬性變量來定義,例如下圖:
這個時候你通過終端打印,可以看到結果輸出是16而不是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13,這個為什么是16,前面應該也講過了,是內存對齊原則的原因,分配的內存小于16的都會直接返回16;這個時候共同體就發揮作用了,因為共用體中變量可以相互覆蓋,可以使幾個不同的變量存放到同一段內存單元中,這樣可以很大程度上節省內存空間,眾所周知BOOL值只有兩種情況0或者1,但是卻占據了一個字節的內存空間,而一個內存空間有8個二進制位(0,1組成的),于是可以嘗試用一個二進制方式來代替bool變量,我們可以定義占用一個字節的char類型來存儲3個bool值:
我們可以在Person對象中直接初始化這個_tallRichHandsome 的值
我們可以_tallRichHandsome的后三位分別為其賦值0或者1來代表tall、rich、handsome
這里只需要搞懂是如何賦值和取值的就可以了,這里我們先看看賦值操作先
二進制的賦值操作
前面說過BOOL變量只有0和1,那么賦值操作就是只要將對應的位置設置為0或者1就可以了,賦值為1呢,我們使用 | (按位或)操作,因為按位或的做法是只要有一個為1,結果為1,所以這里我們如果想將某一位賦值為1的話,就將原來的值和相應的掩碼進行按位或的操作就可以了,例如現在我想將tall賦值為1
根據前面的tall,rich,handsome的圖,我們這里需要將第三位賦值為1,如下圖所示
那如果想將某一位賦值為0的話,需要將對應的掩碼按位取反(~:按位取反符號),之后再與原本的_tallRichHandsome進行按位與操作,例如下面所示:
那么Person類中的set方法就可以按照下面這樣設置:
這個時候我們再來看看二進制的取值;
二進制的取值操作
取值操作我們使用按位& 操作來實現,相同為1不同為0
從圖中的展示結果可以看到這樣的取值操作是沒有問題的,所以我們的get方法實現應該如下:
#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的整數,所以這里我們采取來兩個!!符號來實現,如果&的結果之后是0,那!0 代表1,!1代表0,正確,如果&結果返回1,!1是0,!0是1,返回結果也爭取,所以上面的get方法更改成如下:
雖然測試結果是正確的,但是代碼具有一定的局限性,就是可讀性差,如果需要多添加幾個元素,就得重復上述的操作,這個時候我們可以考慮用位域來操作
位域
位域聲明:位域:位域長度
位域形式大概如下:
set get方法實現如下:
測試代碼和結果如下:
發現結果居然驚現-1,但是log打斷點顯示賦值已經是正確的了
上面計算機那里的07 可以看到后面3為都是111,說明已經是賦值成功的了,為什么直接拿07,不拿多其他多個字節呢,是因為_tallRichHandsome只占據一個內存空間,也就是一個字節,可能會有人疑問為什么是1個字節而不是三個字節,這個應該就是位域的性質了,看下圖吧:
上面的結果都能顯示我們的賦值操作是正確的,那么出錯的可能就應該是取值的時候操作有無導致的,將get方法稍微修改一下先:
可以發現的確是get方法錯來,到那時錯在哪里呢?因為BOOL變量占據一個內存空間,也就是8為,這里我們是將一個只占一個內存空間的一位0b1賦值到8位,前面的7個都是直接用0b前面的值補充,也就是1,這個時候就相當于符號為用1來填充,就全部變成來11111111,為什么明明顯示是255,答案是-1呢,這里牽涉到有符號位和無符號位的關系,如果有符號位呢,這里11111111 就是-1 因為這里11111111是補碼,需要返回原碼,返回原碼的操作就是減1取反,符號位置不變(原碼--》補碼 取反 再加1),無符號位就是255;
出現這種問題有兩種解決方法,第一種就是將位域的長度擴大到2,也就是占用兩個二進制樹,那么就會變成ob01,這個時候賦值到8位的BOOL值時候就會用0取填充,最終就會變成來00000001;第二種方法就是和之前的一樣,使用!!雙非符號來解決問題,最終實現效果如下:
相對來說,結構體的位域則不需要使用掩碼,代碼可讀性增強,但是效力相比直接使用位運算的方式差,因為最終都是要轉化成位運算的操作(匯編環節),所以我們可以采取用共用體,學習共用體之后,我們就可以看源碼里面的一些設計思路來
共用體
為了提高高效率的同時又能有較強的可讀性,可以使用共用體來增強代碼可讀性,同時使用位運算來提高效率
共用體設計如下:
get 方法 set方法如下圖:
上面的共用體中_tallRichHandsome1 占用1個字節,tall1,rich1,handsome1,thin1 只占一位二進制空間,所以結構體占用一個字節,前面打印結果有說到,而char類型的bits也只占一個字節,所以可以共用一個字節的內存;從上面的方法中可以看到,get,set方法并沒有使用到結構體,而結構體的目的是為了增強代碼的可讀性,指明共用體中存儲來哪些值,以及這些值各占多少位空間,同時存儲取值還使用位運算來增加效率;好啦,這個時候可以進入查看isa_t 的源碼了。
runtime-isa_t 源碼
有了之前的鋪墊,現在再來看這份源代碼是否有一些似曾相識的感覺呢?源碼中通過共用體的形式存儲來64位的值,這些值在結構體中被展示出來,通過對bits進行位運算而取出相應位置的值;
還記得我們之前在OC對象本質中提到過一件事,就是對象的isa指針需要同ISA_MASK經過一次& 運算才能得出真正的Class對象地址
從源碼中如果是arm64位的話,可以知道這個ISA_MASK的值是define ISA_MASK? 0x0000000ffffffff8ULL,轉為二進制表現形式是:0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000,可以看到有33位為1:
所以共用體里面的shifcls中存儲的Class,Meta_Class 對象的內存地址信息,其他的對應字段相應對應源碼字段,這里有一個特殊點就是,因為這個ISA_MASK最后三位的值都為0,所以可以得出一個結論就是任何類對象或者猿類對象的內存地址最后三位必定為0,那也就是說明轉為16進制之后,類對象或者元類對象的內存地址最后一位要不就是8要不就是0;
實例證明
運行環境:__arm64__位架構
實例代碼圖和調試圖如下:
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 位數據,可以發現對象isa的33 為和isa&ISA_MASK的加粗33位相同的,代表這個shiftcls存儲的就是類對象地址或者元類對象地址? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
相信上面講了這么多,大家對isa指針應該有了全新的認識,在__arm64__架構之后,isa指針不單單只存儲了Class或者Meta-Class的地址,而是使用了共用體的方式存儲了更多的信息,
其中shiftcls存儲了Class或Meta-Class的地址,需要和ISA_MASK進行按位&操作才可以取出其內存地址值,通過上面的演示結果可以發現shiftcls和類對象地址存儲的33位二進制完全相同,
extra_rc中的19位存儲引用計數減1,實例當中person的引用計數位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已經初始化了,所以magic存儲的就是里面的6位011010
nonpointer :0 代表普通指針,存儲這Class,Meta-Class對象的內存地址,1代表表示優化后使用位域存儲這更多的信息,這里肯定是使用優化后的isa,因此nonpointer的值肯定是1
這個時候可以看到另外一些字段has_assoc 和 weakly_referenced 值都為0,接著我們繼續測試這兩個字段,添加弱引用和關聯對象,來觀察一下has_assoc 和 weakly_referenced 變化
二進制:0000 0000 0000 0000
? ? ? ? ? ? ? 0000 0101 1010 0001
? ? ? ? ? ? ? 0000 0000 0000 0000?
? ? ? ? ? ? ? 1000 1110 0110? 1111
可以看到后面3個都是1,說明測試結果正確
注意:只要設置過關聯對象或者弱引用引用過的對象has_assoc和weakly_referenced 的值就會變成1,不論之后是否將關聯對象置為nil或者斷開弱引用
如果沒有設置過關聯對象,對象釋放時候會更快,這是因為對象在銷毀時會判斷是否有關聯對象進而對對象釋放,上對象釋放的源碼圖:
可以添加微信一起交流學習:fslskz