iOS NSObject对象的本质、内存分配、ISA指针及superclass底层源码分析

3 篇文章 0 订阅
1 篇文章 0 订阅

本篇幅内容较多,但是干货满满,不仅涉及源码分析还涉及模拟系统底层计算分配流程,建议分次食用,耐心看完相信会有很多收获~

开发中使用最多的就是NSObject对象了,最近深入研究了一番,整理出来比较重要也是自己研究的比深入的几个点,通过源码的角度来分析一下,包括对象的底层实现,以及系统是如何使用内存对齐机制来计算对象大小的,包括isa指针及superclass指针等源码级别的分析,特做记录,以供翻阅回顾。

一 对象的本质

OC中的对象分为三种:

实例对象(instance对象)
存储实例变量的值

类对象(calss对象)
存储对象的信息、变量信息、实例方法、协议

元类对象(meta-class对象)
存储类方法

对象在底层是转变为c++的结构体来使用的,举个简单的例子看,比如创建一个Dog类,如下所示:

@interface Dog : NSObject
{
 int age;
 int ID;
 int number;
}
@end

@implementation Dog
@end

一个很简单的Dog类,当编译之后,他会被编译成如下的结构体:

struct Dog_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int ID;
int number;
};

很明显,这是一个结构体类型的数据,其中NSObject_IMPL类型是NSObject编译后的结构体,如下所示:

struct NSObject_IMPL{
Class isa;
}

明显NSObject里面的内容只有一个isa指针,isa指针的作用后面会分析。根据编译后的文件的内容来看,对象在运行的时候确实是以结构体的形式存在的。编译后的Dog结构体里有一个编译后的NSObject类型的结构体数据,因为它是继承于NSObject对象的,如果继承于其他对象的话也会有一个其他对象的结构体数据在里面。

顺便提一下,第一部分和第二部分都会用这个简单的Dog类来分析。

二 内存大小计算

还是以上面的Dog类来分析,看一下他在系统中占用的内存大小是多少,先自己计算一下(以64位系统分析):

Dog类编译后的结构体包含一个NSObject_IMPL类型的结构体数据,这个类型的结构体里有一个Class类型的变量,占用8个字节,所以Dog类里的NSObject_IMPL变量占用8个字节;
int类型的age变量占用4个字节;
int类型的ID变量占用4个字节;
int类型的number变量占用4个字节;
综上所属,Dog类型的数据应该占用8 + 4 + 4 + 4 = 20个字节。

看下系统的输出计算:

Dog类系统计算结果

可以看到系统给出的类的分配的空间大小为24字节(class_getInstanceSize方法),当实际使用的时候,给实例对象分配的大小达到了32字节(malloc_size方法),这个是为什么呢?

我们从源码的角度来对计算的方法一个个分析。

