底层研究 - 对象的底层探索(下)

前言

底层研究 - 对象的底层探索(上)已经探索完对象alloc底层原理,对象的内存对齐和结构体的内存对齐,同时也知道了结构体内顺序对结构体的内存分配大小产生影响,接下来继续探究对象的内存分布

一些用到的lldb指令

  • p/x 以十六进制打印数据
  • p/o 以八进制打印数据
  • p/t 以二进制打印数据
  • p/f 以浮点形式打印数据
  • x/4gx 输出对象的内存地址,x/4gx中4代表输出4个,g代表每一个是8字节大小,x代表以16进制打印。

1、影响对象内存的因素

创建一个Person类,并且实例化一个对象并对其进行赋值,发现打印的结构是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);
}

//打印结果:48

我们打个断点看看p的内存分布情况,由于第一个8字节是isa,直接从第二个8字节开始打印。

p的内存分布

通过打印结果可以发现,第二个地址打印的值并不是我们赋值的成员变量的值,而且agenumbera这三个成员变量的值并没有发现。我们唯一的着手的就只有第二个莫名其妙的地址,尝试把它拆开分成三断打印
消失的变量值

可以发现agenumbera这三个成员变量的值出现了,说明它们在同一个内存地址里。由此我们也可以得出一个结论,成员变量的值在存储时自动重新排序。那为什么这么做呢?可以推测是为了优化内存
对象的内存是8字节对齐,a和age以及number总共占用1+4+2 = 7 个字节,并没有超出8字节,可以放在同一个内存地址内,从而节省了内存的消耗。

2、对象的内存分布

既然对象的属性在赋值后会自动重排,那对象本身的成员变量或者继承自父类的属性会不会也重排呢?

@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内存分布图

从上面的打印结果可以得出两个结论

  • 通过对比person和person1可以发现,自己声明的成员变量并不会被自动重排顺序,并且在内存中的分布是按照成员变量声明的顺序存储的
  • 通过对比person2和person3可以发现,当子类继承自父类时,子类的内存是否被优化,取决于父类的成员变量位置,因为person最后一个成员变量是int,不够8字节,而person1是nsstring类型,已经是8字节。

person2的现象这其实是一个系统级别的优化,并不是重排导致

那如果是属性继承的话,会不会触发属性重排呢?

@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);
}

继承属性

从上图打印的结果可以发现事实上父类自动生成的成员变量并没有和子类自动生成的成员变量一起被系统自动重排顺序,而是各自进行重排。这里出现这样的原因是当子类在继承父类的数据结构时,父类是一块连续的内存空间,子类是没有办法去修改父类的数据结构的,也就是说系统在进行属性重排的时候只是基于某一个类,并不会把子类的成员变量和父类的成员变量重排在一起。

3、位域和联合体

接下来我们先讲解下位域和联合体。

  • 位域:在一个结构体中以位为单位来指定其成员所占内存,但指定的内存大小不能超过该成员类型所占的最大内存大小。
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;

一个正常的结构体,它所占的内存空间由它的数据结构决定,如果不使用位域,struct1占用4个字节,但struct2只占用了1个字节(实际上是1位,8位1字节)

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

当1个字节不够存储时,会自动存入下一个字节
,也就是struct3占用了2个字节。

  • 联合体:将几种不同类型的变量存放到同一段内存单元中,几个变量互相覆盖,联合体的作用是节省一定的内存空间,所占内存取决于最大成员变量,且必须是其最大成员变量(基本数据类型)的整数倍
union Person {
    char *name;
    int number;
    double height;
}p1;

根据规则计算出来:Person结构体的内存大小为8字节

当联合体中有数组时:

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

联合体Person中最大的成员变量是数组,内存占用7个字节,但因其不是基本数据类型,因此Person是double的整数倍,该联合体占8个字节。

联合体和结构体的区别:
结构体(struct)中所有变量是“共存”的,⽽联合体(union)中是各变量是“互斥”的,只能存在⼀个。struct内存空间的分配是粗放的,不管⽤不⽤,全部分配。这样带来的⼀个坏处就是对于内存的消耗要⼤⼀些。但是结构体⾥⾯的数据是完整的。联合体⾥⾯的数据只能存在⼀个,但优点是内存使⽤更为精细灵活,也节省了内存空间。

4、nonPointerIsa

有了联合体和位域的知识,我们看下objc底层是怎么使用的,来到_class_createInstanceFromZone方法

