iOS 底层 day01 OC对象的本质

作者: 望穿秋水小作坊 | 来源:发表于2020-08-20 16:32 被阅读0次
    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import <malloc/malloc.h>
    
    @interface Person : NSObject
    {
        @public
        int _age;
    }
    @end
    @implementation Person
    @end
    
    @interface Student : Person
    {
        @public
        int _no1;
        int _no2;
    }
    @end
    @implementation Student
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] init];
            Student *stu = [[Student alloc] init];
            Person *person = [[Person alloc] init];
        }
        return 0;
    }
    

    代码简述:构建了一个 Person 类,带有一个 _age 成员变量;构建了一个 Student 类,继承自 Person 类,带有 _no1_no2 成员变量;

    请问objstuperson 这三个实例对象,运行时分别会分配多少内存空间? 如果清楚的知道,那么请关闭自此文章。

    1. 什么是 Objective-C ?
    • Objective-C 是一门通用的、高级的、面向对象的编程语言,它扩展了标准的 ANSI C 语言。
    • 还将 SmallTalk式消息传递机制 加入到其中。
    • 目前主要支持的编译器有 Clang ,采用 LLVM 作为后端。
    2. 既然 Objective-C 是对标准 C 的扩展,请思考,那么 NSObject 底层是用 C 语言的什么来实现的呢?
    • 结构体
    • 结构体的第一个成员变量地址,就是这个结构体的地址
    3. 如何证明是使用 C 语言的结构体来实现的呢?
    • 将开篇的代码,使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp 指令进行编译,可以获得 main.cpp 文件,从中我们可以找到如下代码:

    • cpp 是 C plus plus , 也是 C++ 文件的后缀名

    • 通过 xcrun 指令,可以编译单个 .m 文件,对我们观察 Objective-C 代码本质很有帮助

    • 如下所示,就是一个 NSObject_IMPL 结构体

    // IMPL 就是 implement 的缩写
    struct NSObject_IMPL {
        Class isa;
    };
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _age;
    };
    struct Student_IMPL {
        struct Person_IMPL Person_IVARS;
        int _no1;
        int _no2;
    };
    
    4. 那么从上面结构体来看,一个 NSObject_IMPL 结构体对象,理论上会分配多少内存空间呢?
    • 分析上面代码可得知 NSObject_IMPL 只有一个 Class 成员,那么 Class 是什么呢?
    • 我们可以在 Xcode 中点击 Class 定义获得 typedef struct objc_class *Class;,得知 Class 其实就是一个类型为 struct objc_class *指针变量
    • 那么问题就变简单的,在 64 位的操作系统上,指针占 8 个字节,所以一个 NSobject 理论上占 8 个字节。
    5. 实际上我们项目运行中,NSObject 会占据多少内存呢?我们借助两个函数分析。
            NSObject *obj = [[NSObject alloc] init];
            NSLog(@"%zu",class_getInstanceSize([NSObject class]));
            NSLog(@"%zu",malloc_size((__bridge const void *)(obj)));
    
    // 输出日志
    // 2020-08-20 15:47:58.227486+0800 Demo3[7428:431600] 8
    // 2020-08-20 15:47:58.228025+0800 Demo3[7428:431600] 16
    
    • class_getInstanceSize 函数,字面意思是获取实例的大小,好像符合我们的意愿,结果打印的是 8 个字节,和我们上面分析结果一致。

    • malloc_size 是 C 语言函数,获取实例在内存中的大小,结果打印的是 16 字节。

    6. 分析 class_getInstanceSize 方法, 我们只能下载 objc4 源码进行分析了
    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());
    }
    
    • class_getInstanceSize 调用了 alignedInstanceSize 方法
    • alignedInstanceSize 方法有一句清晰的注释说明 此方法用于返回类的成员变量大小ivar 就是成员变量的意思。
    • 由此我们 class_getInstanceSize 返回 8 个字节,就得到圆满解释了。
    • 另外我们发现,苹果爸爸源码中的大括号,也存在两种写法。
    7. 分析 malloc_size 方法
    • 分析 malloc_size 方法,需要从 allocWithZone 入手
    // Replaced by ObjectAlloc
    + (id)allocWithZone:(struct _NSZone *)zone {
        return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
    }
    
    _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
    {
        id obj;
        obj = class_createInstance(cls, 0);
        return obj;
    }
    
    class_createInstance(Class cls, size_t extraBytes)
    {
        return _class_createInstanceFromZone(cls, extraBytes, nil);
    }
    
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                                  bool cxxConstruct = true, 
                                  size_t *outAllocatedSize = nil)
    {
        id obj;
        size_t size = cls->instanceSize(extraBytes);
        obj = (id)calloc(1, size);
    }
    
    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;
     }
    
    • 从上面代码一层层跟进,我们发现最终苹果爸爸会做一个判断if (size < 16) size = 16; 并且还有一个清晰的解释 // CF requires all objects be at least 16 bytes.

    • 这就解答了我们的疑惑,虽然 NSObject,实际上只需要占用 8 个字节,可是苹果爸爸认为 CF 中所有的objects ,不得小于 16 字节,所以,最终给 NSObject 的实例对象分配了 16 字节。

    8. 那么 person 占据多少字节呢?
            Person *person = [[Person alloc] init];
            NSLog(@"person:class_getInstanceSize: %zu",class_getInstanceSize([Person class]));
            NSLog(@"person:malloc_size: %zu",malloc_size((__bridge const void *)(person)));
            
    
    
    // 打印结果
    2020-08-20 16:16:59.162324+0800 Demo3[7806:473714] person:class_getInstanceSize: 16
    2020-08-20 16:16:59.162382+0800 Demo3[7806:473714] person:malloc_size: 16
    
    • 是否和你猜想的一样的?
    • 如果按照我们上面的理论,person 对象调用 class_getInstanceSize方法,应该是 8 + 4 = 12 ,他的成员变量只需要 12 个字节,为什么打印出来是 16 个字节呢?
    • 这关系到计算机基础知识 内存对齐,结构体的大小会是 占据空间最大成员变量 的倍数 。所以最终 12 字节会变成 8 的倍数,也就是 16 字节。
    9. 那么 stu 占据多少字节呢?
            Student *stu = [[Student alloc] init];
            NSLog(@"stu:class_getInstanceSize: %zu",class_getInstanceSize([Student class]));
            NSLog(@"stu:malloc_size: %zu",malloc_size((__bridge const void *)(stu)));
            
    // 打印结果
    2020-08-20 16:16:59.162441+0800 Demo3[7806:473714] stu:class_getInstanceSize: 24
    2020-08-20 16:16:59.162473+0800 Demo3[7806:473714] stu:malloc_size: 32
    
    • 通过前面的知识,class_getInstanceSize 计算内存 8 + 4 + 4 + 4 = 20,然后进行内存对齐,最终是 24 字节,没啥问题。

    • 为什么 malloc_size 会得到 32 字节呢? 这主要是苹果对内存的管理策略,对象最终获得的内存还会加一个约束 最终的内存会以 16 的倍数出现。所以 24 字节,又变成了 32 字节。

    • 对于这一点,读者可以自己往 Student上继续添加成员变量,观察占据内存的变化,来证实。

    相关文章

      网友评论

        本文标题:iOS 底层 day01 OC对象的本质

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