Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

????本文篇幅比較長(zhǎng)哟绊,創(chuàng)作的目的并不是為了在簡(jiǎn)書上刷贊和閱讀量椿胯,而是為了自己日后溫習(xí)知識(shí)所用硼砰。如果有幸被你發(fā)現(xiàn)這篇文章,并且引起了你的閱讀興趣顶掉,請(qǐng)休息充分,靜下心來,精力充足地開始閱讀幽邓,希望這篇文章能對(duì)你有所幫助拐揭。如發(fā)現(xiàn)任何有誤之處撤蟆,肯請(qǐng)留言糾正,謝謝堂污。????

如何理解Objective-C的動(dòng)態(tài)特性家肯?

很多靜態(tài)編程語言,編寫完代碼后盟猖,經(jīng)過編譯連接生成可執(zhí)行文件讨衣,最后就可以在電腦上運(yùn)行起來。

以C語言為例

void test() {
    printf("Hello World");
}
int main() {
    test();
}

以上代碼經(jīng)過編譯之后式镐,main函數(shù)里面就一定會(huì)調(diào)用test()反镇,而test()的實(shí)現(xiàn)也一定會(huì)是和代碼中寫的一樣,這些在編譯完成那一刻就決定了娘汞,運(yùn)行過程中不會(huì)發(fā)生改變的歹茶。C可以說就是典型的靜態(tài)語言。

與之相比,Objective-C就可以在運(yùn)行階段修改之前編譯階段確定好的一些函數(shù)和方法辆亏。

************************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        [person test];
    }
    return 0;
}

***********************CLPerson.h************************
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
- (void)test;
@end


***********************CLPerson.m************************
#import "CLPerson.h"
@implementation CLPerson
- (void)test {
    NSLog(@"%s", __func__);
}

- (void)abc {
    
}
@end

如上面所示代碼风秤,[person test];這句代碼,在運(yùn)行階段扮叨,可以調(diào)用CLPersontest方法缤弦,也可以通過OC的動(dòng)態(tài)特性,使其最終調(diào)用別的方法彻磁,例如abc方法碍沐,甚至,還可以調(diào)用另外一個(gè)類的方法衷蜓。除此之外累提,OC還可以在程序運(yùn)行階段,給類增加方法等磁浇,這就是所謂的動(dòng)態(tài)特性斋陪。

Runtime簡(jiǎn)介

  • Objective-C是一門動(dòng)態(tài)性比較強(qiáng)的編程語言,根C置吓、C++等語言有很大不同
  • Objective-C的動(dòng)態(tài)性是由Runtime API來支撐的
  • Runtime API提供的接口基本都是C語言的无虚,源碼由C/C++/匯編語言編寫

isa詳解

深入Runtime之前,先要解決一個(gè)比較重要的概念——isa衍锚。在早期的Runtime里面友题,isa指針直接指向class/meta-class對(duì)象的地址,isa就是一個(gè)普通的指針戴质。

后來度宦,蘋果從ARM64位架構(gòu)開始,對(duì)isa進(jìn)行了優(yōu)化告匠,將其定義成一個(gè)共用體(union)結(jié)構(gòu)戈抄,結(jié)合 位域 的概念以及 位運(yùn)算 的方式來存儲(chǔ)更多類相關(guān)信息。isa指針需要通過與一個(gè)叫ISA_MASK的值(掩碼)進(jìn)行二進(jìn)制&運(yùn)算后专,才能得到真實(shí)的class/meta-class對(duì)象的地址呛凶。接下來,就具體探究一下蘋果究竟是怎么優(yōu)化的行贪。

首先從源碼角度漾稀,對(duì)比一下變化isa優(yōu)化前后的變化

***************************************
typedef struct objc_class *Class;

typedef struct objc_object {
    Class isa;
} *id;

上面是64位之前,objc_object的定義如上建瘫,isa直接指向objc_class崭捍。

再看看優(yōu)化后objc_object的定義

struct objc_object {
private:
    isa_t isa;

public:

arm64開始,isa的類型變成了isa_t啰脚,這是什么鬼殷蛇?這個(gè)就是接下來討論的重點(diǎn)实夹,先看一下它的源碼

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

// SUPPORT_PACKED_ISA
#endif


#if SUPPORT_INDEXED_ISA

# if  __ARM_ARCH_7K__ >= 2

#   define ISA_INDEX_IS_NPI      1
#   define ISA_INDEX_MASK        0x0001FFFC
#   define ISA_INDEX_SHIFT       2
#   define ISA_INDEX_BITS        15
#   define ISA_INDEX_COUNT       (1 << ISA_INDEX_BITS)
#   define ISA_INDEX_MAGIC_MASK  0x001E0001
#   define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
#       define RC_ONE   (1ULL<<25)
#       define RC_HALF  (1ULL<<6)
    };

