面試題引發的思考:
Q: 簡述isa
指針?
- 從
__arm64__
架構開始,isa
指針: - 不只是存儲了 class對象 或 meta-class對象 的地址;
-
而是使用 共用體結構 存儲了更多信息:
其中shiftcls
存儲了 class對象 或 meta-class對象 的地址;
需要shiftcls
同ISA_MASK
進行按位&
運算取出。
Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。
要想學習Runtime,首先要了解它底層的一些常用數據結構,比如isa
指針:
- 每個OC對象都有一個
isa
指針;- instance對象的
isa
指向class對象;- class對象的
isa
指向meta-class對象;- OC對象的
isa
指針并不是直接指向class對象或者meta-class對象,而是需要&ISA_MASK
通過位運算才能獲取到類對象或者元類對象的地址;
a> 在__arm64__
架構之前,isa
就是一個普通的指針,存儲著Class對象、Meta-Class對象的內存地址;
b> 從__arm64__
架構開始,對isa
進行了優化,變成了一個共用體(union
)結構,還使用位域來存儲更多的信息。
進入OC源碼查看isa
指針,進行更深入的了解:
由上圖可知:
isa
指針其實是一個isa_t
類型的共用體;
此共用體中包含一個結構體;
此結構體內部定義了若干變量,變量后面的值則表示該變量占用的位數,即位域。
接下來一步步分析共用體的優勢。
(1) 探究
// TODO: ----------------- Person類 -----------------
@interface Person : NSObject
// 聲明屬性,系統會自動生成成員變量
@property (nonatomic, assign, getter=isTall) BOOL tall;
@property (nonatomic, assign, getter=isRich) BOOL rich;
@property (nonatomic, assign, getter=isHandsome) BOOL handsome;
@end
@implementation Person
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"%zd", class_getInstanceSize([Person class]));
}
return 0;
}
// 打印結果
Demo[1234:567890] 16
由以上代碼可知:
isa
指針占8個字節,3個BOOL
類型的分別占1個字節,一共11個字節。由于內存對齊原則,所以Person
類對象所占內存應該為16個字節。
BOOL
值包含0
或1
,只需要1個二進制位就可以表示出來,而現在需要占用1個字節共8個二進制位。
可以使用1個字節中的3個二進制位表示3個BOOL
值,這樣可以很大程度上節省內存空間。
如果聲明屬性,系統就會自動生成成員變量,占用的內存會大于1位;所以不可以聲明屬性,需要手動實現每個屬性的set
方法和get
方法。
現在添加一個char
類型的成員變量,char
變量占用一個字節(8位)的內存空間,使用最后3位來存儲3個BOOL
值。
(2) 位運算
-
補碼(負數是以補碼的形式表示)
十進制正整數轉換為二進制數:除2
取余即可;
十進制負整數轉換為二進制數:除2
取余,取反加1
;
// -10用二進制表示
0b0000 0000 0000 1010 -(10除2取余)
0b1111 1111 1111 0101 -(取反)
0b1111 1111 1111 0110 -(加1)
0b1111 1111 1111 0110 -(得出-10的二進制)
-
按位與(&)
同真為真,其余為假:清零特定位、取出特定位;
// 按位與(&)
0b0000 1111 0000 1111
& 0b0000 0000 1111 1111
---------------------
0b0000 0000 0000 1111
-
按位或(|)
同假為假,其余為真:特定位置1
;
// 按位或(|)
0b0000 1111 0000 1111
| 0b0000 0000 1111 1111
---------------------
0b0000 1111 1111 1111
-
按位異或(^)
異值為真,同值為假:特定位取反、交換兩變量的值;
// 按位異或(^)
0b0000 1111 0000 1111
^ 0b0000 0000 1111 1111
---------------------
0b0000 1111 1111 0000
-
取反(~)
按位取反
// 取反(~)
~ 0b0000 1111 0000 1111
---------------------
0b1111 0000 1111 0000
-
左移(<<)
按位左移,高位丟棄,低位補0
;
// 左移(<<)兩位
0b1111 0000 0000 1111 << 2
---------------------
0b1100 0000 0011 1100
-
右移(>>)
按位右移,高位正數補0
,負數補1
;
// 右移(>>)兩位
0b1111 0000 0000 1111 >> 2
---------------------
0b0011 1100 0000 0011
(3) 手動實現屬性的set
方法和get
方法
// TODO: ----------------- Person類 -----------------
//#define TallMask 0b00000001 // 1
//#define RichMask 0b00000010 // 2
//#define HandsomeMask 0b0000100 // 4
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)
@interface Person : NSObject {
char _tallRichHandsome; // 0b0000 0000
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
@implementation Person
- (void)setTall:(BOOL)tall {
if (tall) { // 按位或 - 特定位置1
_tallRichHandsome |= TallMask;
} else { // 掩碼先取反,后按位與 - 特定位置0
_tallRichHandsome &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich {
if (rich) {
_tallRichHandsome |= RichMask;
} else {
_tallRichHandsome &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome {
if (handsome) {
_tallRichHandsome |= HandsomeMask;
} else {
_tallRichHandsome &= ~HandsomeMask;
}
}
- (BOOL)isTall {
// 按位與 - 取出特定位
// 兩次取反!,強制轉換成BOOL類型
return !!(_tallRichHandsome & TallMask);
}
- (BOOL)isRich {
return !!(_tallRichHandsome & RichMask);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome & HandsomeMask);
}
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall: %d, rich: %d, handsome: %d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
// 打印結果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1
上述代碼可以正常存值和取值,但是可拓展性和可讀性差。
(4) 位域
位域聲明:
位域名 : 位域長度
使用位域需要注意以下3點:
- 如果一個字節所剩空間不夠存放另一位域時,應從下一單元起存放該位域;
也可以有意使某位域從下一單元開始。- 位域的長度不能大于數據類型本身的長度;
比如int
類型就不能超過32位二進制位。- 位域可以無位域名,這時它只用來作填充或調整位置;
無名的位域是不能使用的。
// TODO: ----------------- Person類 -----------------
@interface Person : NSObject {
// 位域,tall、rich、handsome按序各占1個二進制位
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
@implementation Person
- (void)setTall:(BOOL)tall {
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
_tallRichHandsome.handsome = handsome;
}
- (BOOL)isTall {
// _tallRichHandsome.tall == 0b1
// 0b1111 1111 == -1
return !!_tallRichHandsome.tall;
}
- (BOOL)isRich {
return !!_tallRichHandsome.rich;
}
- (BOOL)isHandsome {
return !!_tallRichHandsome.handsome;
}
@end
// 打印結果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1
上述代碼使用結構體的位域,不需要再使用掩碼,缺點是效率比使用位運算時低。
(5) 共用體
// TODO: ----------------- Person類 -----------------
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)
@interface Person : NSObject {
union { // 共用體
char bits;
//struct 增加代碼可讀性,可注釋掉
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
@implementation Person
- (void)setTall:(BOOL)tall {
if (tall) {
_tallRichHandsome.bits |= TallMask;
} else {
_tallRichHandsome.bits &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich {
if (rich) {
_tallRichHandsome.bits |= RichMask;
} else {
_tallRichHandsome.bits &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome {
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
} else {
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)isTall {
// 兩次取反!,強制轉換成BOOL類型
return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)isRich {
return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome.bits & HandsomeMask);
}
@end
// 打印結果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1
上述代碼使用共用體對數據進行位運算取值和賦值,效率高同時占用內存少,代碼可讀性高。
(6) isa
詳解
經過以上分析,我們可以清晰地了解到位運算、位域以及共用體的相關知識。
下面我們深入分析isa
的結構:
由上圖可知:
isa_t
共用體存儲了8個字節64位的值,所有信息都存儲在bits
中,這些值在結構體中展現出來,這些值通過對bits
進行掩碼位運算取出。
名稱 | 作用 |
---|---|
nonpointer |
0 代表普通的指針,存儲著Class,Meta-Class對象的內存地址;1 代表優化過,使用位域存儲更多的信息 |
has_assoc |
是否有設置過關聯對象,如果沒有,釋放時會更快 |
has_cxx_dtor |
是否有C++析構函數,如果沒有,釋放時會更快 |
shiftcls |
存儲著Class、Meta-Class對象的內存地址信息 |
magic |
用于在調試時分辨對象是否未完成初始化 |
weakly_referenced |
是否有被弱引用指向過,如果沒有,釋放時會更快 |
deallocating |
對象是否正在釋放 |
has_sidetable_rc |
引用計數器是否過大無法存儲在isa 中;如果為1 ,那么引用計數會存儲在一個叫SideTable 的類的屬性中 |
extra_rc |
里面存儲的值是引用計數器減1
|
重點介紹一下shiftcls
:
shiftcls
存儲著Class、Meta-Class對象的內存地址信息;- 通過
bits & ISA_MASK
取得shiftcls
的值;- 而掩碼
ISA_MASK
值為0x0000000ffffffff8ULL
,說明Class、Meta-Class對象的內存地址值后三位一定為0
。
接下來驗證一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall: %d, rich: %d, handsome: %d", person.isTall, person.isRich, person.isHandsome);
NSLog(@"class: %p, meta-class: %p", [Person class], object_getClass([Person class]));
}
return 0;
}
// 打印結果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1
Demo[1234:567890] class: 0x100001260, meta-class: 0x100001238
由打印結果可知:
class對象的地址值為0x100001260
,尾數是0
;meta-class對象的地址值為0x100001238
,尾數是8
;在16進制下,內存地址的后三位都為0
;驗證結束。
總結可知:
- 從
__arm64__
架構開始,isa
指針不只是存儲了class對象或meta-class對象的地址;- 而是使用共用體結構存儲了更多信息:
其中shiftcls
存儲了class或meta-class的地址;
shiftcls
需要同ISA_MASK
進行按位&
運算才可以取出class對象或meta-class對象的地址。