_class_createInstanceFromZone

进入initInstanceIsa方法内,我们发现其内部也是调用了initIsa方法


initInstanceIsa

进入initIsa方法内,我们发现了其内部就是对对象的isa指针进行初始化,同时我们发现了isa_t的数据类型


initIsa

进入isa_t发现它就是联合体,根据我们掌握的联合体知识可以发现它的目的是兼容旧版本的isa(Class cls)。


isa_t

如今的系统采用的是nonPointerIsa,相较于旧版本,它节省了内存空间。因为对象的isa是一个8字节的Class类型的结构体指针,主要是用来存储对象所属类对象的内存地址的,而存储类对象的内存地址不需要使用8字节这么大的内存空间,所以系统就把一些与对象息息相关的信息也存储到isa的内存空间内,而nonPointerIsa的信息都存储在ISA_BITFIELD这样一个结构体内。

# 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

这个nonPointerIsa的结构体中有不少成员,下面我们看下这些成员变量的作用:

//nonpointer:表示是否对isa指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等
uintptr_t nonpointer        : 1;                 
//has_assoc:关联对象标志位,0没有,1存在
uintptr_t has_assoc         : 1;    
//has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象                                 
uintptr_t has_cxx_dtor      : 1;      
//shiftcls:存储类指针的值。开启指针优化的情况下,在arm64(真机)架构中用33位存储类指针                                   
uintptr_t shiftcls          : 33; 
//magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t magic             : 6;    
//weakly_referenced:标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放                                     
uintptr_t weakly_referenced : 1;      
//unused:没有使用(在之前旧版本,该位置表示 deallocating: 标志对象是否正在释放内存)                               
uintptr_t unused            : 1;    
//has_sidetable_rc:是否需要使用sidetable来存储引用计数,当对象引用计数大于10时,则需要借用该变量存储进位                           
uintptr_t has_sidetable_rc  : 1;       
//extra_rc:表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9.如果引用计数大于10,则需要使用到上面的has_sidetable_rc                              
uintptr_t extra_rc          : 19;

知道nonPointerIsa后,我们看下如何利用nonPointerIsa来获取类对象!

5、利用isa得到类对象

通过nonPointerIsa的定义,我可以知道nonPointerIsa内存储的类对象地址空间在不同架构下占用的位域分别为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文件内创建一个person对象,因为是电脑是x86_64架构的,所以对象占用的位域长度是44位来存储。我们可以分别使用两种方式来获取类对象:

  • ISA_MASK
    我们可以直接用源码提供的ISA_MASK & 上对象地址,就可以获得类对象
  • 位运算
    通过上面的分析我们已经知道isa的内存分配情况,要取出完整的类对象信息,只需要把其余的数据全部清零即可。首先右移3位到最右边,左移20位(3 + 17)到最左边,再右移17位返回原来的位置

属性是从低位往高位存,x86_64架构又是小端模式,地址低位存放在低地址(高位存放在高地址)
例如:0x12345678
-> 大端:12 34 56 78
-> 小端:78 56 34 12

两种方式打印类对象信息

6、new方法

我们经常发现创建对象的方式除了alloc方法外,还有new方法,那么二者有什么区别呢?
我们直接看下objc的源码,找到new方法

new方法

通过源码我们发现,new方法调用callAlloc函数后调用了init方法,也就是说new方法本质上就是alloc + init。

7、总结

  1. 对象⾥⾯存储了⼀个isa指针 + 成员变量的值,isa指针是固定的,占8个字节,所以影响对象内存的只有成员变量(属性会⾃动⽣成带下划线的成员变量)
  2. 在对象的内部是以8字节进⾏对⻬的。
    苹果会⾃动重排成员变量的顺序,将占⽤不⾜ 8 字节的成员挨在⼀起,凑满 8 字节,以达到优化内存的⽬的。
  3. 自己写的成员变量的顺序就按照书写顺序来的,苹果会重拍属性的顺序,但是在重排的时候不会考虑父类,父类和子类各自重排属性。
    4.结构体(struct)中所有变量是“共存”的,⽽联合体(union)中是各变量是“互斥”的,只能存在⼀个。
  4. 位域的宽度不能超过前⾯数据类型的最⼤⻓度。
  5. nonPointerIsa是内存优化的⼀种⼿段,通过位域和联合体兼容了旧版本的isa和存储其他的信息在isa的内存空间中。
  6. new = alloc + init
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容