# else
#   error unknown architecture for indexed isa
# endif

// SUPPORT_INDEXED_ISA
#endif

};

上面的代碼就是蘋果對(duì)于isa優(yōu)化的精華所在,為了看懂上面的代碼粒梦,首先需要從一些基礎(chǔ)知識(shí)開始說亮航。

場(chǎng)景需求分析

首先定義一個(gè)類CLPerson,首先給CLPerson增加幾個(gè)屬性以及成員變量

@interface CLPerson : NSObject
{
    BOOL _tall;
    BOOL _rich;
    BOOL _handsome;
}
@property (nonatomic, assign, getter=isRich) BOOL rich;
@property (nonatomic, assign, getter=isTall) BOOL tall;
@property (nonatomic, assign, getter=isHandsome) BOOL handsome;

對(duì)于它們的使用匀们,無需多說缴淋,如下

#import <Foundation/Foundation.h>
#import "CLPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        person.rich = YES;
        person.tall = NO;
        person.handsome = YES;


        NSLog(@"%zu", class_getInstanceSize([CLPerson class]));
    }
    return 0;
}

通過runtime,我們可以查看到CLPerson類對(duì)象的內(nèi)存占用情況

2019-07-16 13:15:04.083828+0800 OC底層Runtime[2509:80387] 16
Program ended with exit code: 0

通過我之前對(duì)與對(duì)象內(nèi)存布局的分析的文章泄朴,這里可以得出如下結(jié)論:

  • isa占用了8個(gè)字節(jié)
  • _rich重抖、_tall_handsome這三個(gè)成員變量個(gè)占用1個(gè)字節(jié)
  • 因?yàn)橛袃?nèi)存對(duì)齊和bucketSized的因素祖灰,所以類對(duì)象占用16個(gè)字節(jié)的內(nèi)存空間钟沛。

??????但是_rich_tall局扶、_handsome實(shí)際上只可能有2個(gè)值恨统,YES/NO,也就是0和1三妈,它們完全可以用一個(gè)二進(jìn)制位來表示延欠,三個(gè)加在一起也就只需要占用3個(gè)二進(jìn)制位,連半個(gè)字節(jié)都用不了沈跨。有什么方法可以實(shí)現(xiàn)這種節(jié)約內(nèi)存的需求呢???????

如果直接用屬性的話兔综,肯定就會(huì)自動(dòng)生成帶下劃線的成員變量饿凛,這樣就無法精簡(jiǎn)內(nèi)存。所以需要手動(dòng)實(shí)現(xiàn)getter/setter方法以替代屬性软驰。

#import <Foundation/Foundation.h>
@interface CLPerson : NSObject

- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;

- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end

然后在.m文件里面涧窒,用一個(gè)char _tallRichHandsome;(一個(gè)字節(jié))來存儲(chǔ)tall/rich/handsome的信息。

#import "CLPerson.h"
@interface CLPerson()
{
    char _tallRichHandsome; // 0b 0000 0000
}

@end

@implementation CLPerson

- (void)setTall:(BOOL)tall {
    
}
- (void)setRich:(BOOL)rich {
    
}
- (void)setHandsome:(BOOL)handsome {
    
}

- (BOOL)isTall {
   
}
- (BOOL)isRich {
   
}
- (BOOL)isHandsome {
    
}

@end

如果我想利用_tallRichHandsome的后三位來分別存放tall锭亏、rich纠吴、handsome這三個(gè)信息,有什么方法可以辦到呢慧瘤?

取值

首先我們來解決getter方法戴已,也就是取值問題。如何從特定的位里面取出值呢锅减?沒錯(cuò)糖儡,——&(按位與運(yùn)算)

假設(shè)我們規(guī)定

  • tall_tallRichHandsome的右起第1位表示怔匣,
  • rich_tallRichHandsome的右起第2位表示握联,
  • handsome_tallRichHandsome的右起第3位表示,
  • 并且tall=YESrich=NO金闽, handsome=YES纯露,
    那么_tallRichHandsome的值應(yīng)該是 0000 0101
tall (YES) rich (NO) handsome (YES)
_tallRichHandsome 0000 0101 0000 0101 0000 0101
mask碼(用來取值) &0000 0001 &0000 0010 &0000 0100
通過&運(yùn)算得到結(jié)果 0000 0001 0000 0000 0000 0100

根據(jù)&運(yùn)算的特點(diǎn),想要取出特定位上面的值代芜,只需將mask碼中對(duì)應(yīng)位設(shè)置為1埠褪,因?yàn)?原來值 & 1 = 原來值,將mask碼中其他位的設(shè)置為0蜒犯,就可以屏蔽出特定位之外其余位上面的值组橄,因?yàn)?原來值 & 0 = 0,這個(gè)應(yīng)該很好理解罚随。至于取出來的值如何轉(zhuǎn)化成我們所需要的值(在這里我們需要的是YES/NO)蝌借,就有很多辦法了。好了怠益,現(xiàn)在去代碼里面實(shí)現(xiàn)一下霜幼。如下所示

