美文网首页
OC内存分配、对象本质

OC内存分配、对象本质

作者: 再好一点点 | 来源:发表于2021-10-07 11:35 被阅读0次

了解OC内存分配可以从多个维度进行分析,首先用最简单地基类NSObject来分析,然后在拓展到其他复杂的类。

一:通过api获取一个实例变量的大小

如下obj占用多少内存呢?

NSObject *obj = [[NSObject alloc] init];

其实获取一个对象占用多少内存有苹果提供对应的api

        引入头文件
        #import <objc/runtime.h>
        #import <malloc/malloc.h>

       // 获得NSObject实例对象的成员变量占用的大小,结果为8
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        
        // 获得obj指针所指向内存实际占用大小,结果为16
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));

这两个结果为啥不一样呢?单纯从方法名字来看class_getInstanceSize代表获取实例所占用大小,malloc_size代表开辟多少空间,其实应该是一样的才对,但是如果看苹果源码就会看出区别来。

1.下面探索class_getInstanceSize为什么小于malloc_size大小。

苹果objc源码传送门,然后搜索"objc",找到"objc4/"点击进去,然后选取编号比较大的(最大的编号代表是最新的源码)。我下载的是"objc4-818.2.tar"。解压打开工程搜索class_getInstanceSize,找到实现如下:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

class_getInstanceSize继续查找如下:

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

从上边注释就可以看出来,class_getInstanceSize实际获取的是成员变量所占用存储空间的大小,word_align这个是字节对齐(注意后边要用到)。
到这里就可以看出class_getInstanceSize获取的大小实际就是objc实例对象成员变量所占用空间的总和。那么一个NSObject的实例变量objc到底有多少成员变量呢?

2.获取OC对应的C/C++代码

OC对象其实底层都是C/C++,所以想要更加明白的看清楚OC对象的本质就是把OC代码转成成C/C++。
OC的的面向对象其实就是基于C/C++的结构体封装的。
可以这样做,在终端执行如下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 将要输出的文件
参数解释:
iphoneos代表iPhone
-arch arm64表示将OC源码翻译成arm64架构支持的格式。armv7(3位)、i386(模拟器)这些就不需要了。现在iPhone都是64位的所以只转换64位就够了。

比如我的OC代码是这样的:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));//8
        NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));//16
    }
    return 0;
}

经过转换以后打开得到的C++代码可以看到有这样的一段代码:

struct NSObject_IMPL {
    Class isa; // 8个字节
};

其实这个就是NSObject对象的本质,是一个结构体。里边只有一个变量就是isa,类型为Class。而Class其实是一个结构体指针typedef struct objc_class *Class;。所以isa占用8个字节。

到这里就可以看出来为什么class_getInstanceSize获取到的数字为8了,因为一个NSObject实例对象只有一个成员变量,并且这个成员变量占用8个字节。

3.接下来分析malloc_size获取到的为什么是16?

因为一个OC的实例对象是使用alloc来分配内存的,底层是执行的allocWithZone,所以直接在源码中搜索allocWithZone

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

继续查找 _objc_rootAllocWithZone

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

    if (fastpath(!zone)) {
        obj = class_createInstance(cls, 0);
    } else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }

    if (slowpath(!obj)) obj = _objc_callBadAllocHandler(cls);
    return obj;
}

继续查找 _class_createInstancesFromZone

/***********************************************************************
* _class_createInstancesFromZone
* Batch-allocating version of _class_createInstanceFromZone.
* Attempts to allocate num_requested objects, each with extraBytes.
* Returns the number of allocated objects (possibly zero), with 
* the allocated pointers in *results.
**********************************************************************/
unsigned
_class_createInstancesFromZone(Class cls, size_t extraBytes, void *zone, 
                               id *results, unsigned num_requested)
{
    unsigned num_allocated;
    if (!cls) return 0;

    size_t size = cls->instanceSize(extraBytes);

    num_allocated = 
        malloc_zone_batch_malloc((malloc_zone_t *)(zone ? zone : malloc_default_zone()), 
                                 size, (void**)results, num_requested);
    for (unsigned i = 0; i < num_allocated; i++) {
        bzero(results[i], size);
    }

    // Construct each object, and delete any that fail construction.

    unsigned shift = 0;
    bool ctor = cls->hasCxxCtor();
    for (unsigned i = 0; i < num_allocated; i++) {
        id obj = results[I];
        obj->initIsa(cls);    // fixme allow nonpointer
        if (ctor) {
            obj = object_cxxConstructFromClass(obj, cls,
                                               OBJECT_CONSTRUCT_FREE_ONFAILURE);
        }
        if (obj) {
            results[i-shift] = obj;
        } else {
            shift++;
        }
    }

    return num_allocated - shift;    
}

继续查找 size_t size = cls->instanceSize(extraBytes);中的函数 instanceSize的实现

inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

在最后一个方法instanceSize中可以看到这么一段

      // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;

要求所有的对象最少是16字节。所以这也就解释了为什么通过class_getInstanceSize获取到的大小明明只有8个字节,而通过malloc_size获取到的实际开辟空间为16了。这个16字节是苹果的一种规则,也是结构体内存对齐的规则。结构体内存对齐有一条规则是结构体的总大小是内部占用最大内存类型的整数倍。

二:自定义类深入了解空间分配

下面定义一个Person 和 Student类,如下:

@interface Person : NSObject
{
    @public
    int _age;
}

@interface Student : Person
{
    @public
    int _no;
}
@end

源码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));//16
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));//16
        NSLog(@"%zd", sizeof(struct Student_IMPL));//16

        struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
        NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);

        Student *stu = [[Student alloc] init];
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));//16
        NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));//16

    }
    return 0;
}

