我們知道,判定是不是OC對象的本質就是看是否含有isa
指針,在ARM64
架構之前,objc_object
的isa
指針就是一個class
類型,存儲的就是一個指針,而ARM64
系統之后,對isa
進行了優化,變成了一個共用體(union
),還是用位域來存儲更多信息.
今天我們就來研究一下共同體(
union
)和位域.我們先通過一個小場景來開始今天的內容,我們先創建一個
Person
類,類中有三個BOOL
類型的屬性:tall , rich ,handsome
,分別表示高,富,帥.然后通過
class_getInstanceSize
查看這個類對象占用多少字節,發現打印輸出是:16.為什么是16個字節呢?因為3個BOOL
類型的屬性占用3個字節,isa
指針占用8個字節,一共占用11個字節,再內存對齊以后,就是16個字節.這樣我們就會發現一個問題,三個
BOOL
類型的屬性占用了3個字節,其實BOOL
類型屬性本質就是0 或 1
,只占一位,也就是說3個BOOL
屬性放在一個字節就可以搞定.比如說有一個字節的內存0x0000 0000
我們用最后3位分別存放高,富,帥
,如圖所示:?思考一下:怎樣才能做到只用一位去存放三個屬性呢?
只能通過位運算做到了.我們先把屬性相關的代碼刪掉,(因為如果添加了屬性就會自動生成
setter,getter
方法) 再手動添加setter,getter
方法.然后再在.m
中聲明一個成員變量_tallRichHandsome
存儲這三個值:
@interface Person : 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
@interface Person ()
{
char _tallRichHandsome; //0b00000000
}
@end
- 取值用
&
運算符.按位與是兩個為 1 ,結果才為1.如果我們想要獲取某一位的值,只需要把那一位設置成1,其他位設置為0,就可以取出特定位的值.
所以我們只需要在getter
方法中按位與一個特定的值即可,比如我們想要獲取tall
,只需按位與0b 0000 0001
;獲取rich
,就按位與0b 0000 0010
,但是這樣運算得出的結果并不是一個布爾值,而我們是想要BOOL
類型,所以我們可以使用return (BOOL)(_tallRichHandsome & 0b00000001)
轉換類型,也可以這樣return !!(_tallRichHandsome & 0b00000001)
,使用兩個!!
獲取真實布爾值.
- (BOOL)isTall{
return !!(_tallRichHandsome & 0b00000001);
}
檢驗一下這種寫法的效果,我們在Person
類的init
方法中給_tallRichHandsome
賦值為0b00000101
,代表高為1,富為0,帥為1,然后在.m
中打印看看:
可以看到結果完全正確,更改
_tallRichHandsome
值后再打印也完全正確.我們使用掩碼再繼續優化一下上面的寫法,把位運算的值宏定義一下:
#define tallMask 0b00000001
#define richMask 0b00000010
#define handsomeMask 0b00000100
Mask 掩碼,一般用來按位與運算,最好用括號括起來,怕影響到運算結果
繼續優化:
#define tallMask (1<<0) //1 左移 0 位
#define richMask (1<<1)// 1 左移 1 位
#define handsomeMask (1<<2) // 1 左移 2 位
- 剛才驗證了取值,接下來研究一下如何賦值.賦值分為兩種情況:如果賦值
YES
,就使用 按位或運算符(|
).按位或表示一個為 1 ,結果就為 1 .如果賦值NO
,就把目標位設置為 0 ,其他位全設置YES
.
- 比如賦值為
YES
:比如原始值為0b 0000 0101
,標識高:YES , 帥:YES.現在要把富也設置為YES,也就是0b0000 0010
,其他位置不變,就要使用按位或|
:
- 如果賦值為
NO
,比如原始值為0b 0000 0111
,標識高:YES ,富:YES 帥:YES.現在要把高也設置為NO,其他不變.結果就是0b0000 0110
,那就應該把目標位設置為0
,其他位設置為1
,使用按位取反運算符~
,掩碼就應該是0b1111 1110
,然后再按位與&
:
代碼如下:
- (void)setRich:(BOOL)rich{
if (rich) {
_tallRichHandsome |= richMask;
}else{
_tallRichHandsome &= ~richMask;
}
}
測試結果完全正常:
現在就能滿足我們剛開始的目的了,但是這種做法不好擴展也不利于閱讀,我們繼續完善一下,使用位域這種技術.
我們把剛才的代碼更改一下,把
char _tallRichHandsome
更改為
struct{
char tall : 1;//位域 占1位
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
注意:char tall : 1
是位域的格式,表示只占一位
相應的setter , getter
方法更改如下:
- (void)setHandSome:(BOOL)handsome{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)isHandSome{
return _tallRichHandsome.handsome;
}
然后運行一下查看結果:
可以看到給
tall
賦值后的確發生了變化,但是為什么是 -1
呢?我們剛才給tall
賦的值,然后結構體中的順序是tall , rich , handsome
,內存中的位置會按照結構體中的順序從右往左開始存放,也就是現在現在內存中的值應該是0b 0000 0001
,ok,我們來驗證一下:而
01
的二進制就是0000 0001
,完全符合我們剛在的結論,那為什么打印出來的確是 -1
呢?我們看看
getter
方法代碼:
- (BOOL)isTall{
return _tallRichHandsome.tall;
}
getter
方法返回的是BOOL
類型,占用一個字節(8位),而我們_tallRichHandsome.tall
取出來的卻是一位,把一位的1
,存放到8位的地址中0b 0000 0000
,1 放在最后一位,前七位全補成1,結果就成了0b1111 1111
,就成了無符號的 255
,有符號的-1
.關于這個結論我們也可以驗證一下:
所以我們可以還和剛才一樣使用
!!
取出真實布爾值,也可以把char tall : 1
改成char tall : 2
,讓位域占兩位.
struct{
char tall : 2;//位域 占1位
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
運行結果如下:
周星馳高嗎?1
富嗎?0
帥嗎0
發現這樣就不是-1
了,其實這就是位域的一個小特點,我們取出tall
的值01
,存放到一個字節0000 0000
中,結果就是0000 0001
,它會把符號位0
補充到其他6位中,結果就是正常的.
- 到目前為止我們嘗試了兩種方法達到這種目的,一種是使用位運算,另一種是使用結構體的位域.那我們能不能綜合前兩種方法,對結構體進行位運算呢?我們來試試:
如圖所示,結構體根本就無法進行位運算,那怎么辦呢?我們可以參考一下蘋果大大優化isa
指針的做法,使用共用體(union
):
union{
char bits;
struct{
char tall : 1;//位域 占1位
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
我們運行一下發現完全正常,我們把結構體刪掉在運行一下:
union{
char bits;
}_tallRichHandsome;
發現也完全正常,其實這里的結構體完全就是增加代碼的可讀性.這種做法其實就是將位運算和結構體的位域結合在一起,利用結構體的位域增加可讀性,利用位運算達到想取哪里去哪里的目的.
下面我們詳細介紹一下共用體(union
),我們將結構體struct
和共用體union
對比介紹.比如說現在有一個結構體Date
和共用體Date
:
struct{
int year;
int month;
int day;
}Date;
union{
int year;
int month;
int day;
}Day;
他們的內存結構如圖所示:
可以看到,結構體的內存都是獨立的,每個成員占用4個字節,一共占用12個字節;而共用體的內存是連續的,所有成員公用4個字節的內存,共用體內存的大小取決于最大的成員所分配的內存.
下圖就證明了共用體的成員是共用一塊內存的,我們給year
賦值,然后打印month
,結果值確是year
的值:
總結:在ARM64位之前isa就是個普通的指針,實例對象的isa指向類對象,類對象的isa指向實例元類對象,在ARM64位之后,isa進行了優化,采取了共用體的結構,使用64位的內存數據存儲更多的信息,其中的33位存儲具體的地址值.
關于位運算的一些擴展
我們在項目中肯定用過 KVO,[self addObserver:self forKeyPath:@"view" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
它的內部是怎么樣處理我們傳進的多個值得呢?我們可以模仿一下:
typedef enum{
Monday = 1, //0b 0000 0001
Tuesday = 2, //0b 0000 0010
Wedensday = 4, //0b 0000 0100
Thursday = 8, //0b 0000 1000
Friday = 16, //0b 0001 0000
Saturday = 32, //0b 0010 0000
Sunday = 64, //0b 0100 0000
}Week;
我們定義一個Week
類型的結構體,周一 至 周日,并設置初始值,大家可以看到他們的初始值是有規律的,分別是2的0次方,1次方,2次方...6次方
.對應的二進制也是1<<0,1<<1 ... 1<<6
.然后我們在定義一個方法- (void)setWeek:(Week)week
再調用這個方法[self setWeek:Saturday | Sunday];
在這個方法內判斷如果是周末就打印 打游戲,是工作日就打印 敲代碼.怎么實現呢?
首先我們分析一下[self setWeek:Saturday | Sunday];
我們知道或運算是一個為1結果就為1,所以Saturday | Sunday
結果應該是:
0010 0000
| 0100 0000
---------------
0110 0000
然后我們再用這個結果0110 0000
按位與上 Saturday , Sunday,如果結果不為0,就說明符合條件:
- (void)setWeek:(Week)week{
if (week & Saturday) {
NSLog(@"Saturday 打游戲");
}
if (week & Sunday) {
NSLog(@"Sunday 打游戲");
}
}
===============================================
2019-01-31 11:00:59.609826+0800 MultiThread[2195:466594] Saturday 打游戲
2019-01-31 11:00:59.609980+0800 MultiThread[2195:466594] Sunday 打游戲
這樣就實現了我們的需求,我們把[self setWeek:Saturday | Sunday];
改為[self setWeek:Saturday + Sunday];
看看效果:
[self setWeek:Saturday + Sunday];
===============================================
- (void)setWeek:(Week)week{
if (week & Saturday) {
NSLog(@"Saturday 打游戲");
}
if (week & Sunday) {
NSLog(@"Sunday 打游戲");
}
}
===============================================
2019-01-31 11:12:10.965097+0800 MultiThread[2291:476407] Saturday 打游戲
2019-01-31 11:12:10.965285+0800 MultiThread[2291:476407] Sunday 打游戲
結果也完全一樣,說明+
和|
在這里是等價的,但是要注意:只有當他們的初始值是2的n次方的時候才能使用+
號,一般不建議使用+
,這樣會顯得你很low
所以我們還是使用|
蘋果的源碼也是這樣來設計實現的,我們看看:
NSKeyValueObservingOptionNew = 0x01, // 1
NSKeyValueObservingOptionOld = 0x02,// 2
NSKeyValueObservingOptionInitial = 0x04,// 4
NSKeyValueObservingOptionPrior = 0x08,//8
ok,前面講了這么多的掩碼,位域,共用體等等其實都是為了鋪墊,都是為了引出最終的 BOSS ==> isa,現在我們就來仔細看看isa指針.
首先查看isa
源碼:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
現在帶入到項目中驗證一下,注意驗證的時候要使用真機,因為模擬器中存儲的位置和真機的不一樣.
首先我們打印出一個
ViewController
的內存地址:然后用計算器查看這個地址的二進制:
我們對比上面的注釋圖查看分析一下這個二進制:
- 第0位也就是最后邊的1是
nonpointer
的值,如果為0表示這個isa就是一個普通的指針,值存儲著類對象或者元類對象的內存地址;如果為1則表示這個isa是經過優化過的,使用位域存儲更多信息.
...
剩下的信息我就不一一對比了,有興趣的可以自己對比試驗.