上篇内容# iOS底层原理02-objc对象的底层探索(下),我们研究了对象的本质,并且通过对象的isa指针找到了类,那么类的本质是什么呢?这期我们来研究一下。
在开始探讨之前,我们先讲一下在学习面向对象开发时的内核:
万物皆对象
那对象时怎么来的呢?我们带着这个问题开始今天的研究。
一:前言 实例-类对象-元类
实例对象、类对象、元类的获取代码如下:
输出结果如下:
从结果中可以看出,打印的地址都是同一个,所以 NSObject
只有一份,即 NSObject
在内存中永远只存在一份。
总结1:
类的信息
在内存中永远只存在一份,所以 类对象
(类本身就是一个对象,这里说的不是类的实例对象。) 只有一份。
为什么[[obj class] class]
方法获取不到元类
?那原因是什么呢?查找 NSObject 源码得到 class 方法的源码:
总结2:
1, 一切皆对象。每一个对象都对应一个类。 Person
类就是person
变量对象的类, 换句话说就是person
对象的isa指向Person
对应的结构体的类; class
也是对象,描述它的类就是元类,换句话说class
对象的isa指向的就是元类
。
2, 元类保存了类方法的列表
。当一个类方法
被调用时,元类
会首先查找它本身是否有该类方法的实现
, 如果没有则该元类会向它的父类查找该方法, 直到一直找到继承链的头。
3,对象的class 方法
本质是调用object_getClass
,但是类的 class方法
确是直接返回自己,也就是类,所以对象无论调用多少次 class 方法,返回的都是类自己
。
问题: 那么对象的class方法返回的是它的类型,类的class方法返回自己,怎么看类的类型呢?
解答: 我们看到object_getClass
的源码如下:这个方法直接返回的是 isa 指针
的指向,所以可以获取到元类
。
获取类的类型,我们需要 object_getClass([obj1 class])
方法(相当于object_getClass(类名)
)来获取类的类型, 而不是 [[obj1 class] class]
。
综上:无论是类的对象,还是类,如果想获取他的类型是什么,都可以用object_getClass(xx)
方法。
二: isa , objc_object(object对象) 和 Class (类) ?
通过# iOS底层原理02-objc对象的底层探索(下),在alloc最后的流程中是_class_createInstanceFromZone
也就是计算出内存大小
和申请内存地址,并且返回指针
,然后将isa指针
与cls类
绑定在一起。最后的alloc
的返回是 一个obj
,它是id
的类型。所有NSObject
或其子类的alloc
的方法返回出的类型都是objc_object *
类型即id
类型。id类型
就是一个指向objc_object
的指针类型。
Class
类型在底层是一个结构体类型的指针
,这个结构体类型为 objc_class
。我们在源码中进行探索。
总结3:
1,NSObjec对象都是 C 语言结构体
,objc_object结构体
的类型。
2,所有继承自 NSObject
的类实例化后的对象都会包含一个类型为Class 的结构体
,在这些对象的实例变量的结构体中第一个就是 isa
, 这个结构体中包含了当前对象指向的类的信息
。
3,所有的 Class
(也就是类)也会包含这一个 isa
。在 ObjC 中Class
的定义也是一个名为objc_class
的结构体。Objective-C 中 Class (也就是类)也是一个对象
。
4,每个对象都有一个类,对象的类是isa指针
决定的,即 isa 指针
指向对象所属的类。
至此,可以得出结论:Objective-C
中类
也是一个对象
。在objc_class
中,除了isa
之外,一般还有3个成员变量,一个是父类的指针superclass,一个是方法缓存cache,最后一个这个类的实例方法链表
。当一个对象的实例方法
被调用的时候,会通过isa
找到相应的类
,然后在该类的class_data_bits_t
中去查找方法。class_data_bits_t
是指向了类对象的数据区域
。在该数据区域内查找相应方法的对应实现。类
的类方法
调用时,通过类的isa
在元类
中获取方法的实现。
类结构里面的一些元素:
isa 指针
我们之前的文章已经探索过了,在对象初始化的时候,通过isa
可以让对象和类关联,可是为什么在类结构里面还会有isa
呢?
其实就是我们的对象和类关联起来需要isa
,同样的,类和元类之间关联也需要isa
。isa
占用8
个字节。
superclass 指针
superclass 指针
表明当前类指向的是哪个父类
。superclass 指针
占用8
个字节。
一般来说,类的根父类
基本上都是NSObject类
。根元类的父类
也是 NSObject
类。这点很好理解,比如我们自定义一个Person
类,它继承自NSObject
,NSObject
就是继承自objc_object
结构体(c写的),objc_object
结构体里面有个isa
指针结构体,是一个Class
结构体,而Class
结构体又继承自objc_object
结构体。superclass
占用8
个字节。
cache 缓存
cache
的数据结构为cache_t
,顾名思义是一些缓存的信息,总共占用16
个字节,其定义如下:
bits 属性
bits
的数据结构类型是class_data_bits_t
,同时也是一个结构体
类型。
发现了一个好玩的关键字 friend
, 我们来查看一下该关键字的定义:
友元函数
友元函数是可以直接访问类的私有成员的非成员函数。
具体作用我们后面再看
bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型
。
根据上面的指针和内存偏移内容
可以知道,如果我们知道类的地址,并且属性是按照顺序依次排列
的,只要我们知道isa、superclass、cache的内存大小
,那我们就可以通过内存偏移
来得到bits
的内存地址,然后取出bits里面的内容。
在这个class_rw_t
结构体中我们惊奇的发现这里有methods(方法)
、properties(属性)
、protocols(协议)
这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我们就用代码来验证下。\
先定义一个Person类
,里面有 属性:name
,属性:age
,成员变量: hobby
,对象方法 instanceMethod
,类方法classMethod
,然后我们通过lldb
指令打印查看,结合上面的分析,来看看这几个成员都存储在了什么地方
在找bits
的时候是通过内存偏移方法
来找到,这也就是开头先补充的内存偏移的概念。 因为在objc_class
的结构中,isa占8字节
,superclass
占用8
字节,cache
占用16
个字节,将cls
的地址偏移32个
字节即0x100008288
便是bits
的地址。
这里我们就找到了class_rw_t
结构体,接下来继续查看methods,properties,protocols
这几个数组,来看看我们要找的方法、属性、协议
等是不是在里面
那么怎么查看methods
里面的值呢,我们继续
首先发现methods
是一个method_list_t
结构体,里面有个list
,list
下面有个ptr
,我们先尝试到ptr
还是没能清晰,还得继续找,目前发现count = 7
,也就是应该有7个方法,都是什么确看不到,现在发现了一个entsize_list_tt
结构体,有method_t
,method_list_t
结构体参数
找到了两个看似能用的 get()
和 getDescription()
赶紧尝试一下
搞定了一个
同理都找到出来
这个是ARC下的析构函数,后面会详细说到 (objc_method_description) $18 = (name = ".cxx_destruct", types = "v16@0:8")
通过以上打印可以看到,在class_rw_t
中找到了我们所定义的name/age属性、instanceMethod、name/age的setter/getter方法
,但是成员变量hobby和类方法classMethod
都没有找到。
总结4: bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型
。在class_rw_t
中有属性、属性的setter/getter方法、对象方法
。但不包括成员变量
和类方法
。
此时再从class_rw_t
找一找其他线索,发现有一个class_ro_t *ro
的东西,这是一个常量结构体指针
,那么我们要找的成员变量和类方法
会不会在这里呢,点进去看一下
进来一看发现,这里竟然跟class_rw_t
看起来差不多,同样有方法、属性、协议列表
,而且还有一个ivars 列表
,那么这个ivars会不会就是成员变量列表
呢。接下来继续用lldb指令
来查看
果然,我们定义的成员变量hobby
,age
,name
是在class_ro_t
里面的。同样在baseMethodList、baseProperties
里面也找到了我们所定义的属性和对象方法
,这里就不截图了。
总结5:
在class_ro_t
中有成员变量
,并且存在class_ro_t的ivars里面
。
1.在class_rw_t
里面存放的有methods、properties、protocols
。
2.在class_ro_t
里有baseMethodList、baseProtocols、baseProperties、ivars
。
3.class_ro_t
这个结构体是通过const
定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
4.成员变量不生成setter/getter
方法,并且存在class_ro_t的ivars
里面。
5.此时还有一个类方法classMethod
没有找到。
下篇我们来讨论本章的遗留问题:
1.类方法存储到哪了?
2.ro
,rw
,rwe
是什么?
3.苹果设计元类的目的是什么?