NSObject的本质

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的内存应该是 Person 中的 NSObject_IVARS 8 个字节 + _no 的 4 个字节也就是12 + 4 + 4 = 20,但是class_getInstanceSize输出的确是24,因为一个实例对象的大小必须是它最大成员变量的倍数,student最大的成员变量大小是8 (NSObject_IVARS),所以它的倍数就是24,而malloc_size字节对齐的规则是必须是16的倍数,所以malloc_size结果是32.

讲到这里相信大家对OC对象的本质已经有了更清晰的认识,我们可以发现,在类对象中只存放了实例对象和属性,并没有方法.这是因为,一个类的实例对象可以有很多,每个实例对象的值也不一样,而类方法只需要一份供实例对象调用即可,所以类对象中只存储实例对象和属性,不存储方法.那么方法存放在哪里呢?我们下次分晓.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容