class_getInstanceSize
先看class_getInstanceSize方法。这个方法返回的是对类的实例变量分配的大小空间,而且是内存对齐之后的大小,看源码内容(苹果源码获取网站地址:https://opensource.apple.com/tarballs/):
class_getInstanceSize 源码
class_getInstanceSize
class_getInstanceSize源码

上面的三个图片是苹果objc框架的源码截图,展示了具体的class_getInstanceSize方法的实现,可以看到最终决定class_getInstanceSize大小的是word_align字节对齐方法,在这个方法里使用了字节对齐的方法来返回给类的变量实际分配的大小,我们根据方法的流程自己来算一下:
 (x + WORD_MASK) & ~WORD_MASK;

内存分配计算

根据上面的计算流程,class_getInstanceSize其实是进行了一次8倍内存对齐的操作,所以为什么系统计算的class_getInstanceSize方法返回的是24自己想必各位已经很清楚了。

malloc_size
malloc_size方法返回的是对象的实例实际占用的内存大小,malloc_size和class_getInstanceSize一样也采用了内存对齐机制,只不过他用的是16倍的内存对齐机制,就不做具体分析了。相关源代码如下:
malloc_size

值得一提的是,如果我们计算NSObjct大小的话,算然它的对象实例仅有一个8个自己的isa指针,但是我们会发现malloc_size方法返回的是16字节,原因如下:

malloc_size源码

OC的底层代码里对给对象分配的最小内存空间做了限制,限制最小为16字节,所以NSObject对象虽然仅仅有一个ISA指针,但是系统仍然会在实际使用他的实例对象的时候给他分配16个字节的空间。内存对齐机制是系统来决定的,这个机制提高了系统对内存空间的访问效率。

对象在内存中的排列

我们在看一下实例对象在使用的时候在内存中是如何存储的,为了方便内存查看,创建的一个dog对象并对其赋值,如下所示:
内存排列

根据对象的地址,我们看一下在内存中是如何排列的,以及占用的字节大小:

内存排列

每个数字代表一个字节,圈起来一共32的字节,正是malloc_size方法实际分配的字节数。

至此,对于对象在内存中的字节分配计算和使用应该已经非常清晰了吧。

三 ISA指针作用

ISA指针的作用是找到方法调用信息存储的对象,如果找到了就加以调用,我们引入一个SubDog的类来进行分析,此时我们有的类如下:

我们此时有两个类,一个继承于NSObject的Dog类,一个继承于Dog类的SubDog类,SubDog类里有一个实例方法和类方法,我们以SubDog类来分析一下他的实例对象,类对象,元类对象各自存储的信息是什么:

上面的图不仅仅列出来了subDog类的实例对象、类对象、元类对象的存储信息,还标明了Isa指针及superclass指针各自指向的地方。

再重新说明一下实例对象、类对象、元类对象内存储的信息:
实例对象(instance对象)
存储实例变量的值

类对象(calss对象)
存储对象的信息、变量信息、实例方法、协议

元类对象(meta-class对象)
存储类方法

我们看一个简单的实例,如果我们调用了

[subDog testInstanceFun];

这个方法,我们对这个方法的调用进行一个简单的分析:

1.从面向对象的角度来分析,我们创建了一个subDog实例对象,这个对象实现了testInstanceFun实例方法,所以我们调用testInstanceFun是没有问题的

2.从ISA指向的角度来分析:当我们调用subDog的testInstanceFun实例方法的时候,实际上是先通过subDog对象的isa指针寻找到SubDog类对象,SubDog类对象里包含了testInstanceFun方法的信息,所以会直接调用testInstanceFun方法。

这个就是ISA指针在方法调用里的作用。

ISA指针深入分析

实际上ISA指针里存储的就是指向对象的内存地址,我们验证一下实例对象的ISA指针是否是指向其类对象的:

创建一个SubDog的类对象和SubDog的实例对象,并打印输出他们的地址和ISA指针(因为无法直接打印对象的ISA指针,所以我做了一个特殊处理然后将实例对象的ISA指针打印了出来):

可以看到 :
SubDog类对象地址是 0x00000001000022c0
SubDog实例对象地址是 0x001d8001000022c1

他俩的值并不一样,为什么呢?
因为在ios的系统中如果需要通过ISA的地址进行查找的话,需要使用ISA_MASK进行&的操作之后才可以得到真正的地址,

我们试一下:

可以看到SubDog的实例对象通过进行和ISA_MASK的&操作之后得到的就是SubDog类对象地址 0x00000001000022c0

因此,我们可以得知实例对象的ISA指针确实是指向其类对象的,类对象的ISA指向元类对象也是一样的。

扩展:
object_getClass 也是根据ISA指针来返回数据的:
如果传入的是一个实例对象,那么就会返回类对象;
如果传入的是类对象,那么返回的就是元类对象;
如果传入的是元类对象,那么返回的就是元类对象的基类对象。
看源码表述的也很清晰:
object_getClass

objc_getClass 则是根据传入的字符串作为key去一个map表里查找,看是否有跟传入的key一致的类并将其返回,如果没有匹配的话则返回空。

objc_getClass的源码里调用的关键方法是:

四 superclass的作用

superclass从面向对象的角度来看的话,概念和原理是比较清晰的:调用方法时会从自己的方法实现里去找,如果没有实现则去父类里去寻找,如果父类一层层也没实现的话会抛出一个unrecognized异常,如果把这个流程放在我们的图里看的话就是这样的:

superclass

从实例对象开始->寻找实例对象的类对象看是否有方法信息->根据superclass寻找父类类对象信息->根据superclass找到NSObjct类对象->nil

还是验证一下SubDog的superclass指向的是否是Dog类:

很明显SubDog的类对象superclass的值和其父类Dog类对象的地址是一致的。

再看一个比较有意思的例子:

创建一个NSObject的category,并且给他添加一个实例方法,如下:

将这个category引入到我们的测试文件后,然后我们直接调用:

这个时候会发生什么呢?
直接看结果:

一个类对象,居然直接调用成功了父类对象的实例方法,如果从面向对象的角度来看的话是很难说通的吧?那我们通过isa指针和supperclass指针结合来看一下就很清晰了,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ypqzUftv-1598233415567)(https://upload-images.jianshu.io/upload_images/1232108-6fb84e156ac2d9aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

文字说明一下整个流程:
SubDog类对象通过ISA寻找SubDog元类对象看是否有方法实现->SubDog元类对象通过supperclass指针寻找Dog元类对象是否有方法实现->Dog元类对象通过supperclass寻找到NSObject元类对象->(特殊)NSObject元类对象的supperclass是NSObject的类对象,NSObject元类对象通过supperclass找到NSObject的类对象,NSObject的类对象里记录了我们添加的实例方法testNSObjectInstanceFun的实现信息,所以就可以直接调用成功了。

其实我们都知道,方法的调用是通过Objc 发送消息的机制来进行消息传递然后调用的。实际上在消息发送的时候并没有标记这个消息方法是+号消息还是-号消息(类方法或者实例方法),不管是类方法还是实例方法都是通过方法名去找,所以我们通过类对象直接调用实例方法是可以实现的。

附一张网络图:

小结:

至此我们的内容介绍就全部结束了,有问题欢迎留言,我们一起探讨~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值