底層研究 - 對(duì)象的底層探索(下)

前言

底層研究 - 對(duì)象的底層探索(上)已經(jīng)探索完對(duì)象alloc底層原理,對(duì)象的內(nèi)存對(duì)齊和結(jié)構(gòu)體的內(nèi)存對(duì)齊,同時(shí)也知道了結(jié)構(gòu)體內(nèi)順序?qū)Y(jié)構(gòu)體的內(nèi)存分配大小產(chǎn)生影響,接下來繼續(xù)探究對(duì)象的內(nèi)存分布

一些用到的lldb指令

  • p/x 以十六進(jìn)制打印數(shù)據(jù)
  • p/o 以八進(jìn)制打印數(shù)據(jù)
  • p/t 以二進(jìn)制打印數(shù)據(jù)
  • p/f 以浮點(diǎn)形式打印數(shù)據(jù)
  • x/4gx 輸出對(duì)象的內(nèi)存地址,x/4gx中4代表輸出4個(gè),g代表每一個(gè)是8字節(jié)大小,x代表以16進(jìn)制打印。

1、影響對(duì)象內(nèi)存的因素

創(chuàng)建一個(gè)Person類,并且實(shí)例化一個(gè)對(duì)象并對(duì)其進(jìn)行賦值,發(fā)現(xiàn)打印的結(jié)構(gòu)是48.

@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@property (nonatomic ,assign) char a;
@end

@implementation Person
@end

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        Person *p = [Person new];
        p.name = @"小明";
        p.hobby = @"boy";
        p.hight = 1.8;
        p.age = 18;
        p.number = 123;
        p.a = 5;
        NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

//打印結(jié)果:48

我們打個(gè)斷點(diǎn)看看p的內(nèi)存分布情況,由于第一個(gè)8字節(jié)是isa,直接從第二個(gè)8字節(jié)開始打印。

p的內(nèi)存分布

通過打印結(jié)果可以發(fā)現(xiàn),第二個(gè)地址打印的值并不是我們賦值的成員變量的值,而且age、number、a這三個(gè)成員變量的值并沒有發(fā)現(xiàn)。我們唯一的著手的就只有第二個(gè)莫名其妙的地址,嘗試把它拆開分成三斷打印
消失的變量值

可以發(fā)現(xiàn)age、number、a這三個(gè)成員變量的值出現(xiàn)了,說明它們?cè)谕粋€(gè)內(nèi)存地址里。由此我們也可以得出一個(gè)結(jié)論,成員變量的值在存儲(chǔ)時(shí)自動(dòng)重新排序。那為什么這么做呢?可以推測是為了優(yōu)化內(nèi)存。
對(duì)象的內(nèi)存是8字節(jié)對(duì)齊,a和age以及number總共占用1+4+2 = 7 個(gè)字節(jié),并沒有超出8字節(jié),可以放在同一個(gè)內(nèi)存地址內(nèi),從而節(jié)省了內(nèi)存的消耗。

2、對(duì)象的內(nèi)存分布

既然對(duì)象的屬性在賦值后會(huì)自動(dòng)重排,那對(duì)象本身的成員變量或者繼承自父類的屬性會(huì)不會(huì)也重排呢?

@interface Person : NSObject
{
    @public
    int age;
    NSString *name;
    NSString *sex;
    int age1;
}
@end

@interface Person1 : NSObject
{
    @public
    int age;
    NSString *name;
    int age1;
    NSString *sex;
}
@end

@interface Person2 : Person
{
    @public
    char a;
}
@end

@interface Person3 : Person1
{
    @public
    char a;
}
@end

@implementation Person
@end

@implementation Person1
@end

@implementation Person2
@end

@implementation Person3
@end

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    
        Person *person = [[Person alloc] init];
        person->age1 = 18;
        person->name = @"小明";
        person->age = 19;
        person->sex = @"男";
    
        Person1* person1 = [[Person1 alloc] init];
        person1->age1 = 18;
        person1->name = @"小明";
        person1->age = 19;
        person1->sex = @"男";
    
        Person2* person2 = [[Person2 alloc] init];
        person2->age1 = 18;
        person2->name = @"小明";
        person2->age = 19;
        person2->sex = @"男";
        person2->a = 5;
    
        Person3* person3 = [[Person3 alloc] init];
        person3->age1 = 18;
        person3->name = @"小明";
        person3->age = 19;
        person3->sex = @"男";
        person3->a = 5;
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
person和person1內(nèi)存分布圖

