isa的本質(zhì)
在學(xué)習(xí)Runtime之前首先需要對(duì)isa的本質(zhì)有一定的了解,這樣之后學(xué)習(xí)Runtime會(huì)更便于理解。
回顧OC對(duì)象的本質(zhì),每個(gè)OC對(duì)象都含有一個(gè)isa指針,__arm64__之前,isa僅僅是一個(gè)指針,保存著對(duì)象或類對(duì)象內(nèi)存地址,在__arm64__架構(gòu)之后,apple對(duì)isa進(jìn)行了優(yōu)化,變成了一個(gè)共用體(union)結(jié)構(gòu),同時(shí)使用位域來存儲(chǔ)更多的信息。
我們知道OC對(duì)象的isa指針并不是直接指向類對(duì)象或者元類對(duì)象,而是需要&ISA_MASK通過位運(yùn)算才能獲取到類對(duì)象或者元類對(duì)象的地址。今天來探尋一下為什么需要&ISA_MASK才能獲取到類對(duì)象或者元類對(duì)象的地址,以及這樣的好處。
首先在源碼中找到isa指針,看一下isa指針的本質(zhì)。
// 截取objc_object內(nèi)部分代碼
struct objc_object {
private:
isa_t isa;
}
isa指針其實(shí)是一個(gè)isa_t類型的共用體,來到isa_t內(nèi)部查看其結(jié)構(gòu)
// 精簡(jiǎn)過的isa_t共用體
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_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)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
#endif
上述源碼中isa_t是union類型,union表示共用體??梢钥吹焦灿皿w中有一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部分別定義了一些變量,變量后面的值代表的是該變量占用多少個(gè)字節(jié),也就是位域技術(shù)。
共用體:在進(jìn)行某些算法的C語言編程的時(shí)候,需要使幾種不同類型的變量存放到同一段內(nèi)存單元中。也就是使用覆蓋技術(shù),幾個(gè)變量互相覆蓋。這種幾個(gè)不同的變量共同占用一段內(nèi)存的結(jié)構(gòu),在C語言中,被稱作“共用體”類型結(jié)構(gòu),簡(jiǎn)稱共用體。
接下來使用共用體的方式來深入的了解apple為什么要使用共用體,以及使用共用體的好處。
探尋過程
接下來使用代碼來模仿底層的做法,創(chuàng)建一個(gè)person類并含有三個(gè)BOOL類型的成員變量。
@interface Person : NSObject
@property (nonatomic, assign, getter = isTall) BOOL tall;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@property (nonatomic, assign, getter = isHansome) BOOL handsome;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd", class_getInstanceSize([Person class]));
}
return 0;
}
// 打印內(nèi)容
// Runtime - union探尋[52235:3160607] 16
上述代碼中Person含有3個(gè)BOOL類型的屬性,打印Person類對(duì)象占據(jù)內(nèi)存空間為16,也就是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13。因?yàn)閮?nèi)存對(duì)齊原則所以Person類對(duì)象占據(jù)內(nèi)存空間為16。
上面提到過共用體中變量可以相互覆蓋,可以使幾個(gè)不同的變量存放到同一段內(nèi)存單元中,可以很大程度上節(jié)省內(nèi)存空間。
那么我們知道BOOL值只有兩種情況 0 或者 1,但是卻占據(jù)了一個(gè)字節(jié)的內(nèi)存空間,而一個(gè)內(nèi)存空間中有8個(gè)二進(jìn)制位,并且二進(jìn)制只有 0 或者 1 。那么是否可以使用1個(gè)二進(jìn)制位來表示一個(gè)BOOL值,也就是說3個(gè)BOOL值最終只使用3個(gè)二進(jìn)制位,也就是一個(gè)內(nèi)存空間即可呢?如何實(shí)現(xiàn)這種方式?
首先如果使用這種方式需要自己寫方法聲明與實(shí)現(xiàn),不可以寫屬性,因?yàn)橐坏憣傩裕到y(tǒng)會(huì)自動(dòng)幫我們添加成員變量。
另外想要將三個(gè)BOOL值存放在一個(gè)字節(jié)中,我們可以添加一個(gè)char類型的成員變量,char類型占據(jù)一個(gè)字節(jié)內(nèi)存空間,也就是8個(gè)二進(jìn)制位??梢允褂闷渲凶詈笕齻€(gè)二進(jìn)制位來存儲(chǔ)3個(gè)BOOL值。
@interface Person()
{
char _tallRichHandsome;
}
例如_tallRichHansome的值為 0b 0000 0010 ,那么只使用8個(gè)二進(jìn)制位中的最后3個(gè),分別為其賦值0或者1來代表tall、rich、handsome的值。如下圖所示

