OC底层原理:对象的底层探索(下)

348 阅读5分钟

对象的底层探索.png

影响对象内存的因素

对象的内存分布

我们先来看一下ZGPerson这个对象所占用的内存空间是多少?

@interface ZGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) short number;

@end
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "ZGPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZGPerson *p = [ZGPerson new];
        p.name = @"张三";
        p.hobby = @"money";
        p.age = 32;
        p.height = 1.72;
        p.number = 18;
        //40
        NSLog(@"实际占用内存:%zd",class_getInstanceSize(ZGPerson.class));
        //48
        NSLog(@"系统分配内存:%zd",malloc_size((__bridge const void *)p));
    }
    return 0;
}

打印输出结果,ZGPerson实际占用内存是40字节,而系统分配给它的内存是48个字节。 接下来,我们给ZGPerson分别添加一个实例方法,一个类方法,看对它的内存有没有影响。

@interface ZGPerson : NSObject
...
- (void)test;
+ (void)test;

@end

@implementation ZGPerson

- (void)test {
    NSLog(@"%s",__func__);
}

+ (void)test {
    NSLog(@"%s",__func__);
}
@end

编译项目代码,发现输出结果并没有变化,说明给对象添加实例方法和类方法对它的内存并没有任何影响。那么这个现象出现的原因是什么呢?

其实是因为,在我们的实例对象中,实际存储的是对象的isa指针和实例对象的成员变量的具体的值

那么下面我们接下来探讨一下这个问题:成员变量的值它在存储在我们的对象里面的时候是按照我们的赋值顺序进行存储哪,还是会按照属性的书写顺序进行存储?

image.png 苹果会自动重排成员变量的顺序来达到一个内存优化的目的,不满8字节的成员变量的属性,会把其对应的值存放在一个8字节的内存空间里面。

联合体、位域

struct ZGStruct1 {
    char a;
    char b;
    char c;
    char d;
}struct1;

struct ZGStruct2 {
    char a : 1;//位域  00000000
    char b : 1;
    char c : 1;
    char d : 1;
}struct2;

struct ZGStruct3 {
    char a : 7;//位域  00000000
    char b : 2;
    char c : 7;
    char d : 2;
}struct3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //4,1,4
        NSLog(@"%ld,%ld,%ld",sizeof(struct1),sizeof(struct2),sizeof(struct3));
    }
    return 0;
}

char a : 1;//位域这里的1表示的是位域的信息,用位域表示信息需要注意两点:

  • 位域的长度不能超过数据类型的最大长度。
  • 一个位域是存储在同一个内存空间,如果当前位域的空间不够存储,会从下一个字节开始存储,如struct3就是这样的情况。
struct ZGTeacher1 {
    char *name;
    int age;
    double height;
}t1;

int main(int argc, const char * argv[]) {
    @autoreleasepool {   
//        t1.name = "张三";
//        t1.age = 32;
        t1.height = 3.3;
    }
    return 0;
}

image.png

union ZGTeacher2 {
    char *name;
    int age;
    double height;
}t2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        t2.name = "张三";
        t2.age = 32;
        t2.height = 3.3;
    }
    return 0;
}

image.png

我们看到结构体中的属性被赋值后并不会被其他属性影响,而联合体中的属性逐个被赋值后会产生变化。这是什么原因哪?

//0x100008398 0x100008398 0x100008398 0x100008398
 NSLog(@"%p %p %p %p",&t2, &t2.name, &t2.name, &t2.name);

我们为这个联合体和它内部的不同成员变量添加地址打印,发现它们在内存中的地址是一样的。

其实所谓的联合体就是在内存中开辟了一块足够大的内存空间,联合体内的成员变量共用这块内存

那么联合体t2的内存大小是多少哪,我们来打印一下。

//8
NSLog(@"%ld",sizeof(t2));

联合体所占内存大小的计算方法也遵循两条规则:

  • 联合体必须能够容纳最大的成员变量
  • 通过计算出来的大小必须是最大成员变量(基本数据类型)的整数倍
union ZGTeacher3 {
    char a[7];//7
    int b;//4
}t3;// 8

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //8
        NSLog(@"%ld",sizeof(t3)); 
    }
    return 0;
}

通过以上两个联合体的例子,我们可以总结以下联合体和结构体的区别是什么。

联合体和结构体的区别:结构体里的成员变量能够共存,联合体里的成员变量是互斥的,这样设计的好处是可以节省一定的内存空间。

nonPointerIsa

isa详解

  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、MetaClass对象的内存地址。
  • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多信息。

image.png

位域

image.png

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

通过上面的内容我们可以知道shiftcls存储着Class、Meta-Class对象的内存地址信息。 image.png

首先我们右移3位,跳过 nonpointer、has_assoc、has_cxx_dtor,来到shiftcls头部,然后左移31位再右移28位,这个时候我们来到了shiftcls尾部,这个中间部分的就是我们的shiftcls的内容。用p/x LGPerson.class输出打印验证,地址对应上,这里就是存储的我们的类对象Class。(请在真机环境下验证)

new方法

下面代码我重写了ZGPerson的init方法,我们来猜测一下p1.namep2.name的打印结果是什么?它们是否相同?

@interface ZGPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation ZGPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.name = @"张三";
    }
    return self;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZGPerson *p1 = [[ZGPerson alloc]init];
        ZGPerson *p2 = [ZGPerson new];
        NSLog(@"%p",p1.name, p2.name);
    }
    return 0;
}

编译运行后,结果如下:

张三, 张三

不出意外,类似于上面的代码我们都写过,其实这里就运用到了类的多态性init方法其实是一种工厂模式,它遵循了类的多态。

多态的三个条件:

  • 继承ZGPerson继承自NSObject
  • 重写ZGPerson重写NSObjectinit方法
  • 指向NSObject指针指向ZGPerson

以上,就是多态在实际开发中的体现。合理运用类的多态性可以降低代码的耦合度让代码更易扩展。

image.png 我们在源码中搜索+ (id)new {,这里也是调用的callAlloc方法,就等同于alloc + init,所以上面的打印结果等同。