美文网首页
OC对象的本质

OC对象的本质

作者: 小冰山口 | 来源:发表于2024-03-01 22:54 被阅读0次

    Objective-C对象的本质

    • OC 底层实现都是 C, C++
    • OC面相对象都是基于C,C++的结构体实现的
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    

    以上代码, 将main.m文件转换为c++文件

    • NSObject的底层实现
    
    struct NSObject_IMPL {
        Class isa;
    };
    

    typedef struct objc_class *Class
    从以上就可以说明, isa是一个指针, 在64位操作系统中, 占8个字节

    github上下载底层源码

    搜索objc4
    下载后打开
    搜索class_getInstanceSize

    image.png

    发现下面的代码:

    size_t class_getInstanceSize(Class cls)
    {
        if (!cls) return 0;
        return cls->alignedInstanceSize();
    }
    
        // Class's ivar size rounded up to a pointer-size boundary.
        uint32_t alignedInstanceSize() const {
            return word_align(unalignedInstanceSize());
        }
    
    

    Class's ivar size rounded up to a pointer-size boundary.

    上面这句话的意思是, class_getInstanceSize这个方法返回的其实是类的成员变量的大小, 并不是指针指向的那片内存空间的大小, 那么, NSObject的成员变量isa8个字节, 那么这个方法返回的就是8

    alloc本质调用的是allocWithZone
    allocWithZone去搜源码

    image.png
    发现最调用的是_objc_rootAllocWithZone
    然后继续搜
    image.png
    发现调用的是_class_createInstance
    然后继续搜
    image.png image.png

    然后发现决定size大小的是instanceSize这个方法

    image.png
        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;
        }
    

    CF requires all objects be at least 16 bytes.

    这里的逻辑很关键, 当size小于16时, 将size强制置为16. 这是CoreFoundation这个框架决定的

    alignedInstanceSize这个方法, 返回的是8(之前上文提到过的), 而extraBytes通常为0, 所以 size 初始赋值的时候是8+0, 但CoreFoundation要求至少16, 所以加了代码

     if (size < 16) size = 16;
    

    所以最终返回16, 也就是说NSObjectalloc方法开辟的内存空间为16

    所以回答问题

    一个NSObject对象占用多少个字节?

    • 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
    • NSObject对象内部只使用了8个字节(64位环境下, 通过class_getInstanceSize函数获得)

    只使用8个字节放下面这个结构体

    struct Student_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _no;
        int _age;
    };
    

    就是说这个结构体只占8个字节, 理解这一点非常重要

    那我们再来看一个自定义的类的本质
    现在定义一个Student

    @interface Student : NSObject {
        @public
        int _no;
        int _age;
    }
    
    @end
    
    @implementation Student
    
    @end
    

    然后用依然用如下命令行命令, 将文件转换成C++代码:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    

    最后我们发现, Student类的本质是:

    struct NSObject_IMPL {
        Class isa;
    };
    
    struct Student_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _no;
        int _age;
    };
    
    

    因为NSObject_IMPL这个结构体里只有一个isa指针, 那么Student_IMPL就相当于

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

    所以总的字节数就为8+4+4 = 16个字节

    下面, 我将对象的指针赋值给结构体的指针


    image.png

    指针的地址当然是一样的, 但是, 我通过结构体可以访问到内存中的值吗? 答案是可以的:

    image.png

    这也从侧面说明, Student这个对象的本质是一个结构体

    当我们打了断点, 可以看到对象的指针地址:

    image.png image.png image.png

    上图中可以很清晰地看到_no_age的值, iOS采用的是小端模式, 所以读取的时候是从高位到低位, 即
    0x00000005(5)
    0x00000012(18)
    上图中也可以很清晰地知晓Student对象的内存布局
    前面8个字节是存放isa指针的值(8个字节), 紧接着是_no成员变量的值(4个字节),最后是成员变量_age的值(4个字节)

    通过打印也可以知道Student成员变量所占内存的大小是16字节, 存放Student对象所开辟的内存空间也是16字节

    image.png

    思考: Person对象和Student对象各占几个字节?

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

    首先看Person对象, 它的本质是结构体

    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _no;
    };
    

    那么这个结构体占几个字节呢?
    看起来好像是isa8字节, 加上int类型的4个字节, 是12个字节, 但其实不是, 因为这中间涉及到一个概念叫做内存对齐
    也就是说, 内存的大小必须是结构体内所占字节数最大的成员变量所占字节的倍数, 那么isa8个字节, 那么就必须是8的倍数, 也就是最小是16.

    image.png
    从上图也可以看出, class_getInstanceSize这个方法返回的是对齐过的实例对象的大小 image.png

    alignedInstanceSize这个方法也是将未对齐的实例对象大小进行内存对齐
    所以内存对齐后, Person对象所占的内存大小为16个字节

    那么Student又占多少个字节呢?
    Student的本质是:

    struct Student_IMPL {
        struct Person_IMPL Person_IVARS;
        int _age;
    };
    

    看起来, Person_IMPL结构体占16个字节, 再加int类型的_age4个字节, 再因为内存对齐, 应该是32个字节.
    其实不是, 还是16个字节, 这是因为, Person_IMPL的最后4个字节是空的, 没有存放东西, 编译器优化, 会将_age放在后面4个字节中, 所以最终还是16个字节.

    那么再思考, 假设, Student这个对象后面的成员变量不是4个字节的int _age, 而是long long类型的, 那么, Student这个对象又占用多少个字节呢?
    我理解的是, 现在Student这个对象的本质是:

    struct Student_IMPL {
        struct Person_IMPL Person_IVARS;
        long long _score;
    };
    

    首先Person_IMPL这个结构体是占16个字节, 后面4个空字节放不下_score, 那么就必须再开辟4个字节的空间, 那就是20个字节, 但由于内存对齐, 所以最后是16 * 2 = 32个字节

    让我们来看看是不是这样吧?


    image.png

    发现对象创建所开辟的内存空间确实是32个字节,但对象中成员变量的值所占的字节数并不是20个字节, 而是24个字节

    image.png
    查看内存发现, 当long long 类型占8个字节, 最后的4个字节存不下时, 并不会占用最后的4个字节, 而是将那4个字节留空, 另外开辟16个字节, 然后存在低位的8个字节.
    所以这里是要特别注意的!

    如果Student类是这样的结构, 它的内存布局又是怎样的呢?

    @interface Student : Person {
        @public
        int _age;
        int _score;
    }
    @end
    
    @implementation Student
    @end
    

    这里16+4+4 = 24, 成员变量占24个字节, 看起来是没什么问题的. 但是, 最后的_score这个成员变量是存在最后高位的4个字节? 还是倒数第二的4个字节, 最后4个字节留空?

    image.png

    看起来是后面一种情况, 这和上一个例子中long long的那个属性的内存布局又有点不同.

    另外还有一点, 属性和方法
    如果对象中不仅仅有成员变量, 还有属性, 又会是怎样的呢?

    @interface Person : NSObject {
        @public
        int _no;
    }
    @property (nonatomic, assign) int height;
    @end
    
    @implementation Person
    @end
    

    其实, 这个Person对象的本质是:

    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _no;
        int _height;
    };
    

    那么它所占的字节数很明显是16个字节.
    因为@property (nonatomic, assign) int height;这个属性的本质就是生成了带下划线的成员变量 int _height以及set, get方法.
    创建这个对象所开辟的内存空间里不存在方法吗?
    是的, 方法不在创建这个对象所开辟的内存空间里, 因为成员变量的值是可能会变化的, 但方法不会变, 所以方法只有一份, 它不会随着创建对象开辟内存空间而又去占用内存空间, 方法是存在于类方法列表里的.

    最后一点要记住的是:

    class_getInstanceSize(创建一个实例对象, 至少需要多少内存? )就是那个结构体多少空间存储就够了.

    malloc_size(创建一个实例对象, 实际上分配了多少内存? )
    要提高内存的分配速度, 优化内存的分配, 会进行内存对齐, 这里的内存对齐和结构体那里的内存对齐还不太一样(这里是16的倍数)

    sizeof实际上是个运算符, 传进去的是类型, 比如下图, 传进去的是struct Student_IMPL这样一个结构体, 那么它在编译阶段就直接转换成24了, 不会在运行时阶段做任何运算.

    image.png

    但这里如果传进去的是student, 本质上是传进去一个指针, 那指针类型就是8个字节, 返回的也就是8

    image.png

    相关文章

      网友评论

          本文标题:OC对象的本质

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