美文网首页
底层原理:OC对象的本质

底层原理:OC对象的本质

作者: 飘摇的水草 | 来源:发表于2022-02-11 18:42 被阅读0次
    面试题
    1. 一个NSObject对象占用多少内存?
    准备工作
    • 我们新建一个demo来探究:一个NSObject对象占多大内存
    • 我们平时编写的Objective-C代码,底层实现其实都是C\C++代码,也就是说首先会转成C++代码,再转成汇编语言,最终变成机器语言。故最好把OC转成C++才能看清它的本质。
    • 将Objective-C代码转换为C\C++代码
    clang -rewrite-objc main.m -o main.cpp
    
    • 使用如下代码可以指定架构,其中iphoneos代表是真机,arch表示指定架构版本,如果需要链接其他框架,使用-framework参数,比如:-framework UIKit
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
    
    • 我们可以得到一个main-arm64.cpp文件
    • 把cpp文件直接拽入Xcode,为了不显示报错信息,我们不让它参与编译
    探究
    • 点击NSObject我们可以看到OC的定义
    • 在main.cpp文件里面搜索NSObject_IMPL,我们可以看到NSObject转成C++后的本质是一个结构体
    • 我们再点击Class进去,可以看到其实它就是一个指针。那么我们知道在64位环境下,它占8个字节。
    论证
    • 有了这个推论,我们还可以进一步论证我们的观点,这里我们用到runtime,也就是运行时的一个方法class_getInstanceSize
    • 为了验证推论的准确性,我们先找到苹果开源的源码,打开浏览器http://opensource.apple.com/tarballs/搜索objc
    • 点击进去,我们找到数字最大的一个,一般来说也是最新的一个
    • 下载压缩包解压后并打开
    • 搜索class_getInstanceSize,并在.mm文件里面可以看到它的实现
    • 点击3中的alignedInstanceSize()方法,我们可以看到进一步注释,这个方法返回的是类对象成员变量所占用的内存大小
    • 所以通过调用runtime的这个方法我们可以看到,NSObject类对象所占用的内存大小为8个字节
    • 我们还有个叫malloc_size的方法可以查看系统分配内存的大小。由于这里需要传一个C的指针,所以我们打印obj的时候需要桥接一下。然后我们会发现,返回的大小是16而不是8!
    总结
    • 为什么会造成这个不一致呢?我们还是要从源码入手分析,首先还是在objc的源码里面搜索allocWithZone这个方法,查看创建一个对象是内存是怎么分配
    • 我们点击进入class_createInstance这个方法,看到返回的是_class_createInstanceFromZone方法,我们再次点击进去就能很快锁定我们需要的信息:size
    • 我们可以从注释看到,所有对象至少分配16 bytes 的大小,从方法实现来看,如果size < 16,则size = 16

    答:NSObject对象内部只使用了一个指针变量所占用的大小(64bit,8个字节,32bit,4个字节,可以通过objc_getInstanceSize函数获得),但系统分配了16个字节给NSObject对象(通过malloc_size函数获得),即总结如下:系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject对象内部只使用了8个字节空间(64bit环境下,可以通过class_getInstanceSize函数获得)

    • 所以Objective-C的面向对象都是基于C\C++的数据结构实现的。
    • 思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
      • 结构体
    • 思考:一个OC对象在内存中是如何布局的?
      • NSObject的底层实现
      @interface NSObject
       {
         Class isa;
       }
      最终被转换为
      struct NSObject_IMP
      {
        Class isa;
      }
      
      • 相当于创建的NSObject对象最终在内存中就是一个结构体,并且这个结构体只有一个指针成员,而指针在64位的机器上占用8个字节,所以这个结构体在内存中占用8个字节,故一个NSObject对象在内存中也占用8个字节。虽然NSObject也声明了很多方法,但这些方法占用的空间并不是NSObject的,而是其他的,以后再讲。
      • 因为NSObject对象只有一个isa的指针,所以NSObject对象的地址也是isa指针的地址,也是结构体的地址值。
    1. 自定义对象占用多少内存?
    准备工作

    我们新建一个继承自NSObject的Person类,帮它增加三个成员变量,那么Person又会占据多大的内存

    • 照惯例我们还是使用runtime和malloc的函数来查看一下,会发现得到的是24和32,但是为什么是24和32呢?

    照惯例我们还是使用runtime和malloc的函数来查看一下,会发现得到的是24和32,但是为什么是24和32呢?

    论证

    打开main.m所在的文件夹,在终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp,得到重新编译后的C++文件,并把它拽入Xcode中,通过搜索可以看到Person在底层C++的实现,正如我们看到的,一个NSObject对象占8个字节,一个int占4个字节,那么一共占8+4+4+4=20个字节,为什么会打印出来24和32呢?

    • 这里涉及一个内存对齐原则,系统在计算结构体大小时,分配的空间是所需最大内存的倍数,在Person的结构体中,需要最大的内存是8个字节,所以当实际使用20个字节时,实际分配的是3 * 8 = 24个字节,所以Person指向的内存大小为24个字节
    • 我们也可以通过sizeof这个函数,打印出Person_IMPL这个结构体的长度,进一步论证Person转成C++结构体后的实际长度为24
    疑问
    • 那为什么通过malloc函数打印出来的长度会是32呢?我们还是从malloc的源码入手分析一波,打开网址https://opensource.apple.com/tarballs/,搜索libmalloc,选择编号最大的一份下载
    • 解压后打开源码,我们搜索bucket(可以理解为桶,容器),我们可以从注释看到,其实iOS系统在给我们分配内存时有它自己的原则,分配的大小都是16的倍数,所以即使Person_IMPL结构体为24,但是系统也会为它分配32个字节大小的内存
    总结
    • 系统在计算Person时只需要24个字节就够用了,但是实际上在OC分配规则下,系统实际分配的内存空间是32个字节
    • 我们在源码里直接搜索malloc.c,可以看到其实OC内存分配的实现方法并不止一种,只是我们刚刚的这个例子用到的分配方法是这个而已。
    • 以下Student的探究和上面类似
    @interface Student:NSObject
    {
       @public
       int _no;
       int _age;
    }
    Student *stu = [[Student alloc]init];
    stu->_no = 4;
    stu->_age = 5;
    student对象经过clang之后被转换成为
    struct Student_IMPL
    {
      Class isa;
      int _no;
      int _age;
    }
    
    • 结构体Student_IMPL占用多少字节,student对象即占用多少字节
    • student对象一共占用8个字节,8个字节的isa指针,两个分别占用4个字节的_no和_age
    • isa的地址是最低的,所以isa指针的地址就是student对象的地址
    • student_IMPL和student对象之间的强转
    struct Student_IMPL *stuImpl = (_bridge struct Student_IMPL *)stu;
    NSLog(@"_no = %d, _age = %d",stuImpl -> _no, stuImpl -> _age);
    
    • 还可以通过以下方法来获取对象占用的内存大小,对象的存储空间里只存放实例变量,不存放方法
    #import <objc/runtime.h>
    class_getInstanceSize([Student class]);
    
    • 或者使用malloc_size(stu)来获得stu指针指向的内存的大小,其与class_getInstanceSize打印的值可能不一样,一个NSObject对象alloc时至少分配16个字节,这是Core Foundation内部规定的,这个规定在runtime源码部分的allocWithZone方法内部。
    NSLog(@"%zd",malloc_size((_bridge const void *)stu));
    
    窥探内存结构
    • 实时查看内存数据:Debug -> Debug Workflow -> View Memory,但这里一定要打断点才行,然后在出现的页面下方的address里输入stu对象的地址即可。
    • 还可以使用LLDB指令:po stup stu来打印对象地址
    • 还可以用memory read 0x10050e810x 0x10050e810来打印出对象的内存结构,其中0x10050e810表示的是对象的内存地址,优化过后的指令为x/4xw 0x10050e810表示以4个字节来打印对象
    • 改写内存中的数据用:memory write 0x10050e810 6,即把地址为0x10050e810里存放的数据改为6
    • 结构体占用的字节是不是简简单单变量所占用的字节之和,还有一个字节对齐的规则,子类的结构体里包含了父类的实现,父类里面又包含了父类的父类的实现。
    • 看苹果开放的源码地址是:https://opensource.apple.com
    • 内存对齐规则:结构体的大小必须是占用最大内存成员的大小的倍数
    • 相比objc_getInstanceSize我们更关注malloc_size的值大小,objc_getInstanceSize也是内存对齐过后的大小。
    • 内存分配注意点:iOS在分配内存的时候一般是16的倍数,这个iOS底层的规则决定的,最大是256字节,目的是为了内存访问速度最快。
    • GUN:全名是GUN not unix,其开源了很多免费的工具。

    相关文章

      网友评论

          本文标题:底层原理:OC对象的本质

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