isa 概念
isa是相當(dāng)于是OC對象的一個標(biāo)識指針螟够,只要是OC對象就一定會有isa指針,arm64之前isa就是一個指向?qū)ο蠡蛘哳惖闹羔樁严康觯赼rm64之后發(fā)生了一些改進妓笙,isa在arm64之后變成了一個共用體(union)結(jié)構(gòu),同時使用位域的思想來實現(xiàn)能岩,達到節(jié)省內(nèi)存的作用寞宫;從源碼中,我們可以看到一個OC對象的isa指針并不是直接指向類對象或者元類對象的拉鹃,而是要通過一個&ISA_MASK才能獲取到真正的類對象或者元類對象辈赋,這個是為什么呢?在分析之前膏燕,我們有必要先弄懂一下共用體的相關(guān)概念和用法钥屈;
共用體概念
在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內(nèi)存單元中坝辫。也就是使用覆蓋技術(shù)篷就,幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內(nèi)存的結(jié)構(gòu)近忙,在C語言中竭业,被稱作“共用體”類型結(jié)構(gòu),簡稱共用體及舍。
尋找真理
現(xiàn)在假設(shè)一個Person對象有3個布爾類型變量tall未辆,rich,handsome锯玛,那么如果我們平時定義的時候咐柜,肯定都是通過屬性變量來定義,例如下圖:
這個時候你通過終端打印更振,可以看到結(jié)果輸出是16而不是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13炕桨,這個為什么是16,前面應(yīng)該也講過了肯腕,是內(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值:
我們可以在Person對象中直接初始化這個_tallRichHandsome 的值
我們可以_tallRichHandsome的后三位分別為其賦值0或者1來代表tall、rich顶考、handsome
這里只需要搞懂是如何賦值和取值的就可以了驹沿,這里我們先看看賦值操作先
二進制的賦值操作
前面說過BOOL變量只有0和1艘策,那么賦值操作就是只要將對應(yīng)的位置設(shè)置為0或者1就可以了,賦值為1呢渊季,我們使用 | (按位或)操作朋蔫,因為按位或的做法是只要有一個為1,結(jié)果為1却汉,所以這里我們?nèi)绻雽⒛骋晃毁x值為1的話驯妄,就將原來的值和相應(yīng)的掩碼進行按位或的操作就可以了,例如現(xiàn)在我想將tall賦值為1
根據(jù)前面的tall病涨,rich富玷,handsome的圖,我們這里需要將第三位賦值為1既穆,如下圖所示
那如果想將某一位賦值為0的話赎懦,需要將對應(yīng)的掩碼按位取反(~:按位取反符號),之后再與原本的_tallRichHandsome進行按位與操作幻工,例如下面所示:
那么Person類中的set方法就可以按照下面這樣設(shè)置:
這個時候我們再來看看二進制的取值励两;
二進制的取值操作
取值操作我們使用按位& 操作來實現(xiàn),相同為1不同為0
從圖中的展示結(jié)果可以看到這樣的取值操作是沒有問題的囊颅,所以我們的get方法實現(xiàn)應(yīng)該如下:
#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)
接下來我們來驗證一下測試結(jié)果是否正確
從結(jié)果中可以看到盲憎,這個結(jié)果不是我們想要的,我們不能這樣返回一個BOOL變量值的胳挎,因為這個值返回去有可能是一個大于1的整數(shù)饼疙,所以這里我們采取來兩個!!符號來實現(xiàn),如果&的結(jié)果之后是0慕爬,那窑眯!0 代表1屏积,!1代表0磅甩,正確炊林,如果&結(jié)果返回1,卷要!1是0渣聚,!0是1却妨,返回結(jié)果也爭取饵逐,所以上面的get方法更改成如下:
雖然測試結(jié)果是正確的,但是代碼具有一定的局限性彪标,就是可讀性差,如果需要多添加幾個元素掷豺,就得重復(fù)上述的操作捞烟,這個時候我們可以考慮用位域來操作
位域
位域聲明:位域:位域長度
位域形式大概如下:
set get方法實現(xiàn)如下:
測試代碼和結(jié)果如下:
發(fā)現(xiàn)結(jié)果居然驚現(xiàn)-1,但是log打斷點顯示賦值已經(jīng)是正確的了
上面計算機那里的07 可以看到后面3為都是111当船,說明已經(jīng)是賦值成功的了题画,為什么直接拿07,不拿多其他多個字節(jié)呢德频,是因為_tallRichHandsome只占據(jù)一個內(nèi)存空間苍息,也就是一個字節(jié),可能會有人疑問為什么是1個字節(jié)而不是三個字節(jié)壹置,這個應(yīng)該就是位域的性質(zhì)了竞思,看下圖吧:
上面的結(jié)果都能顯示我們的賦值操作是正確的,那么出錯的可能就應(yīng)該是取值的時候操作有無導(dǎo)致的钞护,將get方法稍微修改一下先:
可以發(fā)現(xiàn)的確是get方法錯來盖喷,到那時錯在哪里呢?因為BOOL變量占據(jù)一個內(nèi)存空間难咕,也就是8為课梳,這里我們是將一個只占一個內(nèi)存空間的一位0b1賦值到8位,前面的7個都是直接用0b前面的值補充余佃,也就是1暮刃,這個時候就相當(dāng)于符號為用1來填充,就全部變成來11111111爆土,為什么明明顯示是255椭懊,答案是-1呢,這里牽涉到有符號位和無符號位的關(guān)系雾消,如果有符號位呢灾搏,這里11111111 就是-1 因為這里11111111是補碼挫望,需要返回原碼,返回原碼的操作就是減1取反狂窑,符號位置不變(原碼--》補碼 取反 再加1)媳板,無符號位就是255;
出現(xiàn)這種問題有兩種解決方法泉哈,第一種就是將位域的長度擴大到2蛉幸,也就是占用兩個二進制樹,那么就會變成ob01丛晦,這個時候賦值到8位的BOOL值時候就會用0取填充奕纫,最終就會變成來00000001;第二種方法就是和之前的一樣烫沙,使用Fゲ恪!雙非符號來解決問題锌蓄,最終實現(xiàn)效果如下:
相對來說瘸爽,結(jié)構(gòu)體的位域則不需要使用掩碼您访,代碼可讀性增強,但是效力相比直接使用位運算的方式差剪决,因為最終都是要轉(zhuǎn)化成位運算的操作(匯編環(huán)節(jié))灵汪,所以我們可以采取用共用體,學(xué)習(xí)共用體之后柑潦,我們就可以看源碼里面的一些設(shè)計思路來
共用體
為了提高高效率的同時又能有較強的可讀性享言,可以使用共用體來增強代碼可讀性,同時使用位運算來提高效率
共用體設(shè)計如下:
get 方法 set方法如下圖:
上面的共用體中_tallRichHandsome1 占用1個字節(jié)妒茬,tall1,rich1,handsome1,thin1 只占一位二進制空間担锤,所以結(jié)構(gòu)體占用一個字節(jié),前面打印結(jié)果有說到乍钻,而char類型的bits也只占一個字節(jié)肛循,所以可以共用一個字節(jié)的內(nèi)存;從上面的方法中可以看到银择,get多糠,set方法并沒有使用到結(jié)構(gòu)體,而結(jié)構(gòu)體的目的是為了增強代碼的可讀性浩考,指明共用體中存儲來哪些值夹孔,以及這些值各占多少位空間,同時存儲取值還使用位運算來增加效率;好啦搭伤,這個時候可以進入查看isa_t 的源碼了只怎。
runtime-isa_t 源碼
有了之前的鋪墊,現(xiàn)在再來看這份源代碼是否有一些似曾相識的感覺呢怜俐?源碼中通過共用體的形式存儲來64位的值身堡,這些值在結(jié)構(gòu)體中被展示出來,通過對bits進行位運算而取出相應(yīng)位置的值拍鲤;
還記得我們之前在OC對象本質(zhì)中提到過一件事贴谎,就是對象的isa指針需要同ISA_MASK經(jīng)過一次& 運算才能得出真正的Class對象地址
從源碼中如果是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:
所以共用體里面的shifcls中存儲的Class,Meta_Class 對象的內(nèi)存地址信息景鼠,其他的對應(yīng)字段相應(yīng)對應(yīng)源碼字段仲翎,這里有一個特殊點就是,因為這個ISA_MASK最后三位的值都為0铛漓,所以可以得出一個結(jié)論就是任何類對象或者猿類對象的內(nèi)存地址最后三位必定為0谭确,那也就是說明轉(zhuǎn)為16進制之后,類對象或者元類對象的內(nèi)存地址最后一位要不就是8要不就是0票渠;
實例證明
運行環(huán)境:__arm64__位架構(gòu)
實例代碼圖和調(diào)試圖如下:
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指針應(yīng)該有了全新的認(rèn)識,在__arm64__架構(gòu)之后禀梳,isa指針不單單只存儲了Class或者Meta-Class的地址杜窄,而是使用了共用體的方式存儲了更多的信息,
其中shiftcls存儲了Class或Meta-Class的地址算途,需要和ISA_MASK進行按位&操作才可以取出其內(nèi)存地址值塞耕,通過上面的演示結(jié)果可以發(fā)現(xiàn)shiftcls和類對象地址存儲的33位二進制完全相同,
extra_rc中的19位存儲引用計數(shù)減1嘴瓤,實例當(dāng)中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)粗色標(biāo)注的6位就是magic的值:而例子中的person已經(jīng)初始化了,所以magic存儲的就是里面的6位011010
nonpointer :0 代表普通指針廓脆,存儲這Class筛谚,Meta-Class對象的內(nèi)存地址,1代表表示優(yōu)化后使用位域存儲這更多的信息停忿,這里肯定是使用優(yōu)化后的isa驾讲,因此nonpointer的值肯定是1
這個時候可以看到另外一些字段has_assoc 和 weakly_referenced 值都為0,接著我們繼續(xù)測試這兩個字段,添加弱引用和關(guān)聯(lián)對象吮铭,來觀察一下has_assoc 和 weakly_referenced 變化
二進制:0000 0000 0000 0000
? ? ? ? ? ? ? 0000 0101 1010 0001
? ? ? ? ? ? ? 0000 0000 0000 0000?
? ? ? ? ? ? ? 1000 1110 0110? 1111
可以看到后面3個都是1时迫,說明測試結(jié)果正確
注意:只要設(shè)置過關(guān)聯(lián)對象或者弱引用引用過的對象has_assoc和weakly_referenced 的值就會變成1,不論之后是否將關(guān)聯(lián)對象置為nil或者斷開弱引用
如果沒有設(shè)置過關(guān)聯(lián)對象谓晌,對象釋放時候會更快掠拳,這是因為對象在銷毀時會判斷是否有關(guān)聯(lián)對象進而對對象釋放,上對象釋放的源碼圖:
可以添加微信一起交流學(xué)習(xí):fslskz