美文网首页iOS进阶
01-OC对象的本质

01-OC对象的本质

作者: 光强_上海 | 来源:发表于2020-06-14 19:21 被阅读0次

    OC对象的本质

    我们平时编写的OC代码,最终转换为底层实现基本上绝大部分都是基于C\C++来实现的

    下面展示OC代码最终编译转换的大致流程

    OC -> C\C++ -> 汇编代码 -> 机器代码
    

    也可以理解为OC的面向对象语法基本上都是基于C\C++的数据结构来实现的

    那么OC中的类和对象,最终究竟是基于C\C++的哪种数据结构来实现的尼?

    我们创建一个新工程,然后在main函数中创建一个obj对象,然后我们将这个main.m文件的代码转换为cpp文件,测试代码如下:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSObject *obj = [[NSObject alloc] init];
        }
        return 0;
    }
    

    转换为cpp文件的命令如下:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

    这句命令的格式如下:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx1 -o xxx2

    其中的xxx1代表的是OC源文件名,也就是上面的main.m

    其中的xxx2代表的是输出的CPP文件名,也就是上面的main.cpp

    执行完上面的命令后,会生成一个main.cpp的新文件,我们将此新文件拖拽到工程中,然后取消Add to Tagerts的选中,也就是让工程不编译main.cpp文件

    我们在main.cpp文件中搜索NSObject_IMPL,找到如下结构体代码:

    image
    struct NSObject_IMPL {
        Class isa;
    };
    

    从上面转换的cpp文件中的代码可以看到,NSObject对象的底层数据结构就是一个结构体对象

    我们看下NSObject的OC定义如下:

    image

    OC中NSObject最终转换为底层代码如下:

    image

    我们发现不管是OC中NSObject定义,还是转换为底层的代码,在NSObject的内部都包含了一个Class isa 指针

    我们进入Class内部定义查看,Class定义如下:

    image
    typedef struct objc_class *Class;
    

    我们发现Class是一个指向objc_class结构体的指针。既然Class是一个指针,也就类似于void *NSObject底层代码也就类似于下面代码:

    struct NSObject_IMPL {
        // 这里isa就是'void *'类型的指针
        void * isa;
    };
    

    在C语言中,void *:表示不确定类型指针,void *可以用来申明指针,例如:void * a,这里a就是void *类型的指针

    最终main函数中创建的obj对象,结构表示如下

    NSObject *obj = [[NSObject alloc] init];
    

    如图:


    image

    接下来我们再来探究下创建一个NSObject对象系统会分配多少内存?

    NSLog(@"---%zd",class_getInstanceSize([NSObject class])); // 8
    
    NSLog(@"---%zd", malloc_size((__bridge const void *)obj)); // 16
    

    class_getInstanceSize():表示NSObject的实例对象的成员变量所占用的内存大小

    malloc_size():表示obj指针所指向的内存大小,也就是创建一个obj实例系统所分配的内存大小

    需要注意:

    虽然创建一个obj实例对象系统分配了16个字节的内存,但是obj对象内部真正用到的大小就只有前面8个字节,也就是用来存放isa指针的那8个字节大小,还剩余8个字节大小没有被使用,也就是说NSObject_IMPL结构体也就占用8个字节

    对于class_getInstanceSize()函数,我们可以通过查看objc底层源码如下:

    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() {
            return word_align(unalignedInstanceSize());
        }
    

    通过alignedInstanceSize()函数注释:

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

    可知class_getInstanceSize()函数返回的确实就是实例对象成员变量所占用的内存大小

    我们也可以查看objc底层源码来确认创建一个obj实例对象分配内存的情况

    底层源码查看路径如下:
    objc4源码 -> 全局搜索allocWithZone()-> NSObject.mm -> _objc_rootAllocWithZone() -> class_createInstance() -> _class_createInstanceFromZone() -> calloc() -> instanceSize()

    instanceSize()函数中,我们可以看到如下定义:

    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }
    

    instanceSize函数的注释可以看出,在OC中创建一个实例对象,系统最少分配16个字节内存

    // CF requires all objects be at least 16 bytes.

    我们再来查看下继承自NSObject类的对象,系统分配多少内存?

    我们来看如下代码:

    // GQPerson的本质就是`struct GQPerson_IMPL`
    @interface GQPerson : NSObject {
        @public
        int _no;
        int _age;
    }
    @end
    
    @implementation GQPerson
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            GQPerson *person = [[GQPerson alloc] init];
            person -> _no = 10;
            person -> _age = 20;
            
            // 可以将person对象转为`struct GQPerson_IMPL`类型对象,因为`GQPerson`类型的本质就是`struct GQPerson_IMPL`类型
            struct GQPerson_IMPL *personImpl = (__bridge struct GQPerson_IMPL *)(person);
            NSLog(@"%d -- %d", personImpl ->_no, personImpl ->_age); // 10 20
        }
        return 0;
    }
    

    将上面代码转换为objc底层代码如下:

    struct NSObject_IMPL {
        Class isa; // 8个字节
    };
    
    struct GQPerson_IMPL {
        struct NSObject_IMPL NSObject_IVARS; // 这句等价于`Class isa` 
        int _no; // 4个字节
        int _age; // 4个字节
    };
    

    通过转换后的底层代码GQPerson_IMPL,我们知道创建一个GQPerson_IMPL对象,系统分配了16个字节

    我们通过下面的这两个函数打印也可以看出来,系统分配了16个字节内存

    NSLog(@"---%zd",class_getInstanceSize([GQPerson class])); // 16
    
    NSLog(@"---%zd", malloc_size((__bridge const void *)person)); // 16
    

    示例图如下:

    image

    下面我们再来看一个自定义类继承自另一个自定义类的例子,示例代码如下:

    @interface Person : NSObject
    {
        int _age;
    }
    @end
    
    @implementation Student
    @end
    
    
    @interface Student : Person
    {
        int _no;
    }
    @end
    
    @implementation Person
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
        }
        return 0;
    }
    

    我们将示例代码转换为底层c++代码如下:

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

    我们将上底层代码进行等价优化如下:

    struct NSObject_IMPL {
        Class isa;
    };
    
    // Person_IMPL结构体占用16个字节
    // 结构体内存对齐:结构体占用内存大小,必须是其中最大成员占用内存的倍数
    struct Person_IMPL {
        Class isa; // 8
        int _age; // 4
    };
    
    // Student_IMPL结构体占用16个字节,是因为_no的4个字节正好用在Person_IMPL结构体空余的4个字节上
    struct Student_IMPL {
        Class isa; 8
        int _age; // 4
        int _no; // 4
    };
    

    示例分析图如下:


    image

    通过分析优化的底层代码,我们可以知道personstudent对象都占16个字节内存

    下面我们再来看一个示例:

    @interface Person : NSObject
    {
        int _age;
        int _height;
        int _no;
    }
    @end
    
    @implementation Person
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            Person *person = [[Person alloc] init];
            
            NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
            NSLog(@"%zd", class_getInstanceSize([Person class])); // 24
            NSLog(@"%zd", malloc_size((__bridge struct Person_IMPL *)person)); // 32
        }
        return 0;
    }
    

    我们将上面的代码转换为底层c++代码如下:

    struct NSObject_IMPL {
        Class isa;
    };
    
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS; // 8
        int _age; // 4
        int _height; // 4
        int _no; // 4
    };
    

    我们通过打印可以看出,Person_IMPL结构体所占用内存大小为24,这个正好符合结构体内存对齐原理,但是创建一个Person实例对象为啥会分配32个字节尼,这个是苹果操作系统内存分配原则,具体参照底层代码:

    底层源码查找路径如下:

    libmalloc库 -> 搜索Buckets -> Buckets sized:注释`

    Buckets sized:

    /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

    我们从Buckets sized的注释可以看出,苹果操作系统分配内存的原则:分配最小内存为16,最大为256,所有的分配的内存都是16的倍数。上面24个字节大小不是16的倍数,所以最终分配内存大小为32个字节

    更多文章

    • ReactNative开源项目OneM(1200+star):https://github.com/guangqiang-liu/OneM:欢迎小伙伴们 star
    • 简书主页:包含多篇iOS和RN开发相关的技术文章http://www.jianshu.com/u/023338566ca5 欢迎小伙伴们:多多关注,点赞
    • ReactNative QQ技术交流群(2000人):620792950 欢迎小伙伴进群交流学习
    • iOS QQ技术交流群:678441305 欢迎小伙伴进群交流学习

    相关文章

      网友评论

        本文标题:01-OC对象的本质

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