從上面的打印結(jié)果可以得出兩個(gè)結(jié)論

  • 通過對(duì)比person和person1可以發(fā)現(xiàn),自己聲明的成員變量并不會(huì)被自動(dòng)重排順序,并且在內(nèi)存中的分布是按照成員變量聲明的順序存儲(chǔ)的
  • 通過對(duì)比person2和person3可以發(fā)現(xiàn),當(dāng)子類繼承自父類時(shí),子類的內(nèi)存是否被優(yōu)化,取決于父類的成員變量位置,因?yàn)閜erson最后一個(gè)成員變量是int,不夠8字節(jié),而person1是nsstring類型,已經(jīng)是8字節(jié)。

person2的現(xiàn)象這其實(shí)是一個(gè)系統(tǒng)級(jí)別的優(yōu)化,并不是重排導(dǎo)致

那如果是屬性繼承的話,會(huì)不會(huì)觸發(fā)屬性重排呢?

@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@end

@interface Person1 : Person
@property (nonatomic ,assign) char a;
@end

@implementation Person
@end

@implementation Person1
@end


int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        Person1 *p = [Person1 new];
        p.name = @"小明";
        p.hobby = @"boy";
        p.hight = 1.8;
        p.age = 18;
        p.number = 123;
        p.a = 5;
        NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

繼承屬性

從上圖打印的結(jié)果可以發(fā)現(xiàn)事實(shí)上父類自動(dòng)生成的成員變量并沒有和子類自動(dòng)生成的成員變量一起被系統(tǒng)自動(dòng)重排順序,而是各自進(jìn)行重排。這里出現(xiàn)這樣的原因是當(dāng)子類在繼承父類的數(shù)據(jù)結(jié)構(gòu)時(shí),父類是一塊連續(xù)的內(nèi)存空間,子類是沒有辦法去修改父類的數(shù)據(jù)結(jié)構(gòu)的,也就是說系統(tǒng)在進(jìn)行屬性重排的時(shí)候只是基于某一個(gè)類,并不會(huì)把子類的成員變量和父類的成員變量重排在一起。

3、位域和聯(lián)合體

接下來我們先講解下位域和聯(lián)合體。

  • 位域:在一個(gè)結(jié)構(gòu)體中以位為單位來指定其成員所占內(nèi)存,但指定的內(nèi)存大小不能超過該成員類型所占的最大內(nèi)存大小。
struct Struct1 {
    char a;
    char b;
    char c;
    char d;
}struct1;

struct Struct2 {
    // a: 位域名  4:位域長度
    char a : 1;
    char b : 1;
    char c : 1;
    char d : 1;
}struct2;

一個(gè)正常的結(jié)構(gòu)體,它所占的內(nèi)存空間由它的數(shù)據(jù)結(jié)構(gòu)決定,如果不使用位域,struct1占用4個(gè)字節(jié),但struct2只占用了1個(gè)字節(jié)(實(shí)際上是1位,8位1字節(jié))

struct Struct3 {
    char a : 7;
    char b : 1;
    char c : 1;
    char d : 1;
}struct3;

當(dāng)1個(gè)字節(jié)不夠存儲(chǔ)時(shí),會(huì)自動(dòng)存入下一個(gè)字節(jié)
,也就是struct3占用了2個(gè)字節(jié)。

  • 聯(lián)合體:將幾種不同類型的變量存放到同一段內(nèi)存單元中,幾個(gè)變量互相覆蓋,聯(lián)合體的作用是節(jié)省一定的內(nèi)存空間,所占內(nèi)存取決于最大成員變量,且必須是其最大成員變量(基本數(shù)據(jù)類型)的整數(shù)倍。
union Person {
    char *name;
    int number;
    double height;
}p1;