*************************main.m*****************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        NSLog(@"tall-%d, rich-%d, handsome%d", person.isTall, person.isRich, person.isHandsome);
    }
    return 0;
}

*************************CLPerson.m*****************************

#import "CLPerson.h"
@interface CLPerson()
{
    char _tallRichHandsome;
}

@end

@implementation CLPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        _tallRichHandsome = 0b00000101;//設(shè)定一個(gè)初值
    }
    return self;
}


/*
 mask碼
 tall的mask碼:二進(jìn)制 0b 00000001 ---> 十進(jìn)制 1
 rich的mask碼:二進(jìn)制 0b 00000010 ---> 十進(jìn)制 2
 handsome的mask碼:二進(jìn)制 0b 00000100 ---> 十進(jìn)制 4
 */

- (BOOL)isTall {
    return !!(_tallRichHandsome & 1);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & 2);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & 4);
}
@end

**************************運(yùn)行結(jié)果**************************
2019-07-16 17:54:32.915636+0800 OC底層Runtime[2828:156639] 
tall = 1, 
rich = 0, 
handsome = 1
Program ended with exit code: 0

上面的解決方案里面,我是通過!!(_tallRichHandsome & mask值);來轉(zhuǎn)換成BOOL值的潮改,因?yàn)?code>_tallRichHandsome & mask值得出的結(jié)果狭郑,要么是0,要么是一個(gè)大于0的整數(shù)汇在,因此通過兩次!運(yùn)算翰萨,可以得到對(duì)應(yīng)的BOOL值,0對(duì)應(yīng)NO糕殉,大于0的數(shù)對(duì)應(yīng)YES亩鬼。

mask碼的值可以用二進(jìn)制表示,也可以用十進(jìn)制表示阿蝶,但是在具體的使用中雳锋,需要大量注釋代碼說明mask碼所代表的含義,因此更好的處理方法羡洁,可以將它們定義為宏玷过,通過宏的名字來表述所需要的含義。改寫如下:

#define CLTallMask 1
#define CLRichMask 2
#define CLHandsomeMask 4

- (BOOL)isTall {
    return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & CLHandsomeMask);
}

但是還有一個(gè)問題筑煮,從宏定義里面辛蚊,我們不容易看出到底mask碼是要取出哪一位的值,所以真仲,改成二進(jìn)制表示更好嚼隘,如下

#define CLTallMask 0b00000001
#define CLRichMask 0b00000010
#define CLHandsomeMask 0b00000100

但是仍然不完美,做開發(fā)的哪個(gè)沒有點(diǎn)強(qiáng)迫癥袒餐,寫這么一大串二進(jìn)制飞蛹,太麻煩了谤狡,所以我們有更犀利的方法,沒錯(cuò)卧檐,通過位移運(yùn)算符來表示墓懂,如下

#define CLTallMask (1 << 0) 
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)

1代表0b00000001,也就是二進(jìn)制的1霉囚,1 << 0表示左移0位捕仔,也就是不移動(dòng),那么就代表去右邊最低位上的值盈罐,同理榜跌,1 << 11<< 2就分別表示取右起第二位和第三位上的值盅粪,這樣就清晰易懂了钓葫。

為什么叫MASK
剛接觸編程的時(shí)候票顾,我曾經(jīng)很困惑础浮,用來獲取特定位上的內(nèi)容的這一串二進(jìn)制碼為什么在英文里叫mask,這個(gè)mask為什么要翻譯成掩碼?不知道大家有沒有困惑過奠骄。后來想著想著豆同,突然開竅了了,這個(gè)mask是用來拿到特定位上的值含鳞,也就是查看你想要看到的部位影锈。mask這個(gè)單詞的含義里面有 面具 的意思,面具總知道吧蝉绷,就下面這個(gè)

面具上的幾個(gè)洞鸭廷,分別是眼睛和嘴,因?yàn)槟闳⒓用婢遬arty的時(shí)候潜必,只想讓人看見眼鏡和嘴巴,其他地方都遮掩起來沃但。我們?cè)谔囟ㄎ簧厦娴娜≈荡殴觯皇歉@個(gè)一樣嗎,因此老外給這個(gè)東西取名叫mask碼宵晚,其實(shí)就是為了形象生動(dòng)垂攘,根本不是啥高大上的東西。只不過中文翻譯我個(gè)人覺得太生硬了淤刃,翻譯成 面具碼 豈不是更好晒他。小感慨一下,英文技術(shù)文檔里面有挺多這種翻譯過來很奇怪的名詞逸贾,其實(shí)就是文化差異陨仅,老外從他們的文化角度去給一些概念進(jìn)行了生動(dòng)形象的命名津滞,但到了我們這邊的確是翻譯的慘不忍睹,簡(jiǎn)直就是量產(chǎn)羅玉鳳白粕恕4バ臁!所以學(xué)好英文還是很重要的狐赡,有些翻譯真是害死人撞鹉。