其实上边获取到的这两个实例对象通过class_getInstanceSize和通过malloc_size获取到的结果都是16,这又是为什么?

老规矩可以通过源码分析。
同样的方法获取到的C++源码中有这么一段代码:

struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct  NSObject_IMPL NSObject_IVAS;
    int _age;
};

struct Student_IMPL {
    struct  Person_IMPL Person_IVAS;
    int _no;
};

其实可以把这些代码合并简化一下就是这样的:

struct NSObject {
    Class isa;
};

struct Person {
    Class isa;
    int _age;
};

struct Student {
    Class isa;
    int _age;
    int _no;
};

这样看起来就很简单了吧。

1.首先分析Person实例对象

isa;8个字节
_age;4个字节
这样看起来应该是12字节才对,但是上边提到了class_getInstanceSize方法里边会进行内存对齐,所以结果为16。
malloc_size分配最少为16字节。所以这里是16.

2.分析Student实例对象

isa;8个字节
_age;4个字节
_no;4个字节
这样看起来刚好16字节,所以结果为16.

malloc_size分配最少为16字节,但是单单一个Person就已经占用了16字节了,_no是不是要存放在其他的空间里边?其实不需要的,因为前两个成员变量isa_age才占用了12字节,还剩余4个字节的空间,这个是不能浪费的,又因为int 类型的_no刚好占用4个字节,所以剩余的4个字节刚好用来存放_no。所以结果还是为16。

上边使用sizeof()获取Student对应的结构体结果和class_getInstanceSize是一样的,只不过sizeof()是在编译器就计算出了结果,而class_getInstanceSize是在运行时计算的。

stu对象强转成Student_IMPL然后进行访问也可以得到正确的结果。说明stu数据结构确实和Student_IMPL是一样的。

 struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
画了一张图如下:
内存分配图
3.分析Student增加一个属性

Student如下:

@interface Student : NSObject
{
    @public
    int _no;
}
@property (nonatomic, assign) int weight;
@end

这时候student对象占用空间又是怎么样的呢?

        NSLog(@"student - %zd", class_getInstanceSize([Student class]));//24
        NSLog(@"student - %zd", malloc_size((__bridge const void *) student));//32

首先还是转换C/C++代码。
如下:

struct Student {
    Class isa;
    int _age;
    int _no;
    int _weight;
};

class_getInstanceSize为什么是24?
isa;8个字节
_age;4个字节
_no;4个字节
_weight;4个字节
累计占用20字节,因为存在内存对齐,所以总共需要24字节。

malloc_size最少为16,但是每次开辟空间都是16的倍数,所以大小为32。

4.增加了属性以后,为什么只增加了成员变量没有增加方法?

成员变量每个实例对象会存储一份,因为每个实例对象的同一个变量存储的数据是不同的,但是方法是公用的没必要每个实例对象保存一份。所以实例方法存放在类里边,类方法会存放在元类里边,这些以后专门写篇文章介绍。

三:可视化工具查看

除了代码分析以外还可以使用xcode提供的可视化工具查看内存分布。
Debug -> Debug Workflow -> View Memory可以大致查看内存分布状况。

代码如下:

        Person *person = [[Person alloc] init];
        person.weight = 3;
        person->_age = 4;

内存分布图如下


内存可视化界面

从图中可以看到_age 为04 00 00 00,weight为 03 00 00 00,因为iPhone使用的是小端模式,从高内存往低内存读取数据,其实读取到的顺序是00 00 00 04, 00 00 00 03。

关于OC内存分配大致说这么多吧。

相关文章

  • OC内存分配、对象本质

    了解OC内存分配可以从多个维度进行分析,首先用最简单地基类NSObject来分析,然后在拓展到其他复杂的类。 一:...

  • Objective-C 对象探究

    本文将分析 OC 对象的本质,对象的内存布局,已经如何为对象分配内存。分析的源码来自 objc-812[https...

  • OC对象的本质(上)

    iOS | OC对象本质 | Objective-C 什么是OC语言,OC对象、类的本质是什么,OC对象的内存布局...

  • iOS 底层知识总结

    一、OC语法 1、OC对象的本质 1)一个NSObject对象占用多少内存?A:系统分配16个字节给一个NSObj...

  • OC对象的本质 1--一个OC占用多少内存

    OC 对象本质是结构体类型 即 一个NSObject对象占用多少内存? 系统会分配16个字节给NSObject对象...

  • OC对象的本质

    1、OC对象的本质 OC对象就是一个结构体,结构体中包含了一个isa指针,系统给对象分配得内存空间最小是16个字节...

  • 探寻OC对象的本质

    iOS底层原理总结 - 探寻OC对象的本质 面试题:一个NSObject对象占用多少内存? 探寻OC对象的本质,我...

  • [CH2-Q1]Objective-C的对象——实例对象、类对象

    在CH1-Q1和CH1-Q2两个小节中,我们学习了OC实例对象的本质,并且能够掌握OC实例对象在内存是如何分配的,...

  • OC关于在MRC模式下的内存管理学习

    内存管理 管理范围 任何继承NSObject的对象 只有OC对象才需要进行内存管理的本质原理 1.OC对象在堆中 ...

  • 内存管理

    1.只有OC对象才需要进行内存管理的本质原因 --1.OC对象存放于堆中 --2.非OC对象存在栈中(栈内存会被系...

网友评论

      本文标题:OC内存分配、对象本质

      本文链接:https://www.haomeiwen.com/subject/ifwanltx.html