那么現(xiàn)在面臨的問題就是如何取出8個(gè)二進(jìn)制位中的某一位的值,或者為某一位賦值呢?
取值
首先來看一下取值,假如char類型的成員變量中存儲(chǔ)的二進(jìn)制為0b 0000 0010如果想將倒數(shù)第2位的值也就是rich的值取出來,可以使用&進(jìn)行按位與運(yùn)算進(jìn)而去除相應(yīng)位置的值。
&:按位與,同真為真,其他都為假。
// 示例
// 取出倒數(shù)第三位 tall
0000 0010
& 0000 0100
------------
0000 0000 // 取出倒數(shù)第三位的值為0,其他位都置為0
// 取出倒數(shù)第二位 rich
0000 0010
& 0000 0010
------------
0000 0010 // 取出倒數(shù)第二位的值為1,其他位都置為0
按位與可以用來取出特定的位,想取出哪一位就將那一位置為1,其他為都置為0,然后同原數(shù)據(jù)進(jìn)行按位與計(jì)算,即可取出特定的位。
那么此時(shí)可以將get方法寫成如下方式
#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1
- (BOOL)tall
{
return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome & HandsomeMask);
}
上述代碼中使用兩個(gè)!!(非)來將值改為bool類型。同樣使用上面的例子
// 取出倒數(shù)第二位 rich
0000 0010 // _tallRichHandsome
& 0000 0010 // RichMask
------------
0000 0010 // 取出rich的值為1,其他位都置為0
上述代碼中(_tallRichHandsome & TallMask)的值為0000 0010也就是2,但是我們需要的是一個(gè)BOOL類型的值 0 或者 1 ,那么!!2就將 2 先轉(zhuǎn)化為 0 ,之后又轉(zhuǎn)化為 1。相反如果按位與取得的值為 0 時(shí),!!0將 0 先轉(zhuǎn)化為 1 之后又轉(zhuǎn)化為 0。 因此使用!!兩個(gè)非操作將值轉(zhuǎn)化為 0 或者 1 來表示相應(yīng)的值。
掩碼 : 上述代碼中定義了三個(gè)宏,用來分別進(jìn)行按位與運(yùn)算而取出相應(yīng)的值,一般用來按位與(&)運(yùn)算的值稱之為掩碼。
為了能更清晰的表明掩碼是為了取出哪一位的值,上述三個(gè)宏的定義可以使用<<(左移)來優(yōu)化
<<:表示左移一位,下圖為例。

那么上述宏定義可以使用<<(左移)優(yōu)化成如下代碼
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
設(shè)值
設(shè)值即是將某一位設(shè)值為0或者1,可以使用|(按位或)操作符。 | : 按位或,只要有一個(gè)1即為1,否則為0。
如果想將某一位置為1的話,那么將原本的值與掩碼進(jìn)行按位或的操作即可,例如我們想將tall置為1
// 將倒數(shù)第三位 tall置為1
0000 0010 // _tallRichHandsome
| 0000 0100 // TallMask
------------
0000 0110 // 將tall置為1,其他位值都不變
如果想將某一位置為0的話,需要將掩碼按位取反(~ : 按位取反符),之后在與原本的值進(jìn)行按位與操作即可。
// 將倒數(shù)第二位 rich置為0
0000 0010 // _tallRichHandsome
& 1111 1101 // RichMask按位取反
------------
0000 0000 // 將rich置為0,其他位值都不變
此時(shí)set方法內(nèi)部實(shí)現(xiàn)如下
- (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;
}
}
寫完set、get方法之后通過代碼來查看一下是否可以設(shè)值、取值成功。
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.tall,person.rich,person.handsome);
}
return 0;
}
打印內(nèi)容
Runtime - union探尋[58212:3857728] tall : 1, rich : 0, handsome : 1
可以看出上述代碼可以正常賦值和取值。但是代碼還是有一定的局限性,當(dāng)需要添加新屬性的時(shí)候,需要重復(fù)上述工作,并且代碼可讀性比較差。接下來使用結(jié)構(gòu)體的位域特性來優(yōu)化上述代碼。
位域
將上述代碼進(jìn)行優(yōu)化,使用結(jié)構(gòu)體位域,可以使代碼可讀性更高。 位域聲明 位域名 : 位域長(zhǎng)度;
使用位域需要注意以下3點(diǎn): 1. 如果一個(gè)字節(jié)所剩空間不夠存放另一位域時(shí),應(yīng)從下一單元起存放該位域。也可以有意使某位域從下一單元開始。 2. 位域的長(zhǎng)度不能大于數(shù)據(jù)類型本身的長(zhǎng)度,比如int類型就不能超過32位二進(jìn)位。 3. 位域可以無位域名,這時(shí)它只用來作填充或調(diào)整位置。無名的位域是不能使用的。
上述代碼使用結(jié)構(gòu)體位域優(yōu)化之后。
@interface Person()
{
struct {
char handsome : 1; // 位域,代表占用一位空間
char rich : 1; // 按照順序只占一位空間
char tall : 1;
}_tallRichHandsome;
}
set、get方法中可以直接通過結(jié)構(gòu)體賦值和取值
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
return _tallRichHandsome.tall;
}
- (BOOL)rich
{
return _tallRichHandsome.rich;
}
- (BOOL)handsome
{
return _tallRichHandsome.handsome;
}
通過代碼驗(yàn)證一下是否可以賦值或取值正確
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.tall,person.rich,person.handsome);
}
return 0;
}
首先在log處打個(gè)斷點(diǎn),查看_tallRichHandsome內(nèi)存儲(chǔ)的值