設(shè)值

接下來看一看如何把外部設(shè)定的值保存到對(duì)應(yīng)的位上面去,而且不能影響到其他位上面的值颖侄。
(1)設(shè)值為1鸟雏。正好按位或運(yùn)算(|)就能實(shí)現(xiàn)這里的要求。來回顧一下或運(yùn)算的規(guī)則

  • 0 | 0 = 0
  • 0 | 1 = 1
  • 1 | 0 = 1
  • 1 | 1 = 1

因此根據(jù)上面的特點(diǎn)览祖,跟mask碼進(jìn)行或運(yùn)算()就可以將特定值設(shè)置到目標(biāo)位中孝鹊。因?yàn)?code>mask碼中,對(duì)應(yīng)目標(biāo)位的就是1穴墅,對(duì)應(yīng)非目標(biāo)位的就是0惶室。

(2)設(shè)值為0。上面還介紹了通過按位與運(yùn)算(&)取值玄货,結(jié)合這里的需求皇钞,可以發(fā)現(xiàn),只需要將mask碼按位取反之后松捉,在與目標(biāo)對(duì)象進(jìn)行與運(yùn)算(&)夹界,便可以將指定位設(shè)置為0。
對(duì)飲實(shí)現(xiàn)代碼如下

#import "CLPerson.h"
#define CLTallMask (1 << 0)
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)


@interface CLPerson()
{
    char _tallRichHandsome;
}

@end

@implementation CLPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        _tallRichHandsome = 0b00000101;
    }
    return self;
}

//取值操作
- (BOOL)isTall {
    return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & CLHandsomeMask);
}

//設(shè)定值操作
- (void)setTall:(BOOL)tall {
    if(tall) {
        _tallRichHandsome |= CLTallMask;
    } else {
        _tallRichHandsome &= ~CLTallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if(rich) {
        _tallRichHandsome |= CLRichMask;
    } else {
        _tallRichHandsome &= ~CLRichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if(handsome) {
        _tallRichHandsome |= CLHandsomeMask;
    } else {
        _tallRichHandsome &= ~CLHandsomeMask;
    }
}

@end

調(diào)用及打印結(jié)果

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        
        person.rich = YES;
        person.tall = YES;
        person.handsome = YES;
        
        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
        
    }
    return 0;
}
***********************************************
2019-08-02 11:36:49.081651+0800 OC底層Runtime[1147:65497] 
tall = 1, 
rich = 1, 
handsome = 1
Program ended with exit code: 0

可以看到設(shè)定成功隘世。通過以上的嘗試可柿,就將原本需要3個(gè)字節(jié)來表示的信息,存儲(chǔ)到了一個(gè)字節(jié)里面丙者,以達(dá)到節(jié)省空間的目的复斥。

位域

上面的篇幅,我們通過&兩種位運(yùn)算械媒,實(shí)現(xiàn)節(jié)約內(nèi)存的目標(biāo)目锭,請(qǐng)思考一下,這樣是否完美了呢纷捞?
細(xì)細(xì)分析一下痢虹,會(huì)發(fā)現(xiàn)有如下不足:

  • 后期的維護(hù)時(shí),假如我們有需要新增一個(gè)新的屬性主儡,那么就需要 增加一個(gè)對(duì)應(yīng)的mask碼奖唯,增加對(duì)應(yīng)的set方法, 增加對(duì)應(yīng)的getter方法糜值,還是相對(duì)麻煩的丰捷,而且代碼體積也會(huì)迅速增加坯墨。
  • 我們通過char _tallRichHandsome;表達(dá)了三個(gè)信息——tallrich瓢阴、handsome畅蹂,如果需要表示10個(gè)信息,可想而知這里的命名會(huì)非常長(zhǎng)荣恐,顯然擴(kuò)展性和可讀性都非常差液斜。

現(xiàn)在來看一下下面這段代碼

@interface CLPerson()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
        
    }_tallRichHandsome;
    
//    char _tallRichHandsome;
}

@end

