对象的底层探究(下)

262 阅读11分钟

一、影响对象内存的因素

当我们随意创建一个对象,该对象所占用的内存大小多少,存储了什么,影响内存大小的因素是什么。

在这里我们创建一个FMUserInfo类,并实例化一个对象user1来分析一下:

@interface FMUserInfo : NSObject
//isa  8
@property (nonatomic ,copy) NSString *name;  //8
@property (nonatomic ,copy) NSString *address;  //8
@property (nonatomic ,assign) int age;   //4
@property (nonatomic ,assign) double hight;  //8
@property (nonatomic ,assign) short number;  //2
-(void)getUserCurrentInfo;
-(void)getUserCurrentLocation;
@end

实例化该对象

        FMUserInfo *user1 = [[FMUserInfo alloc]init];
        NSLog(@"user1 实际运行内存分配大小为:%lu",malloc_size((__bridge const void *)(user1)));
        FMUserInfo *user2 = [[FMUserInfo alloc]init];
        user2.name = @"zhangsan";
        user2.address = @"SD";
        user2.age = 18;
        user2.hight = 180.0;
        user2.number = 120;
        NSLog(@"user2 实际运行内存分配大小为:%lu",malloc_size((__bridge const void *)(user2)));
        NSLog(@"创建的FMUserInfo类型对象所有实例变量实际占用的内存大小:%lu", class_getInstanceSize([FMUserInfo class]));

经实际运行结果为: image.png 实际运行结果说明:

  1. 我们创建出一个对象,即便不对对象中的属性进行赋值,对象中的属性仍然占用空间。
  2. FMUserInfo占用的内存大小为40,为8(isa) + 8 + 8 + 4 + 8 + 2 = 38个字节;但是class_getInstanceSize方法底层调用的是alignedInstanceSize -> word_align(unalignedInstanceSize()) -> return (x + WORD_MASK) & ~WORD_MASK;且64位的系统中WORD_MASK为8字节对齐,故这里FMUserInfo实际占用的内存大小为40.
  3. 对象中的方法并没有占用空间。
  4. 实际运行中对象所创建出来的大小为48,因为实际内存分配的时候是以16字节对齐。

如果我们在FMUserInfo类中添加一个char类型的属性,并对会有什么变化?

@interface FMUserInfo : NSObject
//isa 8
@property (nonatomic ,copy) NSString *name;  //8
@property (nonatomic ,copy) NSString *address;  //8
@property (nonatomic ,assign) int age;   //4
@property (nonatomic ,assign) double hight;  //8
@property (nonatomic ,assign) short number;  //2
@property (nonatomic ,assign) char sex;  //1
-(void)getUserCurrentInfo;
-(void)getUserCurrentLocation;
@end

----------------------调用代码-------

FMUserInfo *user2 = [[FMUserInfo alloc]init];
user2.name = @"zhangsan";
user2.address = @"SD";
user2.age = 18;
user2.hight = 180.0;
user2.number = 120;
user2.sex = 1;
NSLog(@"user2 实际运行内存分配大小为:%lu",malloc_size((__bridge const void *)(user2)));
NSLog(@"创建的FMUserInfo类型对象所有实例变量实际占用的内存大小:%lu", class_getInstanceSize([FMUserInfo class]));

然后打印user2

image.png 可以看到内存并没有变化,我们可以通过打印内存中的值来具体查看其内存分布

image.png

image.png 上边两张图中,第一张图为不加char类型属性sex的内存分布,第二张图为加了char类型属性sex的内存分布,我们可以看到系统会对我们创建的对象属性进行自动重排顺序,已达到优化内存的目的

二、继承对内存的影响

这里我们主要探究,子类是否会参与父类的属性重排。由前文可得知,由于苹果系统会对我们的类对象进行属性重排,所以类中属性的顺序并不会影响实际开辟出来的内存大小,那么在父子类的继承关系中,如果子类参与了父类的属性重排,那么父类中属性的顺序变化不会影响子类在实例化对象的时候,开辟出来的内存大小。反之,如果没有参与,则父类的属性顺序就会影响子类开辟的内存大小。我们从实际出发,先准备两个类。

@interface FMTestObject : NSObject
{
    @public
    int count;
    NSObject *objc1;
    NSObject *objc2;
//    int count;
}
@end

@interface FMTestObject1 : FMTestObject
{
    @public
    short _count2;
}

当我们把父类FMTestObject中的count属性进行位置调整的时候,分别打印下实际占用内存大小与系统开辟内存大小,得到如下两张图。

image.png

image.png 其中上图为count属性在第一行时的由子类实例化出的对象内存空间分配情况,下图为count属性在最后一行时的由子类实例化出的对象内存空间分配情况。显而易见,子类并没有参与父类的属性重排。究其原因,当我们的子类在继承父类的数据结构的时候,父类是一块连续的内存空间,子类是没办法修改父类的数据结构的,苹果在进行属性重排的时候,只是基于某个类,并不会把子类的成员变量与父类的成员变量重排在一起。

总结 对象里面存储了一个isa指针 + 成员变量的值isa指针是固定的,占8个字节,所以影响对象内存的只有成员变量(属性会自动生成带下划线的成员变量).

三、联合体位域

联合体

定义

