我們知道,判定是不是OC對象的本質(zhì)就是看是否含有isa指針,在ARM64架構(gòu)之前,objc_object的isa指針就是一個(gè)class類型,存儲的就是一個(gè)指針,而ARM64系統(tǒng)之后,對isa進(jìn)行了優(yōu)化,變成了一個(gè)共用體(union),還是用位域來存儲更多信息.
共用體
今天我們就來研究一下共同體(union)和位域.
我們先通過一個(gè)小場景來開始今天的內(nèi)容,我們先創(chuàng)建一個(gè)Person類,類中有三個(gè)BOOL類型的屬性:tall , rich ,handsome,分別表示高,富,帥.
然后通過class_getInstanceSize查看這個(gè)類對象占用多少字節(jié),發(fā)現(xiàn)打印輸出是:16.為什么是16個(gè)字節(jié)呢?因?yàn)?個(gè)BOOL類型的屬性占用3個(gè)字節(jié),isa指針占用8個(gè)字節(jié),一共占用11個(gè)字節(jié),再內(nèi)存對齊以后,就是16個(gè)字節(jié).
這樣我們就會發(fā)現(xiàn)一個(gè)問題,三個(gè)BOOL類型的屬性占用了3個(gè)字節(jié),其實(shí)BOOL類型屬性本質(zhì)就是0 或 1,只占一位,也就是說3個(gè)BOOL屬性放在一個(gè)字節(jié)就可以搞定.比如說有一個(gè)字節(jié)的內(nèi)存0x0000 0000我們用最后3位分別存放高,富,帥,如圖所示:
?思考一下:怎樣才能做到只用一位去存放三個(gè)屬性呢?
只能通過位運(yùn)算做到了.我們先把屬性相關(guān)的代碼刪掉,(因?yàn)槿绻砑恿藢傩跃蜁詣由蓅etter,getter方法) 再手動添加setter,getter方法.然后再在.m中聲明一個(gè)成員變量_tallRichHandsome存儲這三個(gè)值:
@interfacePerson:NSObject//@property (nonatomic,assign)BOOL tall;//@property (nonatomic,assign)BOOL rich;//@property (nonatomic,assign)BOOL handSome;- (void)setTall:(BOOL)tall;- (void)setRich:(BOOL)rich;- (void)setHandSome:(BOOL)handsome;- (BOOL)isTall;- (BOOL)isRich;- (BOOL)isHandSome;@end@interfacePerson(){char_tallRichHandsome;//0b00000000}@end
取值用&運(yùn)算符.按位與是兩個(gè)為 1 ,結(jié)果才為1.如果我們想要獲取某一位的值,只需要把那一位設(shè)置成1,其他位設(shè)置為0,就可以取出特定位的值.
所以我們只需要在getter方法中按位與一個(gè)特定的值即可,比如我們想要獲取tall,只需按位與0b 0000 0001;獲取rich,就按位與0b 0000 0010,但是這樣運(yùn)算得出的結(jié)果并不是一個(gè)布爾值,而我們是想要BOOL類型,所以我們可以使用return (BOOL)(_tallRichHandsome & 0b00000001)轉(zhuǎn)換類型,也可以這樣return !!(_tallRichHandsome & 0b00000001),使用兩個(gè)!!獲取真實(shí)布爾值.
- (BOOL)isTall{return!!(_tallRichHandsome &0b00000001);}
檢驗(yàn)一下這種寫法的效果,我們在Person類的init方法中給_tallRichHandsome賦值為0b00000101,代表高為1,富為0,帥為1,然后在.m中打印看看:
打印結(jié)果
可以看到結(jié)果完全正確,更改_tallRichHandsome值后再打印也完全正確.我們使用掩碼再繼續(xù)優(yōu)化一下上面的寫法,把位運(yùn)算的值宏定義一下:
#definetallMask 0b00000001#definerichMask 0b00000010#definehandsomeMask 0b00000100
Mask 掩碼,一般用來按位與運(yùn)算,最好用括號括起來,怕影響到運(yùn)算結(jié)果
繼續(xù)優(yōu)化:
#definetallMask (1<<0)//1 左移 0 位#definerichMask (1<<1)// 1 左移 1 位#definehandsomeMask (1<<2)// 1 左移 2 位
剛才驗(yàn)證了取值,接下來研究一下如何賦值.賦值分為兩種情況:如果賦值YES,就使用 按位或運(yùn)算符(|).按位或表示一個(gè)為 1 ,結(jié)果就為 1 .如果賦值NO,就把目標(biāo)位設(shè)置為 0 ,其他位全設(shè)置YES.
比如賦值為YES:比如原始值為0b 0000 0101,標(biāo)識高:YES , 帥:YES.現(xiàn)在要把富也設(shè)置為YES,也就是0b0000 0010,其他位置不變,就要使用按位或|:
如果賦值為NO,比如原始值為0b 0000 0111,標(biāo)識高:YES ,富:YES 帥:YES.現(xiàn)在要把高也設(shè)置為NO,其他不變.結(jié)果就是0b0000 0110,那就應(yīng)該把目標(biāo)位設(shè)置為0,其他位設(shè)置為1,使用按位取反運(yùn)算符~,掩碼就應(yīng)該是0b1111 1110,然后再按位與&:
代碼如下:
- (void)setRich:(BOOL)rich{if(rich) {? ? ? ? _tallRichHandsome |= richMask;? ? }else{? ? ? ? _tallRichHandsome &= ~richMask;? ? }}
測試結(jié)果完全正常:
現(xiàn)在就能滿足我們剛開始的目的了,但是這種做法不好擴(kuò)展也不利于閱讀,我們繼續(xù)完善一下,使用位域這種技術(shù).
我們把剛才的代碼更改一下,把char _tallRichHandsome更改為
struct{chartall :1;//位域 占1位charrich :1;charhandsome :1;? ? }_tallRichHandsome;
注意:char tall : 1是位域的格式,表示只占一位
相應(yīng)的setter , getter方法更改如下:
- (void)setHandSome:(BOOL)handsome{? ? _tallRichHandsome.handsome = handsome;}- (BOOL)isHandSome{return_tallRichHandsome.handsome;}
然后運(yùn)行一下查看結(jié)果:
可以看到給tall賦值后的確發(fā)生了變化,但是為什么是-1呢?我們剛才給tall賦的值,然后結(jié)構(gòu)體中的順序是tall , rich , handsome,內(nèi)存中的位置會按照結(jié)構(gòu)體中的順序從右往左開始存放,也就是現(xiàn)在現(xiàn)在內(nèi)存中的值應(yīng)該是0b 0000 0001,ok,我們來驗(yàn)證一下:
而01的二進(jìn)制就是0000 0001,完全符合我們剛在的結(jié)論,那為什么打印出來的確是-1呢?
我們看看getter方法代碼:
- (BOOL)isTall{return_tallRichHandsome.tall;? ? }
getter方法返回的是BOOL類型,占用一個(gè)字節(jié)(8位),而我們_tallRichHandsome.tall取出來的卻是一位,把一位的1,存放到8位的地址中0b 0000 0000,1 放在最后一位,前七位全補(bǔ)成1,結(jié)果就成了0b1111 1111,就成了無符號的255,有符號的-1.關(guān)于這個(gè)結(jié)論我們也可以驗(yàn)證一下:
的確是 255
所以我們可以還和剛才一樣使用!!取出真實(shí)布爾值,也可以把char tall : 1改成char tall : 2,讓位域占兩位.
struct{chartall :2;//位域 占1位charrich :2;charhandsome :2;? ? }_tallRichHandsome;運(yùn)行結(jié)果如下:周星馳高嗎?1富嗎?0帥嗎0
發(fā)現(xiàn)這樣就不是-1了,其實(shí)這就是位域的一個(gè)小特點(diǎn),我們?nèi)〕鰐all的值01,存放到一個(gè)字節(jié)0000 0000中,結(jié)果就是0000 0001,它會把符號位0補(bǔ)充到其他6位中,結(jié)果就是正常的.
到目前為止我們嘗試了兩種方法達(dá)到這種目的,一種是使用位運(yùn)算,另一種是使用結(jié)構(gòu)體的位域.那我們能不能綜合前兩種方法,對結(jié)構(gòu)體進(jìn)行位運(yùn)算呢?我們來試試:
如圖所示,結(jié)構(gòu)體根本就無法進(jìn)行位運(yùn)算,那怎么辦呢?我們可以參考一下蘋果大大優(yōu)化isa指針的做法,使用共用體(union):
union{charbits;struct{chartall :1;//位域 占1位charrich :1;charhandsome :1;? ? ? ? };? ? }_tallRichHandsome;
我們運(yùn)行一下發(fā)現(xiàn)完全正常,我們把結(jié)構(gòu)體刪掉在運(yùn)行一下:
union{charbits;? ? }_tallRichHandsome;
發(fā)現(xiàn)也完全正常,其實(shí)這里的結(jié)構(gòu)體完全就是增加代碼的可讀性.這種做法其實(shí)就是將位運(yùn)算和結(jié)構(gòu)體的位域結(jié)合在一起,利用結(jié)構(gòu)體的位域增加可讀性,利用位運(yùn)算達(dá)到想取哪里去哪里的目的.
下面我們詳細(xì)介紹一下共用體(union),我們將結(jié)構(gòu)體struct和共用體union對比介紹.比如說現(xiàn)在有一個(gè)結(jié)構(gòu)體Date和共用體Date:
struct{intyear;intmonth;intday;? ? }Date;union{intyear;intmonth;intday;? ? }Day;
他們的內(nèi)存結(jié)構(gòu)如圖所示:
可以看到,結(jié)構(gòu)體的內(nèi)存都是獨(dú)立的,每個(gè)成員占用4個(gè)字節(jié),一共占用12個(gè)字節(jié);而共用體的內(nèi)存是連續(xù)的,所有成員公用4個(gè)字節(jié)的內(nèi)存,共用體內(nèi)存的大小取決于最大的成員所分配的內(nèi)存.
下圖就證明了共用體的成員是共用一塊內(nèi)存的,我們給year賦值,然后打印month,結(jié)果值確是year的值:
總結(jié):在ARM64位之前isa就是個(gè)普通的指針,實(shí)例對象的isa指向類對象,類對象的isa指向?qū)嵗悓ο?在ARM64位之后,isa進(jìn)行了優(yōu)化,采取了共用體的結(jié)構(gòu),使用64位的內(nèi)存數(shù)據(jù)存儲更多的信息,其中的33位存儲具體的地址值.
關(guān)于位運(yùn)算的一些擴(kuò)展
我們在項(xiàng)目中肯定用過 KVO,[self addObserver:self forKeyPath:@"view" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];它的內(nèi)部是怎么樣處理我們傳進(jìn)的多個(gè)值得呢?我們可以模仿一下:
typedefenum{? ? Monday =1,//0b 0000 0001Tuesday =2,//0b 0000 0010Wedensday =4,//0b 0000 0100Thursday =8,//0b 0000 1000Friday =16,//0b 0001 0000Saturday =32,//0b 0010 0000Sunday =64,//0b 0100 0000}Week;
我們定義一個(gè)Week類型的結(jié)構(gòu)體,周一 至 周日,并設(shè)置初始值,大家可以看到他們的初始值是有規(guī)律的,分別是2的0次方,1次方,2次方...6次方.對應(yīng)的二進(jìn)制也是1<<0,1<<1 ... 1<<6.然后我們在定義一個(gè)方法- (void)setWeek:(Week)week再調(diào)用這個(gè)方法[self setWeek:Saturday | Sunday];在這個(gè)方法內(nèi)判斷如果是周末就打印 打游戲,是工作日就打印 敲代碼.怎么實(shí)現(xiàn)呢?
首先我們分析一下[self setWeek:Saturday | Sunday];我們知道或運(yùn)算是一個(gè)為1結(jié)果就為1,所以Saturday | Sunday結(jié)果應(yīng)該是:
? 0010 0000
| 0100 0000
---------------
? 0110 0000
然后我們再用這個(gè)結(jié)果0110 0000按位與上 Saturday , Sunday,如果結(jié)果不為0,就說明符合條件:
- (void)setWeek:(Week)week{if(week & Saturday) {NSLog(@"Saturday 打游戲");? ? }if(week & Sunday) {NSLog(@"Sunday 打游戲");? ? }}===============================================2019-01-3111:00:59.609826+0800MultiThread[2195:466594] Saturday 打游戲2019-01-3111:00:59.609980+0800MultiThread[2195:466594] Sunday 打游戲
這樣就實(shí)現(xiàn)了我們的需求,我們把[self setWeek:Saturday | Sunday];改為[self setWeek:Saturday + Sunday];看看效果:
[selfsetWeek:Saturday + Sunday];===============================================- (void)setWeek:(Week)week{if(week & Saturday) {NSLog(@"Saturday 打游戲");? ? }if(week & Sunday) {NSLog(@"Sunday 打游戲");? ? }}===============================================2019-01-3111:12:10.965097+0800MultiThread[2291:476407] Saturday 打游戲2019-01-3111:12:10.965285+0800MultiThread[2291:476407] Sunday 打游戲
結(jié)果也完全一樣,說明+和|在這里是等價(jià)的,但是要注意:只有當(dāng)他們的初始值是2的n次方的時(shí)候才能使用+號,一般不建議使用+,這樣會顯得你很low所以我們還是使用|
蘋果的源碼也是這樣來設(shè)計(jì)實(shí)現(xiàn)的,我們看看:
NSKeyValueObservingOptionNew=0x01,// 1NSKeyValueObservingOptionOld=0x02,// 2NSKeyValueObservingOptionInitial=0x04,// 4NSKeyValueObservingOptionPrior=0x08,//8
ok,前面講了這么多的掩碼,位域,共用體等等其實(shí)都是為了鋪墊,都是為了引出最終的 BOSS? ==> isa,現(xiàn)在我們就賴仔細(xì)看看isa指針.
首先查看isa源碼:
#if__arm64__#defineISA_MASK? ? ? ? 0x0000000ffffffff8ULL#defineISA_MAGIC_MASK? 0x000003f000000001ULL#defineISA_MAGIC_VALUE 0x000001a000000001ULLstruct{uintptr_tnonpointer? ? ? ? :1;uintptr_thas_assoc? ? ? ? :1;uintptr_thas_cxx_dtor? ? ? :1;uintptr_tshiftcls? ? ? ? ? :33;// MACH_VM_MAX_ADDRESS 0x1000000000uintptr_tmagic? ? ? ? ? ? :6;uintptr_tweakly_referenced :1;uintptr_tdeallocating? ? ? :1;uintptr_thas_sidetable_rc? :1;uintptr_textra_rc? ? ? ? ? :19;#defineRC_ONE? (1ULL<<45)#defineRC_HALF? (1ULL<<18)};
對應(yīng)注解
現(xiàn)在帶入到項(xiàng)目中驗(yàn)證一下,注意驗(yàn)證的時(shí)候要使用真機(jī),因?yàn)槟M器中存儲的位置和真機(jī)的不一樣.
首先我們打印出一個(gè)ViewController的內(nèi)存地址:
然后用計(jì)算器查看這個(gè)地址的二進(jìn)制:
我們對比上面的注釋圖查看分析一下這個(gè)二進(jìn)制:
第0位也就是最后邊的1是nonpointer的值,如果為0表示這個(gè)isa就是一個(gè)普通的指針,值存儲著類對象或者元類對象的內(nèi)存地址;如果為1則表示這個(gè)isa是經(jīng)過優(yōu)化過的,使用位域存儲更多信息.
...
剩下的信息我就不一一對比了,有興趣的可以自己對比試驗(yàn)
轉(zhuǎn)自:http://www.reibang.com/p/e78a858ea7cf