Objective-C的本质(1)——一个NSObject对象占用多少内存

(一)问题:一个NSObject对象占用多少内存?

需要储备的知识点:

  1. 熟悉OC代码的底层实现
    我们平时编写的OC代码,底层其实都是C/C++代码
    所以OC的面向对象都是基于C/C++的数据结构(结构体)来实现
  1. 将OC的代码转换成C/C++代码
    clang -rewrite-objc main.m -o main.cpp
    xcrun -sdk ipnoneos clang -rewrite-objc main.m -o main.cpp
    xcrun -sdk ipnoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
    clang -fobjc-arc -framework Foundation main.m -o main.cpp
  • xcrun:Xcode运行
  • iphoneos:苹果手机系统
  • arm64:不同平台支持的代码不一样
    Windows、mac、iOS
    模拟器(i387)、32bit(armv7)、64bit(arm64)
  • main.m :OC源文件名
  • main.cpp :输出的cpp文件
    支持ARC、指定运行时系统版本,比如
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
    有时候报错:/usr/include/mach/machine/vm_param.h:35:2: error: architecture not supported
    error architecture not supported
    去除掉-arch arm64就可以,具体原因不明。

****内存对齐的两个原则:
原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。
原则 2. 整个Struct的地址必须是最大字节的整数倍。(如果结构体中包含结构体成员,那结合两个结构体找出最大字节数的成员)

1个16进制代表4个2进制
2个16进制代表8个2进制
所以2个16进制代表一个字节

iOS是小端模式,读取内存是从高地址开始读。

大端模式:

字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。

小端模式:

与大端存储模式相反,在小端存储模式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。
例如,16位宽的数0x1234在小端模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
0x12为高字节 0x34为低字节


image.png

而在大端模式CPU内存中的存放方式则为:

image.png
struct LGStruct1 {
    char a;     // 1 + 7
    double b;   // 8
    int c;      // 4
    short d;    // 2 + 2
} MyStruct1; // 22 内存补齐24

struct LGStruct2 {
    double b;   // 8
    char a;     // 1 + 3
    int c;      // 4
    short d;    // 2
} MyStruct2; // 18 内存补齐为24

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%lu-%lu",sizeof(MyStruct1),sizeof(MyStruct2)); // 结果:24 - 24
        
    }
    return 0;
}

3.NSObject的底层实现

//OC中NSObject定义
@interface NSObject  {
    Class isa  ;
}
//转成cpp后的NSObject定义
struct NSObject_IMPL {
    Class isa; // 8个字节
};
// Class的定义(为一个指针)
typedef struct objc_class *Class;

注:一个指针占4个字节。指针即为地址,指针占几个字节跟语言无关,而是跟系统的寻址能力有关,譬如:以前是16位地址,指针即为2个字节,现在一般是32位系统,所以是4个字节,以后64位,则就占8个字节。
因此isa指针在内存中占8个字节。

NSObject内存布局

//NSObject对象内存中只有一个isa指针,内存地址假如为 0x100400110
//生成的obj的地址就为0x100400110
NSObject *obj = [[NSObject alloc] init]; 
// 16个字节

//引入#import 
// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

//引入#import   
// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));

这里是OC源码地址
https://opensource.apple.com/tarballs/

(1)、通过查看class_getInstanceSize源码,得知class_getInstanceSize函数打印为8的原因;

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

// 获得实力对象的成员变量所占用的大小
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

(2)、通过查看alloc分配内存源码源码,得知malloc_size函数打印为16的原因;


alloc分配内存源码

CF要求所有对象至少16字节

具体源码如下:

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

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);//——————>点击进入方法
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}
——————————————————————————————————————
// obj = class_createInstance(cls, 0);
id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);//——————>点击进入方法
}
——————————————————————————————————————
//_class_createInstanceFromZone(cls, extraBytes, nil);
static __attribute__((always_inline)) 
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);//——————>点击进入方法
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}
——————————————————————————————————————
//size_t size = cls->instanceSize(extraBytes);
//系统分配内存,获取当前实力对象的内存大小(只包含isa指针,为8个字节),如果小于16个字节,会直接分配16字节。
size_t instanceSize(size_t extraBytes) {
        //获取当前实力对象的内存大小(只包含isa指针,为8个字节)
        size_t size = alignedInstanceSize() + extraBytes;//——————>点击进入方法
        // CF 要求所有对象至少为16个字节。.
        //小于16个字节,会直接分配16字节。
        if (size < 16) size = 16;
        return size;
    }
——————————————————————————————————————
// size_t size = alignedInstanceSize() + extraBytes;
//class_getInstanceSize的方法也是调用的这个函数
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
NSObject内存布局

总结:1. 系统跟配了16个字节给NSObject对象(通过malloc_size函数获得)
2.但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

注:2个容易混淆的函数

//创建一个实例对象,实际上分配了多少内存?
#import 
malloc_size((__bridge const void *)obj);

//创建一个实例对象,至少需要多少内存?
//类似sizeof(),区别为传入参数不同,sizeof为运算符,不为函数,是传入的类型为多大,编译时直接确定,比如传入person对象,显示是person的类型,指针的大小。class_getInstanceSize为函数,传递为类,显示传入类的大小
#import 
class_getInstanceSize([NSObject class]);

例如:

struct MJPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; //8
    int _age;//4
    int _height;//4
    int _no;//4
}; // 计算结构体大小,内存对齐,24

@interface Person : NSObject
{
    int _age;
    int _height;
    int _no;
}
@end

@implementation MJPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[MJPerson alloc] init];

        NSLog(@"%zd %zd",
              malloc_size((__bridge const void *)(p))); // 32
              class_getInstanceSize([Person class]), // 24
    }
    return 0;
}

分析:
1.malloc_size()为类对象动态分配的内存大小,是操作系统内存对齐之后的大小,补齐规则为16的整数倍。即如果一个类对象需要的内存大小为20,通过malloc_size()方法获取的大小为32,即16的倍数;(16字节原因:通过libmalloc源码获知,其中一种分配方式:操作系统提前规划好了为16个字节倍数的空间,最大为256,使用时,系统直接取出对应空间大小的内存)
2.class_getInstanceSize()方法获取的内存大小是结构体内存对齐之后的大小,补齐规则为前面的地址必须是后面的地址正数倍和按照类的成员变量所占内存最大值的整数倍。即如果一个类对象需要的内存大小为20,最大成员为isa内存占用8字节,通过class_getInstanceSize()方法获取的大小为24,即8的倍数

*******进一步举例解释:

 LGTeacher  *p = [LGTeacher alloc];
        // isa ---- 8
        p.name = @"LG_Cooci";   // 8
        p.age  = 18;            // 4
        p.height = 185;         // 8
        p.hobby  = @"女";       // 8
NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p))); 
       结果 :40 - 48
        // 内存补齐为: 40
        // 系统分配为 :48
        // 对象申请的内存的大小 VS 系统开辟的大小 不一致
        // 前面8字节对齐 - 针对的是:对象里面的属性
        // 16字节对齐 - 针对的是:对象
        // 原因:避免越界风险,便于寻址 

(二)思考:一个Person对象、一个Student对象占用多少内存空间?

struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
}; // 16 内存对齐:结构体的大小必须是最大成员大小的倍数

struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16
    int _no; // 4 存在于没有使用的那四个字节中
}; // 16

如图:


Person 对象和Student对象的C++代码

Person 对象和Student对象内存图片

答案:都为16字节。

你可能感兴趣的:(Objective-C的本质(1)——一个NSObject对象占用多少内存)