根據(jù)規(guī)則計(jì)算出來:Person結(jié)構(gòu)體的內(nèi)存大小為8字節(jié)

當(dāng)聯(lián)合體中有數(shù)組時(shí):

union Person {
    char a[7];
    int number;
    double height;
}p1;

聯(lián)合體Person中最大的成員變量是數(shù)組,內(nèi)存占用7個(gè)字節(jié),但因其不是基本數(shù)據(jù)類型,因此Person是double的整數(shù)倍,該聯(lián)合體占8個(gè)字節(jié)。

聯(lián)合體和結(jié)構(gòu)體的區(qū)別:
結(jié)構(gòu)體(struct)中所有變量是“共存”的,?聯(lián)合體(union)中是各變量是“互斥”的,只能存在?個(gè)。struct內(nèi)存空間的分配是粗放的,不管?不?,全部分配。這樣帶來的?個(gè)壞處就是對(duì)于內(nèi)存的消耗要??些。但是結(jié)構(gòu)體??的數(shù)據(jù)是完整的。聯(lián)合體??的數(shù)據(jù)只能存在?個(gè),但優(yōu)點(diǎn)是內(nèi)存使?更為精細(xì)靈活,也節(jié)省了內(nèi)存空間。

4、nonPointerIsa

有了聯(lián)合體和位域的知識(shí),我們看下objc底層是怎么使用的,來到_class_createInstanceFromZone方法

_class_createInstanceFromZone

進(jìn)入initInstanceIsa方法內(nèi),我們發(fā)現(xiàn)其內(nèi)部也是調(diào)用了initIsa方法


initInstanceIsa

進(jìn)入initIsa方法內(nèi),我們發(fā)現(xiàn)了其內(nèi)部就是對(duì)對(duì)象的isa指針進(jìn)行初始化,同時(shí)我們發(fā)現(xiàn)了isa_t的數(shù)據(jù)類型


initIsa

進(jìn)入isa_t發(fā)現(xiàn)它就是聯(lián)合體,根據(jù)我們掌握的聯(lián)合體知識(shí)可以發(fā)現(xiàn)它的目的是兼容舊版本的isa(Class cls)。


isa_t

如今的系統(tǒng)采用的是nonPointerIsa,相較于舊版本,它節(jié)省了內(nèi)存空間。因?yàn)閷?duì)象的isa是一個(gè)8字節(jié)的Class類型的結(jié)構(gòu)體指針,主要是用來存儲(chǔ)對(duì)象所屬類對(duì)象的內(nèi)存地址的,而存儲(chǔ)類對(duì)象的內(nèi)存地址不需要使用8字節(jié)這么大的內(nèi)存空間,所以系統(tǒng)就把一些與對(duì)象息息相關(guān)的信息也存儲(chǔ)到isa的內(nèi)存空間內(nèi),而nonPointerIsa的信息都存儲(chǔ)在ISA_BITFIELD這樣一個(gè)結(jié)構(gòu)體內(nèi)。

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      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 unused            : 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

這個(gè)nonPointerIsa的結(jié)構(gòu)體中有不少成員,下面我們看下這些成員變量的作用:

//nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化,0:純isa指針,1:不止是類對(duì)象地址,isa包含了類信息、對(duì)象的引用計(jì)數(shù)等
uintptr_t nonpointer        : 1;                 
//has_assoc:關(guān)聯(lián)對(duì)象標(biāo)志位,0沒有,1存在
uintptr_t has_assoc         : 1;    
//has_cxx_dtor:該對(duì)象是否有C++或者Objc的析構(gòu)器,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯,如果沒有,則可以更快的釋放對(duì)象                                 
uintptr_t has_cxx_dtor      : 1;      
//shiftcls:存儲(chǔ)類指針的值。開啟指針優(yōu)化的情況下,在arm64(真機(jī))架構(gòu)中用33位存儲(chǔ)類指針                                   
uintptr_t shiftcls          : 33; 
//magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間
uintptr_t magic             : 6;    
//weakly_referenced:標(biāo)志對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC的弱變量,沒有弱引用的對(duì)象可以更快釋放                                     
uintptr_t weakly_referenced : 1;      
//unused:沒有使用(在之前舊版本,該位置表示 deallocating: 標(biāo)志對(duì)象是否正在釋放內(nèi)存)                               
uintptr_t unused            : 1;    
//has_sidetable_rc:是否需要使用sidetable來存儲(chǔ)引用計(jì)數(shù),當(dāng)對(duì)象引用計(jì)數(shù)大于10時(shí),則需要借用該變量存儲(chǔ)進(jìn)位                           
uintptr_t has_sidetable_rc  : 1;       
//extra_rc:表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減1,例如,如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9.如果引用計(jì)數(shù)大于10,則需要使用到上面的has_sidetable_rc                              
uintptr_t extra_rc          : 19;

