IOS的isa指針優(yōu)化

我們知道,判定是不是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末娜扇,一起剝皮案震驚了整個(gè)濱河市错沃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袱衷,老刑警劉巖捎废,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異致燥,居然都是意外死亡登疗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門嫌蚤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辐益,“玉大人,你說我怎么就攤上這事脱吱≈钦” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵箱蝠,是天一觀的道長续捂。 經(jīng)常有香客問我,道長宦搬,這世上最難降的妖魔是什么牙瓢? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮间校,結(jié)果婚禮上矾克,老公的妹妹穿的比我還像新娘。我一直安慰自己憔足,他們只是感情好胁附,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著滓彰,像睡著了一般控妻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上找蜜,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天饼暑,我揣著相機(jī)與錄音,去河邊找鬼。 笑死弓叛,一個(gè)胖子當(dāng)著我的面吹牛彰居,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撰筷,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼陈惰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了毕籽?” 一聲冷哼從身側(cè)響起抬闯,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎关筒,沒想到半個(gè)月后溶握,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒸播,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年睡榆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袍榆。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胀屿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出包雀,到底是詐尸還是另有隱情宿崭,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布才写,位于F島的核電站葡兑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赞草。R本人自食惡果不足惜铁孵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望房资。 院中可真熱鬧,春花似錦檀头、人聲如沸轰异。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搭独。三九已至,卻和暖如春廊镜,著一層夾襖步出監(jiān)牢的瞬間牙肝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留配椭,地道東北人虫溜。 一個(gè)月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像股缸,于是被迫代替她去往敵國和親衡楞。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359