因?yàn)?code>_tallRichHandsome占據(jù)一個(gè)內(nèi)存空間,也就是8個(gè)二進(jìn)制位,我們將05十六進(jìn)制轉(zhuǎn)化為二進(jìn)制查看

上圖中可以發(fā)現(xiàn),倒數(shù)第三位也就是tall值為1,倒數(shù)第二位也就是rich值為0,倒數(shù)一位也就是handsome值為1,如此看來和上述代碼中我們?cè)O(shè)置的值一樣??梢猿晒x值。
接著繼續(xù)打印內(nèi)容: Runtime - union探尋[59366:4053478] tall : -1, rich : 0, handsome : -1
此時(shí)可以發(fā)現(xiàn)問題,tall與handsome我們?cè)O(shè)值為YES,講道理應(yīng)該輸出的值為1為何上面輸出為-1呢?
并且上面通過打印_tallRichHandsome中存儲(chǔ)的值,也確認(rèn)tall和handsome的值都為1。我們?cè)俅未蛴?code>_tallRichHandsome結(jié)構(gòu)體內(nèi)變量的值。

上圖中可以發(fā)現(xiàn),handsome的值為0x01,通過計(jì)算器將其轉(zhuǎn)化為二進(jìn)制

可以看到值確實(shí)為1的,為什么打印出來值為-1呢?此時(shí)應(yīng)該可以想到應(yīng)該是get方法內(nèi)部有問題。我們來到get方法內(nèi)部通過打印斷點(diǎn)查看獲取到的值。
- (BOOL)handsome
{
BOOL ret = _tallRichHandsome.handsome;
return ret;
}
打印ret的值