代碼中,使用結(jié)構(gòu)體struct取代之前的char _tallRichHandsome叠穆,結(jié)構(gòu)體內(nèi)有三個(gè)成員——tall少漆、richhandsome硼被。每個(gè)成員后面的: 1代表這個(gè)成員占用1個(gè)位示损。成員前面的類型關(guān)鍵字不產(chǎn)生實(shí)際作用,只不過定義變量的語法規(guī)定需要有類型關(guān)鍵字嚷硫,這里為了統(tǒng)一都寫成char检访,成員實(shí)際占用內(nèi)存的大小由后面的這個(gè): X來表示,X就表示占用的位數(shù)仔掸。這個(gè)就是位域脆贵,關(guān)于這個(gè)概念的具體內(nèi)容,可以自行查看C語言相關(guān)基礎(chǔ)知識(shí)起暮。因?yàn)?code>struct作為一個(gè)整體單元卖氨,分配內(nèi)存的最小單位是一個(gè)字節(jié),那么tall负懦、rich筒捺、handsome這三個(gè)成員會(huì)按照先后定義的順序,在這一個(gè)字節(jié)的8位空間里面纸厉,從右至左排布系吭。


相應(yīng)地,下面需要調(diào)整一下對(duì)應(yīng)的getter/setter方法

******************************CLPerson.m*************************************
@implementation CLPerson

- (BOOL)isTall {
    return _tallRichHandsome.tall;
}
- (BOOL)isRich {
    return _tallRichHandsome.rich;
}
- (BOOL)isHandsome {
    return _tallRichHandsome.handsome;
}

- (void)setTall:(BOOL)tall {
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
    _tallRichHandsome.handsome = handsome;
}

@end

******************************main.m*************************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        person.tall = NO;
        person.rich = YES;
        person.handsome = NO;
        
        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
                
    }
    return 0;
}
******************************打印輸出*************************************
2019-08-02 14:53:31.980516+0800 OC底層Runtime[1333:126711] 
tall = 0, 
rich = -1, 
handsome = 0
Program ended with exit code: 0

從上面的輸出結(jié)果可以看出颗品,貌似getter/setter像是生效了肯尺,但是好像rich有點(diǎn)問題,設(shè)置成YES抛猫,最后打印出來了是-1蟆盹,應(yīng)該是1才符合預(yù)期的孩灯,這個(gè)問題先不管后面來解決闺金,我們可以加一個(gè)斷點(diǎn),開看一下結(jié)構(gòu)體_tallRichHandsome情況


也可以在lldb窗口通過命令得到

lldb) p/x person->_tallRichHandsome
((anonymous struct)) $0 = (tall = 0x00, rich = 0x01, handsome = 0x00)
(lldb) 

結(jié)果很清晰的顯示了峰档,三個(gè)成員tall败匹、rich寨昙、handsome的值確實(shí)是被正確設(shè)置了。
此外掀亩,還可以通過直接查看_tallRichHandsome的內(nèi)存中的情況舔哪,來說明結(jié)果。
首先通過下面命令拿到_tallRichHandsome的內(nèi)存地址

