美文网首页iOS底层原理
小码哥底层原理笔记:OC对象的本质

小码哥底层原理笔记:OC对象的本质

作者: chilim | 来源:发表于2020-04-28 14:29 被阅读0次

    (默认64位系统下)
    OC的对象结构都是通过基础C\C++的结构体实现的。

    NSObject的本质

    我们先看一个简单对象NSObject:

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] init];
        }
        return 0;
    }
    

    我们通过命令行将OC的mian.m文件转化为c++文件

    clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)
    生成 main.cpp
    

    我们可以指定架构模式的命令行,使用xcode工具 xcrun

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

    在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)代码如下:

    struct NSObject_IMPL {
        Class isa; //8个字节
    };
    //指针
    typedef struct objc_class *Class;
    

    可见一个NSObject对象其实就是一个NSObject_IMPL结构体。
    通过以下代码打印得知一个NSobject类实例对象的大小是8字节,也就说NSObject_IMPL结构体大小是占8字节,由于NSObject_IMPL结构体中只有一个isa指针成员变量,所以一个isa指针占8个字节。

    //获得NSObject类实例对象的大小:8个字节
    NSLog(@"%zd", class_getInstanceSize([NSObject class]));
    

    我们再通过以下代码看到iOS系统实际分配了16个字节给这个NSobject对象,但是NSobject对象实际只用了8个字节

    //获得obj指针所指向的内存大小:16个字节,虽然分配了16但是实际只是用了8个字节
    NSLog(@"%zd", malloc_size((__bridge const void *)obj));
    

    当然方法也占用内存空间,但是实例对象并不保存方法,方法存在其他地方。

    自定义类Person的本质

    Person对象继承自NSObject

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

    同理,我们转化成C++代码如下:

    struct NSObject_IMPL {
        Class isa;
    };
    
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;//占8字节,即:Class isa;
        int _age;//占4字节
        int _height;//占4字节
        int _no;//占4字节
    };//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节
    

    因为OC对象的本质就是一个结构体,所以结构体占用多少自己,对象就占用多少字节。
    我们分析,一个Person对象总共有4个成员变量,父类NSObject的NSObject_IMPL占8个字节,三个int成员变量分别占4个字节,所以加一起Person对象一共占20字节,但是内存占用大小应该是8的倍数,所以最终打印出来是占用24个字节。

    NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
    Person *person = [[Person alloc] init];//实例对象的方法不存放方法
    NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小
    

    但正常的OC对象的属性是@property,如下代码:

    @interface Person : NSObject
    {
        int _age;
    }
    @property (nonatomic, assign) int height;//也是转化为成员变量保存
    @end
    

    实际上上述代码相当于:

    @interface Person : NSObject
    {
        int _age;
        int _height;
    }
    ///拆分成成员变量和get,set方法,这里的方法不保存在实例对象中,保存在对象的分类中。因为方法只需要保存一份就行,如果保存在实例对象中,那么每次alloc初始化一个实例对象就要copy一份方法。这样就浪费内存了。
    - (void)setHeight{
        _height = 0;
    }
    - (int)getHeight{
       retutrn _height;
    }
    @end
    

    我们再来看一个更复杂一点的

    /* Person */
    @interface Person : NSObject
    {
        int _age;
    }
    @end
    @implementation Person
    @end
    /* Student */
    @interface Student : Person
    {
        int _no;
    }
    @end
    @implementation Student
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            NSLog(@"%zd  %zd",
                  class_getInstanceSize([Person class]),
                  class_getInstanceSize([Student class])
                  );
    //实际分配了多少字节
    NSLog(@"person - %zd  student - %zd", malloc_size((__bridge const void *)person),malloc_size((__bridge const void *)stu));
        }
        return 0;
    }
    

    打印结果:

    16  16
    person - 16  student - 16
    

    我们依据上面的分析与发现,类对象实质上是以结构体的形式存储在内存中,画出真正的内存图例

    Student类实例对象的存储结构
    实际Person和Student对象在内存中都是以结构体的形式存储,看右半部分图可知。
    我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。因为NSObject实际上就是一个只包含isa指针的结构体
    接下来我们分析Person实例对象中,NSObject_IMPL占用8个字节,_age占4个字节,由于一个OC对象至少占16字节,所以Person实例对象占16字节。在Student实例对象中Person_IMPL占12个字节,_no占4个字节,所以Student实例对象也是占16字节。
    再来一个例子:
    struct NSObject_IMPL {
        Class isa;
    };
    
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;//占8字节
        int _age;//占4字节
        int _height;//占4字节
        int _no;//占4字节
    };//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节
    
    @interface Person : NSObject
    {
        int _age;
        int _height;
        int _no;
    }
    
    @end
    
    @implementation Person
    
    @end
    
    @interface Student : Person
    {
        int _weight;
    }
    
    @end
    
    @implementation Student
    
    
    @end
    
    struct Student_IMPL
    {
        struct Person_IMPL Person_IVARS;
        int _weight;
    };
    
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
            Person *person = [[Person alloc] init];//实例对象的方法不存放方法
            NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小
            NSLog(@"person - %zd", malloc_size((__bridge const void *)person));//32, OC分配的大小,OC分配给对象的内存是16的倍数
        }
        return 0;
    }
    
    

    注意:class_getInstanceSize方法是获取实例对象实际占用的内存大小,malloc_size方法是获取系统分配给对象的内存大小
    内存对齐原则
    1、实际所占内存大小应该是8的倍数,例如算出来的实际所占大小是24,补齐之后应该是24。
    2、iOS系统分配给对象的内存大小最少是16字节,并且是16的倍数,例如实际对象占用内存24,但是系统会分配32。

    注:Double类型占8字节,其他如NSString,NSNumber等对象占16字节

    面试题

    一个NSObject对象占用多少内存?
    答:系统分配了16个字节给NSObject(通过malloc_size函数获得),但是NSObject对象内部只使用了8个字节的空间(通过class_getInstanceSize函数获得)(64位系统下)

    相关文章

      网友评论

        本文标题:小码哥底层原理笔记:OC对象的本质

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