通過打印ret的值發(fā)現(xiàn)其值為255,也就是1111 1111,此時(shí)也就能解釋為什么打印出來值為 -1了,首先此時(shí)通過結(jié)構(gòu)體獲取到的handsome的值為0b1只占一個(gè)內(nèi)存空間中的1位,但是BOOL值占據(jù)一個(gè)內(nèi)存空間,也就是8位。當(dāng)僅有1位的值擴(kuò)展成8位的話,其余空位就會(huì)根據(jù)前面一位的值全部補(bǔ)位成1,因此此時(shí)ret的值就被映射成了0b 11111 1111。
11111111 在一個(gè)字節(jié)時(shí),有符號(hào)數(shù)則為-1,無符號(hào)數(shù)則為255。因此我們?cè)诖蛴r(shí)候打印出的值為-1
為了驗(yàn)證當(dāng)1位的值擴(kuò)展成8位時(shí),會(huì)全部補(bǔ)位,我們將tall、rich、handsome值設(shè)置為占據(jù)兩位。
@interface Person()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
}
此時(shí)在打印就發(fā)現(xiàn)值可以正常打印出來。 Runtime - union探尋[60827:4259630] tall : 1, rich : 0, handsome : 1
這是因?yàn)?,在get方法內(nèi)部獲取到的_tallRichHandsome.handsome為兩位的也就是0b 01,此時(shí)在賦值給8位的BOOL類型的值時(shí),前面的空值就會(huì)自動(dòng)根據(jù)前面一位補(bǔ)全為0,因此返回的值為0b 0000 0001,因此打印出的值也就為1了。
因此上述問題同樣可以使用!!雙感嘆號(hào)來解決問題。!!的原理上面已經(jīng)講解過,這里不再贅述了。
使用結(jié)構(gòu)體位域優(yōu)化之后的代碼
@interface Person()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
}
@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)tall
{
return !!_tallRichHandsome.tall;
}
- (BOOL)rich
{
return !!_tallRichHandsome.rich;
}
- (BOOL)handsome
{
return !!_tallRichHandsome.handsome;
}
上述代碼中使用結(jié)構(gòu)體的位域則不在需要使用掩碼,使代碼可讀性增強(qiáng)了很多,但是效率相比直接使用位運(yùn)算的方式來說差很多,如果想要高效率的進(jìn)行數(shù)據(jù)的讀取與存儲(chǔ)同時(shí)又有較強(qiáng)的可讀性就需要使用到共用體了。
共用體
為了使代碼存儲(chǔ)數(shù)據(jù)高效率的同時(shí),有較強(qiáng)的可讀性,可以使用共用體來增強(qiáng)代碼可讀性,同時(shí)使用位運(yùn)算來提高數(shù)據(jù)存取的效率。
使用共用體優(yōu)化的代碼
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
@interface Person()
{
union {
char bits;
// 結(jié)構(gòu)體僅僅是為了增強(qiáng)代碼可讀性,無實(shí)質(zhì)用處
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
@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)tall
{
return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
上述代碼中使用位運(yùn)算這種比較高效的方式存取值,使用union共用體來對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)。增加讀取效率的同時(shí)增強(qiáng)代碼可讀性。
其中_tallRichHandsome共用體只占用一個(gè)字節(jié),因?yàn)榻Y(jié)構(gòu)體中tall、rich、handsome都只占一位二進(jìn)制空間,所以結(jié)構(gòu)體只占一個(gè)字節(jié),而char類型的bits也只占一個(gè)字節(jié),他們都在共用體中,因此共用一個(gè)字節(jié)的內(nèi)存即可。
并且在get、set方法中并沒有使用到結(jié)構(gòu)體,結(jié)構(gòu)體僅僅為了增加代碼可讀性,指明共用體中存儲(chǔ)了哪些值,以及這些值各占多少位空間。同時(shí)存值取值還使用位運(yùn)算來增加效率,存儲(chǔ)使用共用體,存放的位置依然通過與掩碼進(jìn)行位運(yùn)算來控制。
此時(shí)代碼已經(jīng)算是優(yōu)化完成了,高效的同時(shí)可讀性高,那么此時(shí)在回頭看isa_t共用體的源碼
isa_t源碼
此時(shí)我們?cè)诨仡^查看isa_t源碼
// 精簡(jiǎn)過的isa_t共用體
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# 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)
};
#endif
};
經(jīng)過上面對(duì)位運(yùn)算、位域以及共用體的分析,現(xiàn)在再來看源碼已經(jīng)可以很清晰的理解其中的內(nèi)容。源碼中通過共用體的形式存儲(chǔ)了64位的值,這些值在結(jié)構(gòu)體中被展示出來,通過對(duì)bits進(jìn)行位運(yùn)算而取出相應(yīng)位置的值。
這里主要關(guān)注一下shiftcls,shiftcls中存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址信息,我們之前在OC對(duì)象的本質(zhì)中提到過,對(duì)象的isa指針需要同ISA_MASK經(jīng)過一次&(按位與)運(yùn)算才能得出真正的Class對(duì)象地址。

那么此時(shí)我們重新來看ISA_MASK的值0x0000000ffffffff8ULL,我們將其轉(zhuǎn)化為二進(jìn)制數(shù)

上圖中可以看出ISA_MASK的值轉(zhuǎn)化為二進(jìn)制中有33位都為1,上面提到過按位與的作用是可以取出這33位中的值。那么此時(shí)很明顯了,同ISA_MASK進(jìn)行按位與運(yùn)算即可以取出Class或Meta-Class的值。
同時(shí)可以看出ISA_MASK最后三位的值為0,那么任何數(shù)同ISA_MASK按位與運(yùn)算之后,得到的最后三位必定都為0,因此任何類對(duì)象或元類對(duì)象的內(nèi)存地址最后三位必定為0,轉(zhuǎn)化為十六進(jìn)制末位必定為8或者0。
isa中存儲(chǔ)的信息及作用
將結(jié)構(gòu)體取出來標(biāo)記一下這些信息的作用。
struct {
// 0代表普通的指針,存儲(chǔ)著Class,Meta-Class對(duì)象的內(nèi)存地址。
// 1代表優(yōu)化后的使用位域存儲(chǔ)更多的信息。
uintptr_t nonpointer : 1;
// 是否有設(shè)置過關(guān)聯(lián)對(duì)象,如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_assoc : 1;
// 是否有C++析構(gòu)函數(shù),如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_cxx_dtor : 1;
// 存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址信息
uintptr_t shiftcls : 33;
// 用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化
uintptr_t magic : 6;
// 是否有被弱引用指向過。
uintptr_t weakly_referenced : 1;
// 對(duì)象是否正在釋放
uintptr_t deallocating : 1;
// 引用計(jì)數(shù)器是否過大無法存儲(chǔ)在isa中
// 如果為1,那么引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫SideTable的類的屬性中
uintptr_t has_sidetable_rc : 1;
// 里面存儲(chǔ)的值是引用計(jì)數(shù)器減1
uintptr_t extra_rc : 19;
};
驗(yàn)證
通過下面一段代碼驗(yàn)證上述信息存儲(chǔ)的位置及作用
// 以下代碼需要在真機(jī)中運(yùn)行,因?yàn)檎鏅C(jī)中才是__arm64__ 位架構(gòu)
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
NSLog(@"%@",person);
}
首先打印person類對(duì)象的地址,之后通過斷點(diǎn)打印一下person對(duì)象的isa指針地址。
首先來看一下打印的內(nèi)容