(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $1 = 0x00000001033025c8

然后通過命令查看該地址所對(duì)應(yīng)內(nèi)存的情況

(lldb) x 0x00000001033025c8
0x1033025c8: 02 00 00 00 00 00 00 00 41 f0 2f 96 ff ff 1d 00  ........A./.....
0x1033025d8: 80 12 00 00 01 00 00 00 06 00 05 00 05 00 00 00  ................

這個(gè)結(jié)果怎么看呢槽棍,首先要知道捉蚤,這種打印方式,是按照16進(jìn)制來顯示的炼七,那么每2個(gè)數(shù)字就代表一個(gè)字節(jié)缆巧,上面我們說了_tallRichHandsome實(shí)際占用1個(gè)字節(jié)大小,所以它對(duì)應(yīng)的值應(yīng)該是打印結(jié)果中的最開始的2個(gè)數(shù)字 02豌拙,而這個(gè)值轉(zhuǎn)換成二進(jìn)制是0000 0010陕悬,三個(gè)成員tallrich按傅、handsome在其中對(duì)應(yīng)的位上的值分別是0捉超、10唯绍,這樣就和我們的設(shè)定吻合了拼岳,證明了我們的getter/setter方法生效了。

回到上面我們遺留的問題推捐,為什么被設(shè)置成YES的成員裂问,內(nèi)存里面驗(yàn)證了沒有問題,為何最終被打印出來的卻是-1呢牛柒?原因在于堪簿,getter方法中返回值的時(shí)候,做了一次強(qiáng)制轉(zhuǎn)換皮壁。如何理解呢


我們通過下面的方法驗(yàn)證椭更,將rich的getter方法調(diào)整如下,并在返回的地方加上斷點(diǎn)

- (BOOL)isRich {
    BOOL ret = _tallRichHandsome.rich;
    return ret;
}

通過lldb打印ret的內(nèi)存結(jié)果如下

(lldb) p/x &ret
(BOOL *) $0 = 0x00007ffeefbff42f 255
(lldb) x 0x00007ffeefbff42f
0x7ffeefbff42f: ff bc 9e a9 7b ff 7f 00 00 70 e4 80 01 01 00 00  ....{....p......
0x7ffeefbff43f: 00 80 f4 bf ef fe 7f 00 00 95 0c 00 00 01 00 00  ................
(lldb) 

可以看到ret內(nèi)存里面是ff蛾魄,也就是二進(jìn)制的11111111虑瀑,確實(shí)如我們上面所說,結(jié)果在強(qiáng)轉(zhuǎn)是有這個(gè)問題滴须,

實(shí)際上舌狗,在轉(zhuǎn)換的時(shí)候,是根據(jù)對(duì)象值的最左邊位上的值進(jìn)行補(bǔ)值填充操作的扔水,因?yàn)镹O對(duì)應(yīng)的是0痛侍,一位二進(jìn)制的0轉(zhuǎn)換成BOOL,其余位上都補(bǔ)0魔市,所以不會(huì)影響最終結(jié)果主届。

至于這里為什么一個(gè)字節(jié)上的11111111被輸出的時(shí)候顯示-1赵哲,有疑問的話請(qǐng)復(fù)習(xí)一下有符號(hào)數(shù)的表達(dá)方式,這里不做贅述君丁。
對(duì)于當(dāng)前的這個(gè)問題枫夺,解決辦法也不少,我們可以用之前進(jìn)行兩次绘闷!運(yùn)算橡庞,就可以得到1了

- (BOOL)isRich {
        return !!_tallRichHandsome.rich;;
}

或者,可以擴(kuò)充一下成員信息所需要的位數(shù)

@interface CLPerson()
{
    struct {
        char tall : 2;
        char rich : 2;
        char handsome : 2;
        
    }_tallRichHandsome;
    
//    char _tallRichHandsome;
}

@end

這樣印蔗,如果誰需要設(shè)置成YES毙死,因?yàn)檎加昧?位,所以結(jié)果會(huì)是0b01喻鳄,按照補(bǔ)位填充的規(guī)則扼倘,應(yīng)該是0b0000 0001,不會(huì)影響最終值除呵。

小結(jié):使用上面的優(yōu)化方案再菊,我們精減了getter/setter的代碼實(shí)現(xiàn),還省去了mask碼颜曾。缺點(diǎn)是在取值的時(shí)候由于存在補(bǔ)位轉(zhuǎn)換纠拔,導(dǎo)致最終取值不夠精準(zhǔn)(第一種方案通過位運(yùn)算取值的方式不存在這個(gè)問題)。

共用體

接下來泛豪,我們來研究一下蘋果采用的優(yōu)化方案稠诲。蘋果實(shí)際上是基于上面第一種方案中的位運(yùn)算方法,結(jié)合聯(lián)合體/共用體(union)這個(gè)技術(shù)來實(shí)現(xiàn)的诡曙。

首先來回顧一下union這個(gè)概念臀叙,

union Person {
    char * name;//占用8個(gè)字節(jié)
    int age; // 占用 4個(gè)字節(jié)
    bool isMale ; //占用1個(gè)字節(jié)
}; //

系統(tǒng)會(huì)為union Person分配8個(gè)字節(jié)空間,它的3個(gè)成員共用這一段8字節(jié)的空間价卤。對(duì)比一下struct的定義

struct Person {
    char * name;//占用8個(gè)字節(jié)
    int age; // 占用 4個(gè)字節(jié)
    bool isMale ; //占用1個(gè)字節(jié)
}; //

根據(jù)內(nèi)存對(duì)其原則劝萤,系統(tǒng)為struct Person分配16字節(jié)內(nèi)存,其3個(gè)成員會(huì)擁有各自獨(dú)立使用的內(nèi)存空間慎璧。
用一張圖來總結(jié)如下

回到關(guān)于蘋果優(yōu)化的問題床嫌,首先看如下代碼

@interface CLPerson()
{
    union {
        char bits;
        
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
            
        };
    } _tallRichHandsome;
    
}

@end

_tallRichHandsome定義成一個(gè)unionunion中的定義的成員是共享內(nèi)存空間的胸私,按照上面的寫法厌处,我們?cè)趯?shí)際進(jìn)行位運(yùn)算實(shí)現(xiàn)getter/setter的時(shí)候,使用char bits;岁疼,bits就是很多位的意思阔涉,具體要多少位,靠它前面的類型關(guān)鍵字來確定,這里我們需要8位就夠洒敏,所以通過char來定義。因?yàn)橄旅娴?code>struct和char bits;是共享內(nèi)存的疙驾,實(shí)際使用中不會(huì)用到這個(gè)struct凶伙,但是可以借助它來解釋bits里面各個(gè)位所代表的含義,體會(huì)一下它碎。那么getter/setter修改如下

