美文网首页
OC对象的本质

OC对象的本质

作者: buding_ | 来源:发表于2024-03-10 19:11 被阅读0次

    一个NSObject对象占用多少内存?

    通过runtime和malloc来分析

    #import <objc/runtime.h>
    #import <malloc/malloc.h>
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSObject *obj = [[NSObject alloc] init];
        
        size_t T1 = class_getInstanceSize([NSObject class]);//获取到实例对象的成员变量所占用的内存大小
        NSLog(@"%zd",T1);
        
        size_t T2 = malloc_size( (__bridge const void *)(obj));//获取到对象真正占用的内存大小
        NSLog(@"%zd",T2);
        
        //2024-02-22 16:48:22.792416+0800 MJ-2[61805:1273297] 8
        //2024-02-22 16:48:22.792449+0800 MJ-2[61805:1273297] 16
    }
    

    通过代码可以知道一个NSObject对象占用内存是16字节(64位机器上);

    进一步分析:
    首先我们知道OC代码通过编译是会经历以下的变化:
    OC -> C++/C -> 汇编 -> 机器

    通过以下命令可以将OC代码转换为C++代码
    xcrun -sdk iphones clang -arch arm64 -rewrite-objc OC源文件 -o 输出CPP文件
    

    那么可以通过分析转换后的C++代码去进一步分析:

    //OC中NSObject 的定义
    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    
    //转换为C++后的定义
    struct NSObject_IMPL {
        Class isa;
    }
    typedef struct obj_class *Class; //Class即为一个指向结构体的指针
    
    //继承父类
    @interface BDObject:  NSObject {
      int _age;
      int _no;
    }
    
    struct BDObject_IMPL {
      struct NSObject_IMPL NSOBJECT_IVARS;
      int _age;
      int _no;
    }
    
    

    可以知道NSObject内部是有一个结构体指针,占用内存8个字节;
    继续跟踪源码,查找得出结论,当分配内存最小为16个字节

    通过https://github.com/apple-oss-distributions下载源码
       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;
        }
    
    内存对齐

    除了NSObject的限制最小size为16字节外,编译时的内存分配时还有一个重要的编译器特性叫做内存对齐;

    内存对齐是计算机内存管理中的一个重要概念。内存对齐实际上是将数据存放在内存中某种特定的地址上,这种地址根据数据类型的特定属性来确定,这样可以增加硬件的存取速度。
    以下是关于内存对齐的几个关键点:
    1 对齐的实际需求来自硬件。许多硬件平台要求访问特定类型的数据时必须位于特定的内存边界上。如果数据没有对齐,那么CPU需要进行两次访问才能获取完整的数据,这会导致访问速度降低。
    2 数据存储的地址应该是该数据类型大小的整数倍。 例如,一个 int 类型在32位系统中通常为4字节,因此,int类型的数据的地址应该是4的倍数。同理,一个 char 类型的大小为1字节,因此可以存放在任何地址上。
    3 编译器常常会自动对齐。为了提高效率,编译器在为结构体或类分配内存时,通常会自动地进行内存对齐,使得每一个数据成员都存放在其大小的整数倍的地址上。这可能会导致某些内存空间被浪费,这部分未被使用的内存空间我们通常称之为“填充字节”。
    4 可以通过编译器特性来控制对齐方式。在某些情况下,可能需要手动进行更精细粒度的内存对齐控制,可以使用编译器提供的特性来进行控制。例如,在GCC中,可以使用attribute((aligned(n)))语法来指定特定的对齐需求。
    5 不恰当的内存对齐可能导致运行效率下降,同时在某些体系结构下甚至可能导致错误。
    注意: 不同的编译器对齐的方式可能不同,不同的体系结构的对齐要求也可能不同。

    成员变量和成员函数

    在C++中,结构体或类通常有成员变量和成员函数(方法)两种组成部分。
    1 结构体中的成员变量存储在内存中该结构体对象的地址空间内。每个实例化的结构体对象都在内存中有其自己的存储空间,用来存储它的成员变量。如果你有两个相同的结构体对象,他们将分别在内存中拥有各自独立的存储空间。
    2 在同一个程序实例中,无论创建了多少个这种结构体的对象,其成员函数(如果有的话)只存储一次。所有的对象共享相同的成员函数代码。这是因为成员函数并不依赖于对象的具体实例,成员函数的代码是通用的,因此只需要在内存中存储一份,而不需要为每个对象都存储一份。当你调用一个对象的成员函数时,实际上是将该对象的地址作为隐藏参数传递给该函数,以便函数知道它应该操作哪个对象。

    总结,结构体或类的成员变量在每个对象的内存空间中都有一份,而成员函数在内存中只有一份,被所有的对象共享

    struct obj_class {
        Class isa;
        Class superclass;
        cache_t cache; //方法缓存
        class_data_bits_t bits; //用于获取具体的类信息
     }
     struct class_rw_t {
        uint32_t flags;
        uint32_t version;
        const class_ro_t *_ro;
        method_list_t *methods; //方法列表
        property_list_t *properties; //属性列表
        const protocol_list_t * protocols; //协议列表
        Class firstSubclass;
        Class nextSiblingClass;
        char *demangledName;
     }
     struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;  //instance对象占用的内存空间
        const char *name; //类名
        method_list_t * baseMethodList;
        property_list_t * baseProtocols;
        const ivar_list_t * ivars; //成员变量列表
        const uint8_t * weakIvarLayout;
        property_list_t * baseProperties;
     }
    
    实例对象、类对象、元类对象

    在 iOS 中,一个类的对象的信息主要存储在两个地方:实例对象和类对象。
    1 实例对象(Instance Object): 实例对象保存了类的实例变量。每当创建一个新的类的实例时,就会分配一块新的内存来存储实例变量,这块内存中的信息(即实例变量的值)能够在实例的生命周期内被获取和修改。实例对象通常包含一个指向他的类对象的指针,用于访问类级别的信息。
    2 类对象(Class Object): 类对象包含的信息有实例方法列表、协议列表、属性列表等。然后类对象还会包含一个指针isa指向他的元类对象,
    通过执行 [InstanceObject class]与object_getClass(InstanceObject)可获得类对象
    3 元类对象(Meta Object):元类对象的内存结构与类对象是一样的。元类对象中存储了类方法列表、指向NSObject(基类)的isa指针。
    通过执行object_getClass(ClassObject)可获取元类对象
    4 类对象的实例方法列表与元类对象的类方法列表,这就构成了完整的消息发送机制。因此,无论是实例方法的调用还是类方法的调用,其实质都是在寻找类对象或元类对象中的方法列表,然后调用其中的实现。

    要注意的是,实例对象和类对象的内存是分开管理的,每个实例对象都有自己独立的内存空间来存储实例变量,而类对象的信息则是被所有实例对象共享的。

    在 Objective-C 中,类对象和其元类对象包含的信息,主要是存储在数据段(Data Segment)中。这个数据段是程序的可读 / 写内存区域,用于存储全局变量以及静态变量。因为类对象和元类对象在程序运行期间是固定存在的,不会因为函数的调用结束而消失,因此它们并不像局部变量那样存储在栈(Stack)中,也不像动态创建的实例对象那样存储在堆(Heap)中。
    操作系统会为每个加载到内存中的程序分配一块内存空间,这块空间被分为不同的段,包括代码段(Code Segment),数据段(Data Segment),栈(Stack)和堆(Heap)。不同类型的数据会被存放在不同的段中。
    代码段(Text Segment):也被称为文本段或指令段,通常包括二进制可执行代码以及只读数据,例如常量字符串和跳转表。
    数据段(Data Segment):数据段通常是用来存储程序中初始化的全局变量和静态变量。
    数据段又可以细分为 Initialized Data Section,也就是已经被初始化了的数据;以及 Uninitialized Data Section(又被叫做 BSS Segment),对于这部分数据,在程序开始运行之前系统会将其初始化为零或者空指针。
    堆(Heap):堆是用于动态内存分配的区域,例如 C、C++ 中的 malloc()、new 操作符分配的内存和 Java、Python 等语言的对象实例都存储在堆中。堆的大小不固定,可以由程序进行扩展或收缩。
    栈(Stack):栈用于存储每次函数调用时的局部变量和返回地址。每当一个函数被调用时,一个新的栈帧会被创建并推入栈顶。当函数返回时,相应的栈帧会被弹出。

    相关文章

      网友评论

          本文标题:OC对象的本质

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