iOS 底层 day01 OC对象的本质

1. 什么是 Objective-C ?
  • Objective-C 是一门通用的、高级的、面向对象的编程语言,它扩展了标准的 ANSI C 语言。
  • 还将 SmallTalk式消息传递机制 加入到其中。
  • 目前主要支持的编译器有 Clang ,采用 LLVM 作为后端。
2. 既然 Objective-C 是对标准 C 的扩展,请思考,那么 NSObject 底层是用 C 语言的什么来实现的呢?
  • 结构体
  • 结构体的第一个成员变量地址,就是这个结构体的地址
3. 如何证明NSObject是使用 C 语言的结构体来实现的呢?
#import 
#import 
#import 

@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 成员变量;

  • 将上述的代码,使用 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 源码进行分析了
  • 下载地址 https://opensource.apple.com/tarballs/objc4/objc4-723.tar.gz

  • 解压源码,用 Xcode 打开,我们可以找到如下代码

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对象的本质)