@implementation CLPerson
- (BOOL)isTall {
    return !!(_tallRichHandsome.bits & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome.bits & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome.bits & CLHandsomeMask);
}

- (void)setTall:(BOOL)tall {
    if(tall) {
        _tallRichHandsome.bits |= CLTallMask;
    } else {
        _tallRichHandsome.bits &= ~CLTallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if(rich) {
        _tallRichHandsome.bits |= CLRichMask;
    } else {
        _tallRichHandsome.bits &= ~CLRichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if(handsome) {
        _tallRichHandsome.bits |= CLHandsomeMask;
    } else {
        _tallRichHandsome.bits &= ~CLHandsomeMask;
    }
}
@end

*************************main.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        CLPerson *person = [[CLPerson alloc] init];
        person.tall = NO;
        person.rich = YES;
        person.handsome = NO;

        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);

    }
    return 0;
}
************************輸出結(jié)果**************************
2019-08-02 17:20:07.157392+0800 OC底層Runtime[1673:197854] 
tall = 0, 
rich = 1, 
handsome = 0
Program ended with exit code: 0

可以看到函荣,成功實(shí)現(xiàn)了getter/setter的需求。實(shí)際上和我們第一種方案里面的char _tallRichHandsome的使用是完全相同的扳肛,只不過這里換成了_tallRichHandsome.bits傻挂,不同的是這里我們通過union中的struct來增強(qiáng)代碼的可讀性,其實(shí)用下面的寫法挖息,省略掉struct定義金拒,得到的結(jié)果完全相同

@interface CLPerson()
{
    union {
        char bits;
    } _tallRichHandsome;
    
}

@end

需要注意的是,第一種方案里面套腹,對(duì)于成員信息(tall绪抛、richhandsome)在內(nèi)存里面的位置电禀,我們是通過結(jié)構(gòu)體來定義的幢码,而蘋果的方案里面,則實(shí)際上依靠mask碼來控制的尖飞,mask碼的位移數(shù)就代表了成員信息的位置症副,而union里面的那個(gè)struct最重要的作用就是解釋說明bits內(nèi)部的成員信息,就是為了增強(qiáng)可讀性政基,就是為了讓人容易看懂贞铣。所以以后在閱讀源碼的時(shí)候再看到這種union,再也不用害怕了沮明,就那么回事咕娄。


蘋果isa優(yōu)化總結(jié)

現(xiàn)在回到開篇的有關(guān)isa的源碼

struct objc_object {
private:
    isa_t isa;

public:

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;

    };

#endif

};

這里,精減掉了一些兼容性代碼珊擂,只保留針對(duì)iOS部分的代碼圣勒,根據(jù)本文研究的一些話題,我們可以將蘋果對(duì)isa的優(yōu)化概括為

通過位運(yùn)算和位域以及聯(lián)合體技術(shù)摧扇,更加充分的利用了isa的內(nèi)存空間圣贸,將對(duì)象的真正的地址存放在了isa內(nèi)存的其中33位上面,其余的31位被用來存放對(duì)象相關(guān)的其他信息扛稽。下面是isa其他位上的作用說明

  • nonpointer—— 0吁峻,代表普通指針,存儲(chǔ)著class、meta-class對(duì)象的內(nèi)存地址用含;1矮慕,代表優(yōu)化過,使用位域存儲(chǔ)更多信息
  • has_assoc—— 是否設(shè)置過關(guān)聯(lián)對(duì)象啄骇,如果沒有痴鳄,施放時(shí)會(huì)速度更快
  • has_cxx_dtor—— 是否有C++的稀構(gòu)函數(shù),如果沒有缸夹,施放時(shí)會(huì)更快
  • shiftcls—— 這個(gè)部分存儲(chǔ)的是真正的Class痪寻、Meta-Class對(duì)象的內(nèi)存地址信息,因此要通過 isa & ISA_MASK才能取出這里33位的值虽惭,得到對(duì)象的真正地址橡类。
  • magic—— 用于在調(diào)試的時(shí)候分辨對(duì)象是否完成了初始化
  • weekly_referenced—— 是否被弱飲用指針指向過,如果沒有芽唇,釋放時(shí)會(huì)更快
  • extra_rc—— 里面存儲(chǔ)的值是 引用計(jì)數(shù) - 1
  • deallocating——對(duì)象是否正在被釋放
  • has_sidtable_rc——引用計(jì)數(shù)器是否過大無法存儲(chǔ)在isa中顾画,若果是,這里就為1匆笤,引用計(jì)數(shù)就會(huì)被存儲(chǔ)在一個(gè)叫SideTable的類的屬性中亲雪。