知道nonPointerIsa后,我們看下如何利用nonPointerIsa來獲取類對(duì)象!

5、利用isa得到類對(duì)象

通過nonPointerIsa的定義,我可以知道nonPointerIsa內(nèi)存儲(chǔ)的類對(duì)象地址空間在不同架構(gòu)下占用的位域分別為52、33、44

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
.....
        uintptr_t shiftcls_and_sig  : 52;                                      ......
#   else
......
        uintptr_t shiftcls          : 33; 
.....
#   endif
# elif __x86_64__
.....
      uintptr_t shiftcls          : 44; 
......
#   endif

在源碼的main文件內(nèi)創(chuàng)建一個(gè)person對(duì)象,因?yàn)槭请娔X是x86_64架構(gòu)的,所以對(duì)象占用的位域長度是44位來存儲(chǔ)。我們可以分別使用兩種方式來獲取類對(duì)象:

  • ISA_MASK
    我們可以直接用源碼提供的ISA_MASK & 上對(duì)象地址,就可以獲得類對(duì)象
  • 位運(yùn)算
    通過上面的分析我們已經(jīng)知道isa的內(nèi)存分配情況,要取出完整的類對(duì)象信息,只需要把其余的數(shù)據(jù)全部清零即可。首先右移3位到最右邊,左移20位(3 + 17)到最左邊,再右移17位返回原來的位置

屬性是從低位往高位存,x86_64架構(gòu)又是小端模式,地址低位存放在低地址(高位存放在高地址)
例如:0x12345678
-> 大端:12 34 56 78
-> 小端:78 56 34 12

兩種方式打印類對(duì)象信息

6、new方法

我們經(jīng)常發(fā)現(xiàn)創(chuàng)建對(duì)象的方式除了alloc方法外,還有new方法,那么二者有什么區(qū)別呢?
我們直接看下objc的源碼,找到new方法

new方法

通過源碼我們發(fā)現(xiàn),new方法調(diào)用callAlloc函數(shù)后調(diào)用了init方法,也就是說new方法本質(zhì)上就是alloc + init。

7、總結(jié)

  1. 對(duì)象??存儲(chǔ)了?個(gè)isa指針 + 成員變量的值,isa指針是固定的,占8個(gè)字節(jié),所以影響對(duì)象內(nèi)存的只有成員變量(屬性會(huì)?動(dòng)?成帶下劃線的成員變量)
  2. 在對(duì)象的內(nèi)部是以8字節(jié)進(jìn)?對(duì)?的。
    蘋果會(huì)?動(dòng)重排成員變量的順序,將占?不? 8 字節(jié)的成員挨在?起,湊滿 8 字節(jié),以達(dá)到優(yōu)化內(nèi)存的?的。
  3. 自己寫的成員變量的順序就按照書寫順序來的,蘋果會(huì)重拍屬性的順序,但是在重排的時(shí)候不會(huì)考慮父類,父類和子類各自重排屬性。
    4.結(jié)構(gòu)體(struct)中所有變量是“共存”的,?聯(lián)合體(union)中是各變量是“互斥”的,只能存在?個(gè)。
  4. 位域的寬度不能超過前?數(shù)據(jù)類型的最??度。
  5. nonPointerIsa是內(nèi)存優(yōu)化的?種?段,通過位域和聯(lián)合體兼容了舊版本的isa和存儲(chǔ)其他的信息在isa的內(nèi)存空間中。
  6. new = alloc + init
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容