將類對(duì)象地址轉(zhuǎn)化為二進(jìn)制

將person的isa指針地址轉(zhuǎn)化為二進(jìn)制

shiftcls : shiftcls中存儲(chǔ)類對(duì)象地址,通過上面兩張圖對(duì)比可以發(fā)現(xiàn)存儲(chǔ)類對(duì)象地址的33位二進(jìn)制內(nèi)容完全相同。
extra_rc : extra_rc的19位中存儲(chǔ)著的值為引用計(jì)數(shù)減一,因?yàn)榇藭r(shí)person的引用計(jì)數(shù)為1,因此此時(shí)extra_rc的19位二進(jìn)制中存儲(chǔ)的是0。
magic : magic的6位用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化,上述代碼中person已經(jīng)完成初始化,那么此時(shí)這6位二進(jìn)制中存儲(chǔ)的值011010即為共用體中定義的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL的值。
nonpointer : 這里肯定是使用的優(yōu)化后的isa,因此nonpointer的值肯定為1
因?yàn)榇藭r(shí)person對(duì)象沒有關(guān)聯(lián)對(duì)象并且沒有弱指針引用過,可以看出has_assoc和weakly_referenced值都為0,接著我們?yōu)閜erson對(duì)象添加弱引用和關(guān)聯(lián)對(duì)象,來觀察一下has_assoc和weakly_referenced的變化。
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
// 為person添加弱引用
__weak Person *weakPerson = person;
// 為person添加關(guān)聯(lián)對(duì)象
objc_setAssociatedObject(person, @"name", @"xx_cc", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"%@",person);
}
重新打印person的isa指針地址將其轉(zhuǎn)化為二進(jìn)制可以看到has_assoc和weakly_referenced的值都變成了1

注意:只要設(shè)置過關(guān)聯(lián)對(duì)象或者弱引用引用過對(duì)象has_assoc和weakly_referenced的值就會(huì)變成1,不論之后是否將關(guān)聯(lián)對(duì)象置為nil或斷開弱引用。
如果沒有設(shè)置過關(guān)聯(lián)對(duì)象,對(duì)象釋放時(shí)會(huì)更快,這是因?yàn)閷?duì)象在銷毀時(shí)會(huì)判斷是否有關(guān)聯(lián)對(duì)象進(jìn)而對(duì)關(guān)聯(lián)對(duì)象釋放。來看一下對(duì)象銷毀的源碼
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa = obj->getIsa();
// 是否有c++析構(gòu)函數(shù)
if (isa->hasCxxDtor()) {
object_cxxDestruct(obj);
}
// 是否有關(guān)聯(lián)對(duì)象,如果有則移除
if (isa->instancesHaveAssociatedObjects()) {
_object_remove_assocations(obj);
}
objc_clear_deallocating(obj);
}
return obj;
}
相信至此我們已經(jīng)對(duì)isa指針有了新的認(rèn)識(shí),__arm64__架構(gòu)之后,isa指針不單單只存儲(chǔ)了Class或Meta-Class的地址,而是使用共用體的方式存儲(chǔ)了更多信息,其中shiftcls存儲(chǔ)了Class或Meta-Class的地址,需要同ISA_MASK進(jìn)行按位&運(yùn)算才可以取出其內(nèi)存地址值。
Runtime 博文推薦
Runtime圖解
Runtime 10種實(shí)際用法
完整總結(jié)
objc_msgSend
詳解
快速上手
消息機(jī)制
Method Swizzling開發(fā)實(shí)例匯總
最實(shí)用的runtime總結(jié)
實(shí)際開發(fā)中的應(yīng)用