為什么上面的has_assochas_cxx_dtor疚膊、weekly_referenced會(huì)影響對(duì)象釋放的速度呢义辕?objc源碼里面有答案:對(duì)象在釋放的時(shí)候,會(huì)調(diào)用void *objc_destructInstance(id obj)方法

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

從源碼的注釋以及實(shí)現(xiàn)邏輯寓盗,很容易看出灌砖,程序會(huì)

  • 根據(jù)obj->hasCxxDtor()來決定是否調(diào)用object_cxxDestruct(obj)進(jìn)行C++析構(gòu),
  • 根據(jù)obj->hasAssociatedObjects()來決定是否調(diào)用_object_remove_assocations(obj)進(jìn)行關(guān)聯(lián)對(duì)象引用的移除傀蚌。

obj->clearDeallocating();里面

isa.weakly_referencedisa.has_sidetable_rc會(huì)決定是否進(jìn)行weak_clear_no_lock(&table.weak_table, (id)this);table.refcnts.erase(this);操作基显。
因此isa中上述的這幾個(gè)值會(huì)影響到對(duì)象釋放的速度。


ISA_MASK的細(xì)節(jié)

我在詳解isa&superclass指針中有過如下總結(jié)


而本文開篇的iOS源碼里面中有如下規(guī)定善炫,在iOS下(也就是arm64)撩幽,

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL

上面的ISA_MASK是通過16進(jìn)制表示的,不太方便看箩艺,我們通過科學(xué)計(jì)算器轉(zhuǎn)換一下

這樣可以清晰的看到窜醉,通過 isa & ISA_MASK 取出來的到底是哪幾位上面的值。同時(shí)還可以發(fā)現(xiàn)一個(gè)小細(xì)節(jié)艺谆,最終得出來的對(duì)象的地址值榨惰,會(huì)得到36個(gè)有效二進(jìn)制位,而最后的四位静汤,只可能是 1000 或者 0000琅催,也就是十六進(jìn)制下的 80居凶,因此對(duì)象的地址最后一位(十六進(jìn)制下),一定是80藤抡。體會(huì)一下侠碧,然后通過代碼走一波

#import "ViewController.h"
#import <objc/runtime.h>
#import "CLPerson.h"


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"ViewController類對(duì)象地址:%p", [ViewController class]);
    NSLog(@"ViewController元類對(duì)象地址:%p", object_getClass([ViewController class]));
    NSLog(@"CLPerson類對(duì)象地址:%p", [CLPerson class]);
    NSLog(@"CLPerson元類對(duì)象地址:%p", object_getClass([CLPerson class]));
    
}
@end

*************************************************************打印輸出

2019-08-05 10:49:42.408303+0800 iOS-Runtime[1276:57991] ViewController類對(duì)象地址:0x103590dc8
2019-08-05 10:49:42.408405+0800 iOS-Runtime[1276:57991] ViewController元類對(duì)象地址:0x103590df0
2019-08-05 10:49:42.408481+0800 iOS-Runtime[1276:57991] CLPerson類對(duì)象地址:0x103590e90
2019-08-05 10:49:42.408565+0800 iOS-Runtime[1276:57991] CLPerson元類對(duì)象地址:0x103590e68
(lldb) 

有關(guān)isa的探討到這里就結(jié)束了。


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缠黍,一起剝皮案震驚了整個(gè)濱河市弄兜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嫁佳,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谷暮,死亡現(xiàn)場(chǎng)離奇詭異蒿往,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)湿弦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門瓤漏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颊埃,你說我怎么就攤上這事蔬充。” “怎么了班利?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵饥漫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我罗标,道長(zhǎng)庸队,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任闯割,我火速辦了婚禮彻消,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宙拉。我一直安慰自己宾尚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布谢澈。 她就那樣靜靜地躺著煌贴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锥忿。 梳的紋絲不亂的頭發(fā)上崔步,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音缎谷,去河邊找鬼井濒。 笑死灶似,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瑞你。 我是一名探鬼主播酪惭,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼者甲!你這毒婦竟也來了春感?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤虏缸,失蹤者是張志新(化名)和其女友劉穎鲫懒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刽辙,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窥岩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宰缤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颂翼。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慨灭,靈堂內(nèi)的尸體忽然破棺而出朦乏,到底是詐尸還是另有隱情,我是刑警寧澤氧骤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布呻疹,位于F島的核電站,受9級(jí)特大地震影響筹陵,放射性物質(zhì)發(fā)生泄漏诲宇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一惶翻、第九天 我趴在偏房一處隱蔽的房頂上張望姑蓝。 院中可真熱鬧,春花似錦吕粗、人聲如沸纺荧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宙暇。三九已至,卻和暖如春议泵,著一層夾襖步出監(jiān)牢的瞬間占贫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工先口, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留型奥,地道東北人瞳收。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像厢汹,于是被迫代替她去往敵國(guó)和親螟深。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354