isa的本質(zhì)
在學(xué)習(xí)Runtime之前首先需要對isa的本質(zhì)有一定的了解斋日,這樣之后學(xué)習(xí)Runtime會更便于理解型凳。
回顧OC對象的本質(zhì),每個OC對象都含有一個isa指針碍拆,__arm64__
之前逸寓,isa僅僅是一個指針居兆,保存著對象或類對象內(nèi)存地址,在__arm64__
架構(gòu)之后竹伸,apple對isa進(jìn)行了優(yōu)化泥栖,變成了一個共用體(union)結(jié)構(gòu),同時使用位域來存儲更多的信息。
我們知道OC對象的isa指針并不是直接指向類對象或者元類對象吧享,而是需要&ISA_MASK
通過位運(yùn)算才能獲取到類對象或者元類對象的地址魏割。今天來探尋一下為什么需要&ISA_MASK
才能獲取到類對象或者元類對象的地址,以及這樣的好處钢颂。
首先在源碼中找到isa指針钞它,看一下isa指針的本質(zhì)。
// 截取objc_object內(nèi)部分代碼
struct objc_object {
private:
isa_t isa;
}
isa指針其實是一個isa_t類型的共用體甸陌,來到isa_t內(nèi)部查看其結(jié)構(gòu)
// 精簡過的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
表示共用體
盐股∏恚可以看到共用體中有一個結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部分別定義了一些變量疯汁,變量后面的值代表的是該變量占用多少個字節(jié)牲尺,也就是位域
技術(shù)。
共用體:在進(jìn)行某些算法的C語言編程的時候幌蚊,需要使幾種不同類型的變量存放到同一段內(nèi)存單元中谤碳。也就是使用覆蓋技術(shù),幾個變量互相覆蓋溢豆。這種幾個不同的變量共同占用一段內(nèi)存的結(jié)構(gòu)蜒简,在C語言中,被稱作“共用體”類型結(jié)構(gòu)漩仙,簡稱共用體搓茬。
接下來使用共用體的方式來深入的了解apple為什么要使用共用體,以及使用共用體的好處队他。
探尋過程
接下來使用代碼來模仿底層的做法卷仑,創(chuàng)建一個person類并含有三個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
個BOOL類型的屬性麸折,打印Person類對象占據(jù)內(nèi)存空間為16
锡凝,也就是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13
。因為內(nèi)存對齊原則所以Person類對象占據(jù)內(nèi)存空間為16垢啼。
上面提到過共用體中變量可以相互覆蓋窜锯,可以使幾個不同的變量存放到同一段內(nèi)存單元中,可以很大程度上節(jié)省內(nèi)存空間芭析。
那么我們知道BOOL值只有兩種情況 0 或者 1衬浑,但是卻占據(jù)了一個字節(jié)的內(nèi)存空間,而一個內(nèi)存空間中有8個二進(jìn)制位放刨,并且二進(jìn)制只有 0 或者 1 工秩。那么是否可以使用1個二進(jìn)制位來表示一個BOOL值,也就是說3個BOOL值最終只使用3個二進(jìn)制位,也就是一個內(nèi)存空間即可呢助币?如何實現(xiàn)這種方式浪听?
首先如果使用這種方式需要自己寫方法聲明與實現(xiàn),不可以寫屬性眉菱,因為一旦寫屬性迹栓,系統(tǒng)會自動幫我們添加成員變量。
另外想要將三個BOOL值存放在一個字節(jié)中俭缓,我們可以添加一個char
類型的成員變量克伊,char
類型占據(jù)一個字節(jié)內(nèi)存空間,也就是8個二進(jìn)制位华坦≡复担可以使用其中最后三個二進(jìn)制位來存儲3個BOOL值。
@interface Person()
{
char _tallRichHandsome;
}
例如_tallRichHansome的值為 0b 0000 0010
惜姐,那么只使用8個二進(jìn)制位中的最后3個犁跪,分別為其賦值0或者1來代表tall、rich歹袁、handsome
的值坷衍。如下圖所示
那么現(xiàn)在面臨的問題就是如何取出8個二進(jìn)制位中的某一位的值,或者為某一位賦值呢条舔?
取值
首先來看一下取值枫耳,假如char類型的成員變量中存儲的二進(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)行按位與計算坯钦,即可取出特定的位预皇。
那么此時可以將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);
}
上述代碼中使用兩個!!(非)
來將值改為bool類型。同樣使用上面的例子
// 取出倒數(shù)第二位 rich
0000 0010 // _tallRichHandsome
& 0000 0010 // RichMask
------------
0000 0010 // 取出rich的值為1婉刀,其他位都置為0
上述代碼中(_tallRichHandsome & TallMask)
的值為0000 0010
也就是2吟温,但是我們需要的是一個BOOL類型的值 0 或者 1 ,那么!!2
就將 2 先轉(zhuǎn)化為 0 突颊,之后又轉(zhuǎn)化為 1鲁豪。相反如果按位與取得的值為 0 時潘悼,!!0
將 0 先轉(zhuǎn)化為 1 之后又轉(zhuǎn)化為 0。 因此使用!!
兩個非操作將值轉(zhuǎn)化為 0 或者 1 來表示相應(yīng)的值爬橡。
掩碼 : 上述代碼中定義了三個宏治唤,用來分別進(jìn)行按位與運(yùn)算而取出相應(yīng)的值,一般用來按位與(&)運(yùn)算的值稱之為掩碼糙申。
為了能更清晰的表明掩碼是為了取出哪一位的值宾添,上述三個宏的定義可以使用<<(左移)
來優(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缕陕,可以使用|(按位或)
操作符。 | : 按位或疙挺,只要有一個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险掀,其他位值都不變
此時set方法內(nèi)部實現(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)需要添加新屬性的時候埠啃,需要重復(fù)上述工作,并且代碼可讀性比較差伟恶。接下來使用結(jié)構(gòu)體的位域特性來優(yōu)化上述代碼碴开。
位域
將上述代碼進(jìn)行優(yōu)化,使用結(jié)構(gòu)體位域博秫,可以使代碼可讀性更高潦牛。 位域聲明 位域名 : 位域長度;
使用位域需要注意以下3點: 1. 如果一個字節(jié)所剩空間不夠存放另一位域時挡育,應(yīng)從下一單元起存放該位域巴碗。也可以有意使某位域從下一單元開始。 2. 位域的長度不能大于數(shù)據(jù)類型本身的長度即寒,比如int類型就不能超過32位二進(jìn)位橡淆。 3. 位域可以無位域名召噩,這時它只用來作填充或調(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;
}
通過代碼驗證一下是否可以賦值或取值正確
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處打個斷點抵蚊,查看_tallRichHandsome內(nèi)存儲的值
因為_tallRichHandsome
占據(jù)一個內(nèi)存空間,也就是8個二進(jìn)制位溯革,我們將05十六進(jìn)制轉(zhuǎn)化為二進(jìn)制查看
上圖中可以發(fā)現(xiàn)贞绳,倒數(shù)第三位也就是tall值為1,倒數(shù)第二位也就是rich值為0致稀,倒數(shù)一位也就是handsome值為1冈闭,如此看來和上述代碼中我們設(shè)置的值一樣《兜ィ可以成功賦值萎攒。
接著繼續(xù)打印內(nèi)容: Runtime - union探尋[59366:4053478] tall : -1, rich : 0, handsome : -1
此時可以發(fā)現(xiàn)問題,tall與handsome我們設(shè)值為YES矛绘,講道理應(yīng)該輸出的值為1為何上面輸出為-1呢耍休?
并且上面通過打印_tallRichHandsome
中存儲的值,也確認(rèn)tall
和handsome
的值都為1货矮。我們再次打印_tallRichHandsome
結(jié)構(gòu)體內(nèi)變量的值羊精。
上圖中可以發(fā)現(xiàn),handsome的值為0x01囚玫,通過計算器將其轉(zhuǎn)化為二進(jìn)制
可以看到值確實為1的喧锦,為什么打印出來值為-1呢?此時應(yīng)該可以想到應(yīng)該是get方法內(nèi)部有問題抓督。我們來到get方法內(nèi)部通過打印斷點查看獲取到的值燃少。
- (BOOL)handsome
{
BOOL ret = _tallRichHandsome.handsome;
return ret;
}
打印ret的值
通過打印ret的值發(fā)現(xiàn)其值為255,也就是1111 1111
铃在,此時也就能解釋為什么打印出來值為 -1了阵具,首先此時通過結(jié)構(gòu)體獲取到的handsome
的值為0b1
只占一個內(nèi)存空間中的1位,但是BOOL值占據(jù)一個內(nèi)存空間涌穆,也就是8位怔昨。當(dāng)僅有1位的值擴(kuò)展成8位的話,其余空位就會根據(jù)前面一位的值全部補(bǔ)位成1宿稀,因此此時ret的值就被映射成了0b 11111 1111
趁舀。
11111111
在一個字節(jié)時,有符號數(shù)則為-1祝沸,無符號數(shù)則為255矮烹。因此我們在打印時候打印出的值為-1
為了驗證當(dāng)1位的值擴(kuò)展成8位時越庇,會全部補(bǔ)位,我們將tall奉狈、rich卤唉、handsome值設(shè)置為占據(jù)兩位。
@interface Person()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
}
此時在打印就發(fā)現(xiàn)值可以正常打印出來仁期。 Runtime - union探尋[60827:4259630] tall : 1, rich : 0, handsome : 1
這是因為桑驱,在get方法內(nèi)部獲取到的_tallRichHandsome.handsome
為兩位的也就是0b 01
,此時在賦值給8位的BOOL類型的值時跛蛋,前面的空值就會自動根據(jù)前面一位補(bǔ)全為0熬的,因此返回的值為0b 0000 0001
,因此打印出的值也就為1了赊级。
因此上述問題同樣可以使用!!
雙感嘆號來解決問題押框。!!
的原理上面已經(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ù)的讀取與存儲同時又有較強(qiáng)的可讀性就需要使用到共用體了兑徘。
共用體
為了使代碼存儲數(shù)據(jù)高效率的同時,有較強(qiáng)的可讀性墨微,可以使用共用體來增強(qiáng)代碼可讀性道媚,同時使用位運(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)代碼可讀性翘县,無實質(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共用體來對數(shù)據(jù)進(jìn)行存儲谴分。增加讀取效率的同時增強(qiáng)代碼可讀性锈麸。
其中_tallRichHandsome
共用體只占用一個字節(jié),因為結(jié)構(gòu)體中tall牺蹄、rich忘伞、handsome都只占一位二進(jìn)制空間,所以結(jié)構(gòu)體只占一個字節(jié)沙兰,而char類型的bits也只占一個字節(jié)氓奈,他們都在共用體中,因此共用一個字節(jié)的內(nèi)存即可鼎天。
并且在get舀奶、set
方法中并沒有使用到結(jié)構(gòu)體,結(jié)構(gòu)體僅僅為了增加代碼可讀性斋射,指明共用體中存儲了哪些值育勺,以及這些值各占多少位空間但荤。同時存值取值還使用位運(yùn)算來增加效率,存儲使用共用體涧至,存放的位置依然通過與掩碼進(jìn)行位運(yùn)算來控制腹躁。
此時代碼已經(jīng)算是優(yōu)化完成了,高效的同時可讀性高南蓬,那么此時在回頭看isa_t
共用體的源碼
isa_t源碼
此時我們在回頭查看isa_t源碼
// 精簡過的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)過上面對位運(yùn)算纺非、位域以及共用體的分析,現(xiàn)在再來看源碼已經(jīng)可以很清晰的理解其中的內(nèi)容赘方。源碼中通過共用體的形式存儲了64位的值铐炫,這些值在結(jié)構(gòu)體中被展示出來,通過對bits
進(jìn)行位運(yùn)算而取出相應(yīng)位置的值蒜焊。
這里主要關(guān)注一下shiftcls
倒信,shiftcls
中存儲著Class、Meta-Class
對象的內(nèi)存地址信息泳梆,我們之前在OC對象的本質(zhì)中提到過鳖悠,對象的isa指針需要同ISA_MASK
經(jīng)過一次&(按位與)運(yùn)算才能得出真正的Class對象地址。
那么此時我們重新來看ISA_MASK
的值0x0000000ffffffff8ULL
优妙,我們將其轉(zhuǎn)化為二進(jìn)制數(shù)
上圖中可以看出ISA_MASK
的值轉(zhuǎn)化為二進(jìn)制中有33位都為1乘综,上面提到過按位與的作用是可以取出這33位中的值。那么此時很明顯了套硼,同ISA_MASK
進(jìn)行按位與運(yùn)算即可以取出Class或Meta-Class的值卡辰。
同時可以看出ISA_MASK
最后三位的值為0,那么任何數(shù)同ISA_MASK
按位與運(yùn)算之后邪意,得到的最后三位必定都為0九妈,因此任何類對象或元類對象的內(nèi)存地址最后三位必定為0,轉(zhuǎn)化為十六進(jìn)制末位必定為8或者0雾鬼。
isa中存儲的信息及作用
將結(jié)構(gòu)體取出來標(biāo)記一下這些信息的作用萌朱。
struct {
// 0代表普通的指針,存儲著Class策菜,Meta-Class對象的內(nèi)存地址晶疼。
// 1代表優(yōu)化后的使用位域存儲更多的信息。
uintptr_t nonpointer : 1;
// 是否有設(shè)置過關(guān)聯(lián)對象又憨,如果沒有翠霍,釋放時會更快
uintptr_t has_assoc : 1;
// 是否有C++析構(gòu)函數(shù),如果沒有蠢莺,釋放時會更快
uintptr_t has_cxx_dtor : 1;
// 存儲著Class寒匙、Meta-Class對象的內(nèi)存地址信息
uintptr_t shiftcls : 33;
// 用于在調(diào)試時分辨對象是否未完成初始化
uintptr_t magic : 6;
// 是否有被弱引用指向過。
uintptr_t weakly_referenced : 1;
// 對象是否正在釋放
uintptr_t deallocating : 1;
// 引用計數(shù)器是否過大無法存儲在isa中
// 如果為1浪秘,那么引用計數(shù)會存儲在一個叫SideTable的類的屬性中
uintptr_t has_sidetable_rc : 1;
// 里面存儲的值是引用計數(shù)器減1
uintptr_t extra_rc : 19;
};
驗證
通過下面一段代碼驗證上述信息存儲的位置及作用
// 以下代碼需要在真機(jī)中運(yùn)行蒋情,因為真機(jī)中才是__arm64__ 位架構(gòu)
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
NSLog(@"%@",person);
}
首先打印person類對象的地址埠况,之后通過斷點打印一下person對象的isa指針地址。
首先來看一下打印的內(nèi)容
將類對象地址轉(zhuǎn)化為二進(jìn)制
將person的isa指針地址轉(zhuǎn)化為二進(jìn)制
shiftcls : shiftcls
中存儲類對象地址棵癣,通過上面兩張圖對比可以發(fā)現(xiàn)存儲類對象地址的33位二進(jìn)制內(nèi)容完全相同辕翰。
extra_rc : extra_rc
的19位中存儲著的值為引用計數(shù)減一,因為此時person的引用計數(shù)為1狈谊,因此此時extra_rc
的19位二進(jìn)制中存儲的是0喜命。
magic : magic
的6位用于在調(diào)試時分辨對象是否未完成初始化,上述代碼中person已經(jīng)完成初始化河劝,那么此時這6位二進(jìn)制中存儲的值011010
即為共用體中定義的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL
的值壁榕。
nonpointer : 這里肯定是使用的優(yōu)化后的isa,因此nonpointer
的值肯定為1
因為此時person對象沒有關(guān)聯(lián)對象并且沒有弱指針引用過赎瞎,可以看出has_assoc
和weakly_referenced
值都為0牌里,接著我們?yōu)閜erson對象添加弱引用和關(guān)聯(lián)對象,來觀察一下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)對象
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)對象或者弱引用引用過對象has_assoc
和weakly_referenced
的值就會變成1牡辽,不論之后是否將關(guān)聯(lián)對象置為nil或斷開弱引用。
如果沒有設(shè)置過關(guān)聯(lián)對象敞临,對象釋放時會更快态辛,這是因為對象在銷毀時會判斷是否有關(guān)聯(lián)對象進(jìn)而對關(guān)聯(lián)對象釋放。來看一下對象銷毀的源碼
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)對象挺尿,如果有則移除
if (isa->instancesHaveAssociatedObjects()) {
_object_remove_assocations(obj);
}
objc_clear_deallocating(obj);
}
return obj;
}
相信至此我們已經(jīng)對isa指針有了新的認(rèn)識奏黑,__arm64__
架構(gòu)之后,isa指針不單單只存儲了Class或Meta-Class的地址编矾,而是使用共用體的方式存儲了更多信息熟史,其中shiftcls
存儲了Class或Meta-Class的地址,需要同ISA_MASK
進(jìn)行按位&運(yùn)算才可以取出其內(nèi)存地址值洽沟。
Runtime 博文推薦
Runtime圖解
Runtime 10種實際用法
完整總結(jié)
objc_msgSend
詳解
快速上手
消息機(jī)制
Method Swizzling開發(fā)實例匯總
最實用的runtime總結(jié)
實際開發(fā)中的應(yīng)用