联合体也叫共用体,是一种构造类型的数据结构。在一个联合体内能够定义多种不同的数据类型。一个被说明为该联合体类型的变量中。同意装入该联合体所定义的不论什么一种数据。这些数据共享同一段内存,以达到节省空间的目的。 联合体有两个特性:

  1. 在union中,分配内存空间的大小,等于占内存最大的数据类型字节大小。
  2. 共享同一段内存 以最简单的一个联合体为例:
union Un//联合类型的声明,union是联合体关键字
{
    char c;//1字节
    int i;//4字节
}un1;

image.png

我们通过打印知道这个联合体总计占4个字节,而联合体成员i是int类型的,它占了4个字节,另外一个c是char类型占了1个字节,两个一起占了4个字节。说明c和i必然有一处是共用一块空间的,再者有un1本身和它的两个成员是一个地址如上图0x104861520,说明首地址是重合的.

故:由于联合体这种特点就导致了,你改变c,i也会随之改变。这里和结构体是完全不一样的,结构体成员相互独立,但联合体不一样,改一个,其他的也会改变。所以这里,在同一时间,你只能使用一个联合体成员,你使用c就不要用i,因为你c改变的时候,一定会影响到你i的使用,程序非常容易出问题。

内存分析

在计算联合体大小之前我们必须知道两个知识点:

  1. 联合体必须能够容纳最大的成员变量(联合体的大小至少是最大成员的大小)
  2. .通过1计算出的联合体⼤⼩必须是联合体中占内存⼤⼩最⼤的基本数据类型⼤⼩的整数倍

举例说明:

union Un1
{
    char c[5];//1个char类型占1字节,5个占5字节
    int i;//4字节
} un1;
union Un2
{
    short c[7];//1个short类型占2字节,7个占14字节
    int i;//4字节
}un2;

image.png Un1解释: char创建一个大小为5的数组和放5个char类型的是一样道理,其基本数据类型为char,为1字节。int类型的i自身大小4字节,根据上述的联合体内存规则,最大成员大小为5,但是5不是最大基本数据类型的整数倍,所以我们需要对齐到最大基本数据类型的整数倍为8字节。

Un2解释: short创建的c数组,我们同上可知其c基本数据类型char是2字节,i的基本数据类型int是4字节,最大成员大小也就是c数组大小为14字节,14并不是最大基本数据类型4的整数倍,14往上对齐到16,16是4的整数倍。故内存大小为16。

联合体和结构体的区别

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

位域

定义

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域定义:

struct 位域结构名 {
    位域列表
};

例如:

struct FMStruct {
    // a: 位域名  7:位域长度
    char a : 7;
    char b : 2;
    char c : 7;
    char d : 2;
};

内存分析

下面是对于位域的几点说明:

  1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。
  2. 位域的长度不能超过数据类型的最大长度。 例如:char类型成员变量最大只占8位,那么位域长度最大不能超过8,又如int类型为4字节32位,那么位域长度最大不能超过32 我们可以根据以上规则分析下位域结构体的内存。
struct Struct2 {
    // a: 位域名  32:位域长度             所占内存
    int  m : 32; // m 直接占用32位               ---4
    char a : 7;  // a 占用 7位                  ---1
    char b : 2;  // b 由于上一字节所剩空间为1,位域不能跨两个字节,所以b从新字节开始放。 ---1
    char c : 7;  // c 由于上一字节所剩空间为6,位域不能跨两个字节,所以c从新字节开始放  ---1
    char d : 2;  // d 由于上一字节所剩空间为1,位域不能跨两个字节,所以d从新字节开始放  ---1
    char e : 6;  // e 由于上一字节所剩空间为6,可以存放e
    char f : 2;  // e 由于上一字节所剩空间为0,所以f从新字节开始放    --- 1
}struct2;

这里我们可以看到 struct2结构体共占用9字节,但是由于结构体的内存对齐,需要是其最大成员的整数倍,故为12字节。

四、nonpointerIsa

在之前的文章中,我们知道_class_createInstanceFromZone方法用于创建对象,开辟内存空间、并把对象isa与类做关联。那么类又是如何与类做关联的呢?

image.png

image.png_class_createInstanceFromZone函数中,我们看到如上两图所示,在开辟内存空间后,obj都会调用initIsa方法。

image.png

image.png initIsa方法中,最核心的就是对对象的isa指针进行初始化,同时我们发现了isa_t的数据类型为union联合体类型。 在联合体中,我们可以看到对于nonpointerIsa的定义;由于nonpointerIsa不同设备定义不同,分成x86arm64分别对应下图一与下图二

x86版本.png

arm64版本.png 其每个字段对应含义如下方所示:

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

五、如何利用isa的位运算得到类对象

下面我以M1电脑为例,创建一个FMPersion的实例对象。

@interface FMPerson : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,assign) int age;
@end

----------------
FMPerson *p = [FMPerson alloc];

那么如何获取到他的类信息呢? 有两种办法:

  1. 苹果爸爸给了我们可以方便获取类信息的掩码ISA_MASK,我们通过对象的地址& ISA_MASK就能得到类地址。
  2. 根据规则,位运算,手动计算。

办法1

由于我的电脑是M1型号,所以使用arm64的ISA_MASKdefine ISA_MASK 0x0000000ffffffff8ULL

image.png

办法2

由于苹果系统是小字段类型,所以根据内存分布可以得知shiftcls存储在[3-35]之间,也就是需要地址值左移28位,然后右移31位,然后左移三位,即可得到真实值,如下图所示:

image.png