小码哥IOS底层原理

NSObject的本质

2019-02-01  本文已影响4人  小心韩国人

NSObject是所有类的基类,所有的类都继承自它,它太平常了,平常到我们从不去多加任何思考,但是它又那么重要,因为他是OC的基础,所以今天我们将通过一下几点对NSObject抽丝剥茧,看看他的底层到底是怎样的:

  • 一个 NSObject 对象占用多少内存?
  • 对象的 isa 指针指向哪里?
  • OC 的类信息存放在哪里?

思考一下:一个 OC 对象在内存中是如何布局的?
我们创建一个简单 Command line 项目,然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -0 main.cpp命令把 .m 文件转换为 c++文件查看一下底层代码,然后在转换后的.cpp文件中搜索struct NSObject_IMPL {找到:

struct NSObject_IMPL {
    __unsafe_unretained Class isa;
};

这就是NSObject的底层实现,我们也可以直接进入到NSObject.h的头文件中看看NSObject是如何定义的:

@interface NSObject {
    Class isa;
}
@end

可以发现NSObject.h头文件的定义和.cpp文件中都是一样的,NSObject的底层就是一个 C++ 结构体,结构体中只有一个class类型的isa成员.这个class又是什么类型呢?点进去看一下:

typedef struct objc_class *Class;

原来class就是一个指向struct objc_class结构体类型的指针!现在知道了Class是一个指针,而NSObject底层就只有一个Class isa那我们就知道了NSObject占用多少内存了.因为指针在64位系统中占8个字节,在32位系统中占4个字节.所以我么可以猜测:NSObject在内存中占8个字节.我们猜测的正确吗?下面开始验证一下:

NSLog(@"NSObject 占用了 %zd 个字节?", class_getInstanceSize([NSObject class]));
//打印输出
NSObject 占用了 8 个字节?

难道我们的猜测是正确的?
注意:class_getInstanceSize()是获取某一个类创建出来的实例对象所占用的内存大小.
系统中还有一个方法是取出一个指针所指向的内存的大小:malloc_size(<#const void *ptr#>)运行一下代码:

NSObject *obj = [[NSObject alloc]init];
NSLog(@"NSObject 占用了 %zd 个字节?", class_getInstanceSize([NSObject class]));
NSLog(@"obj指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)obj));

// 打印
NSObject 占用了 8 个字节?
obj指针指向的内存占用了 16 个字节?

好了,现在有两个结果:8 和 16.哪一个才一个 NSObject 对象占用多少内存?的结果呢?答案是16个字节.因为class_getInstanceSize ()返回的其实并不是一个对象的全部内存大小,实际上它返回的是一个类的实例对象的成员变量所占用的内存大小,我们可以通过 runtime 的源码看一下:
查看步骤:

  1. 打开 runtime 源码搜索class_getInstanceSize
  2. 找到
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
  1. 点击进入alignedInstanceSize:
    // Class's ivar size rounded up to a pointer-size boundary.
翻译:返回的是class的ivar大小
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

可以看到, class_getInstanceSize()的确返回的是实例对象的成员变量锁占用的大小.其实 class_getInstanceSizemalloc_size的关系就好比下图:

所以正确的说法应该是:

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

我们还可以从 runtime 底层代码查看 NSObjectalloc方法来验证刚刚得出的结论.
验证步骤:打开runtime源码 --> 搜索allocWithZone--> 点击进入class_createInstanceFromZone方法 --> 点击进入 _class_createInstanceFromZone可以看到创建的alloc方法是调用obj = (id)calloc(1, size);传入了一个size,而这个size是调用instanceSize(extraBytes)获得,我们再进入instanceSize(extraBytes)内部,它的底层实现是这样的:

    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;//小于 16 ,就让他等于 16.
        return size;
    }

通过底层源码我们可以看到,如果 size < 16,就让他 等于 16,所以,NSObject的内存最小是16个字节.
我们还可以通过 view Memory 侧面查看一下NSObject的内存地址:


可以看到obj所占用的16个字节中,前8个字节是有值的,后8个字节全是0,其实前8个字节中存放的就是我们上面分析的isa.当然,这种方法不太严谨,了解一下就行.如果不想通过 view Memory工具查看,还可以使用命令行查看,先介绍几个常用的 LLDB 命令:
  • 打印
    print , p : 打印
    po : print object简写,打印对象
  • 读取
    memory read 等于 x : 读取内存
    比如: x/4xg 0x10086 : 读取 0x10086内存地址,显示4段,每段表示8个字节,以16进制显示.
    这里的 4 代表: 数量
    x 代表: 格式(x是16进制,f是浮点,d是10进制)
    g 代表: 字节大小(b:是byte,表示1字节;h:是half word,表示2字节;w:是word,表示4个字节;g:是giant word,表示8个字节)
  • 修改
    memory write 内存地址 数值: memory write 0x10086 18

我们使用 LLDB 命令打印一下obj地址:


我们在项目中肯定是使用自定义的类,所以我们拓展一下,定义两个类Person,Student
// Person
@interface Person : NSObject
{
@public
int _no;
}
@end

@implementation Person
@end

// Student
@interface Student : Person
{
    int _age;
}
@end
@implementation Student
@end

然后再实例化这两个类,打印输出它们占用的内存大小:

Person *person = [[Person alloc]init];
NSLog(@"Person 占用了 %zd 个字节?", class_getInstanceSize([Person class]));
NSLog(@"person指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)person));
Student *student = [[Student alloc]init];
        
NSLog(@"student 占用了 %zd 个字节?", class_getInstanceSize([Student class]));
NSLog(@"student指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)student));

大家可以猜测一下打印的结果是什么?
我们分析一下:Person继承NSObject,它的底层应该是这样:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; //指针,占8个字节
    int _no;//4个字节
};

所以class_getInstanceSize结果应该是12个字节,malloc_size结果应该是16个字节,因为内存对齐的原则.
Student继承自Person,它的底层应该是这样:

struct Student_IMPL {
    struct Person_IMPL Person_IVARS; 
    int _age;
};

Studentclass_getInstanceSize 和 malloc_size输出结果应该是多少呢?
我们直接来看一下运行结果:

 Person 占用了 16 个字节?
 person指针指向的内存占用了 16 个字节?
 student 占用了 16 个字节?
 student指针指向的内存占用了 16 个字节?

可以看到打印的都是16个字节,我们刚才分析的Personclass_getInstanceSize结果应该是12个字节呀?怎么输出的是16个字节?ok,我们从runtime源码中寻找答案,打开runtime源码找到class_getInstanceSize底层实现:

    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

align是对齐的意思,通过代码我们可以看出来,传入一个没有对齐的大小返回内存对齐后的大小.现在我们应该明白了class_getInstanceSize为什么是16而不是12了,因为这是内存对齐后的结果.
为什么student也是输出16个字节呢?因为通过以上分析我们知道Person占用了12个字节,但是系统给person分配了16个字节,还有4个自己接是空闲的,而student内部有一个int _age占用4个字节,系统当然不会放着4个空闲的字节不用,再去开辟内存,所以结果就是 12 + 4 = 16 个字节.

思考一下,如果再给student增加一个成员变量int _height,student的内存会有什么变化呢?
Person 占用了 16 个字节?
person指针指向的内存占用了 16 个字节?
student 占用了 24 个字节?
student指针指向的内存占用了 32 个字节?

输出结果如上所示,增加了int _height后,student的内存应该是 12 + 4 + 4 = 20,但是class_getInstanceSize输出的确是24,因为一个实例对象的大小必须是它最大成员变量的倍数,student最大的成员变量大小是12,所以它的2倍就是24,而malloc_size字节对齐的规则是必须是16的倍数,所以malloc_size结果是32.

讲到这里相信大家对OC对象的本质已经有了更清晰的认识,我们可以发现,在类对象中只存放了实例对象和属性,并没有方法.这是因为,一个类的实例对象可以有很多,每个实例对象的值也不一样,而类方法只需要一份供实例对象调用即可,所以类对象中只存储实例对象和属性,不存储方法.那么方法存放在哪里呢?我们下次分晓.
上一篇 下一篇

猜你喜欢

热点阅读