本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。
上一节,我们通过Clang
的方式得到了可以查看的编译文件,从而看到了Objc的类的本质——结构体。
本节则将通过Clang
和lldb
命令,探索一下,类这个结构体的结构。查看一下类的结构中的元素的意义,以及我们经常使用的成员变量、实例变量、属性、实例方法、类方法等等都在类的哪些结构中。把类的骨架探索出来。
一、类和元类的生成时期
有关于对象的生成时期,大家应该很清楚,对象都是在我们书写代码的时候由类申请的内存,从而生成了对象。
那么,类和元类是什么时候生成的呢?
答案 : 在编译期。
怎么验证?
还是用之前的Objc781可编译源码来尝试。
大家都知道,当我们在使用xcode的时候commond + B
可以编译但不执行我们的程序,并且可以在Products
文件夹里面找到编译完成的文件。
那么我们可以通过machOView这个软件来查看一下,commond + B
完成编译后,我们定义的类,还有系统中的元类,有没有出现。
machOView提取码 : spij
步骤 :
在main.m
文件中,我们创建一个JDPerosn
类。然后commond + B
。
#import
#import
@interface JDPerson : NSObject
@end
@implementation JDPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//0x00007ffffffffff8
JDPerson *person = [JDPerson alloc];
NSLog(@"Hello, World!");
}
return 0;
}
编译结果 :
很明显,没有stop running
的选项,证明我没有执行,有提示显示,证明我编译过了。
操作 :
看到那个Products
文件夹中的JDTest
了吗,那就是编译后的文件,直接拖入machOView
。
最终结果 :
直接找到类关联,看画红框的里面——JDPerson
。类出来了吧,那么元类呢?还记得isa
的指向吗?类也有isa
吧,指向的是元类吧,那证明了什么?证明了类是元类生出来的吧,类都出来了,元类能没有吗?
证明结束。
结论就是 :
类和元类是在编译期就生成的。
二、类是哪个结构体
即然要探索类的结构,那么我们就获取一个对象的类在系统环境下,它的结构是什么样子的?
所以先创建一个类对象,然后获取它的类。
#import
#import
@interface JDPerson : NSObject
@end
@implementation JDPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//0x00007ffffffffff8
JDPerson *person = [JDPerson alloc];
Class pClass = object_getClass(person);
NSLog(@"Hello, World!");
}
return 0;
}
通过rumtime的object_getClass
,我们可以获取一个对象的类在系统编译情况下的一个类的信息。并且返回一个Class
的对象。但是发现Class
对象不能点进去看,于是我把这个main.m
文件的编译文件打开看看,这个Class
到底是什么。
操作 :
进入到当前项目的main.m
所在的文件夹下,执行以下命令。
clang -rewrite-objc main.m -o main.cpp
这时候main.m
的文件夹下会出现经过clang
编译以后的main.cpp
文件。
结果 :
操作 :
打开main.cpp
,找到我们当前的这个JDPerson
。
结果 :
操作 :
原来是重命名的结构体,那objc_object
又是什么呢?继续搜索它。搜索struct objc_object
,看看有没有和Class
相关的。
于是找到了
结果 :
并且发现了一个结构,就是objc_object
这个结构体里面,唯一的元素是Class
,正是object_getClass
返回类型,而且发现了Class
也是一个重命名的结构体——objc_class
。
操作 :
即然找到了Class
的本质,那么就回到项目里面,直接在project
下搜索源码查看一下,这个struct objc_class
的结构。
结果 :
结论 :
原来
object_class
这个结构体是继承与objc_object
的。所以类的本质都是一个objc_object
结构体。
问题 :
- 为什么找
Class
- 为什么不是
runtime.h
中的objc_class
。 - 为什么不进
objc-runtime-old.h
的objc_class
。
操作 :
因为通过
object_getClass
,我们万物的类都可以用Class
接收。runtime.h
中的objc_class
后面明显写了弃用。不进
old
是因为有新的。看新不看旧。
这里就可以放另一个神图了,相信大家也都见过。从上面的分析一起看,就更清楚这个神图的走势了。
三、类的结构
1. 最普通的类的结构探索
看图6,解释一波这些结构体里面的元素都是什么
Class ISA
:isa
指针。为什么被注释了?因为它是客观存在的,但又不是objc_class
自己的,那它是谁的呢?明显来自于它的父类objc_object
。Class superclass
:objc_class
的父类。cache_t cache
: 以前缓存的指针和虚函数表class_data_bits_t bits
:class_rw_t *
加上自定义rr
/alloc
标志。
我们结合lldb
看一下这个类的实例的内存情况 :
前面两个还可以看出来一个是isa
的指针指向,一个是superClass
,第三个开始就找不到了。
那么后面的两个都是什么呢?下面的开始探索。
2. 类的属性存储
上面已经知道了一个普通的没有属性的类的结构,那么如果我们加上属性,属性会存储到哪里呢?
直接在JDPerson
里面定义一个myJDName
属性和一个JDHobby
成员变量。
@interface JDPerson : NSObject
{
NSString *JDHobby;
}
@property (nonatomic,copy) NSString *myJDName;
@end
我们来找一下,这两个在类中定义的变量都存储在类的哪个结构里面。
先来一波猜测,因为上面我们已经看到了类的结构了。
第一个,isa
是不可能的,po
地址都告诉我们了,人家指针里面存的是指向的地址,指向的是元类。
第二个,superClass
。也po
出来了,是NSObject
。
第三个cache_t
开始po
不出来了,但是第三个地址也说明了,那是缓存。
所以,应该就是在第四个变量中class_data_bits_t bits
字节中。
但是第三个开始都读不出来了呀,而且图9都看到了,第四个地址是0啊,根本po
不出东西呀。
那么我们改变方法,即然直接po
不出东西,我们就计算指针,直接po
第四个的内存地址。
方法 :
- 已知条件:
-
Class
对象的首地址 :
isa
指针的大小 : 都说了是指针,所以,8字节
。superclass
的大小,这是一个Class
的变量吧,进去Class
看,这是结构体指针呀,typedef struct objc_class *Class;
也是8字节
。
其实直接根据0x1000081c0
和0x1000081d0
就可以了,前两个都说了是isa
和superClass
,那第三个就是cache_t
呗。
思路 :
cache_t cache
的首地址 + cache_t cache
的内存大小 = class_data_bits_t bits
的首地址
拿到首地址,我们就拿到了它的对象,再从源码中找到class_data_bits_t
的结构,根据这个结构,去找到属性在哪里,是不是就ok了?
所以思路上的拦路虎 : cache_t cache
是多大?
2.1 搞清楚cache_t cache
多大
点进去看cache_t
的结构。
struct cache_t
多余的我就不贴了,反正是个结构体,结构体的大小等于结构体里面的元素的大小的总和,所以我要找属于这个结构体自己的内部元素。
所以,刨除所有的static
,因为static
静态变量吧,人家在自己的全局区里面。
剩下的如下 :
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
}
public
里面不要管了吧,那是人家的函数。
先说这句宏#if __LP64__
,64位系统都会有,所以这个宏就当没有就好了。
然后就有一个很友好的处理,它的宏判断if
里面的大小是一模一样的。
if
里面 :
: 结构体指针,_buckets 8字节
,进_mask mask_t
看一下是typedef uint32_t mask_t;
: 就是无符号整型
,4字节
。
两个elif
里面 :
uintptr_t
: 看到ptr
这三个字母就知道是指针了,所以,8字节
。mask_t
跟上面一样,4字节
。
再看,下面两个不需要判断的 :
-
uint16_t
: 这是unsigned short
,所以,2
字节。
结论 :
cache_t cache
的内存大小 = 8 + 4 + 2 + 2 = 16
2.2 获取class_data_bits_t bits
的数据
直接计算吧,16进制加减法,上面都说清了。用0x1000081c0
的话,就加32
,用0x1000081d0
的话,就加16
。结果是 : 0x1000081e0
。
所以,直接去读这个地址上的内存。
操作 :
lldb
输入 : p (class_data_bits_t *)0x1000081e0
结果 :
为什么要加(class_data_bits_t *)
,因为我们通过逻辑上的分析,已经确认了这里就是class_data_bits_t
类型的指针。
这时候,我们已经拿到了objc_class
里面的bits
的指针,并且是class_data_bits_t
这个类型的指针。
然后我们进入class_data_bits_t
,看看怎么能拿到它的数据。
函数中的第一个就是data()
函数,而且知道了返回的data()
类型是class_rw_t
。
操作 :
lldb
输入 : p $1->data()
结果 :
我们查看bits.data()
里面有什么东西。
操作 :
lldb
输入 : p *$2
结果 :
2.3 类的属性所在
通过2.2的查看bits.data()
我们知道了类的数据信息都存放在了class_rw_t
里面,但是属性在哪里,还没找到。
可是看图14,我们可以知道的是,这些属性里面,只有Value
可能存在着属性,因为其他的元素都用结果表明了它们不可能存储着类的成员变量和属性。
这个时候其实已经很难探索了,因为找不到下一步的思路,只能站在上帝视角先把已知的知识拿来用,继而验证它的正确性,得到我们的结果。
这里其实有一步试错,因为在class_rw_t
里面我找到了property_array_t
类型的数组,在里面找到了list()
函数,但是拿到的只有定义的成员变量JDHobby
,没有属性以及属性产生的myJDName
的成员变量。
我们知道的是类的属性存储在ro
的ivars
里面,那就验证以下,是不是这样的。
操作 :
拿到class_rw_t
变量的ro
地址。
打印ro
中的数据信息 :
获取ivar
的内容 :
这里,我们发现,我们取ivar_list_t
的首地址的内容,在这个ivar_list_t
里面,有我们的成员变量JDHobby
,并且ivar_list_t
应该是一片连续的内存空间,而且count = 2
,说明有两个元素,所以直接get(0)
和get(1)
看一下。
这时候,我们找到了我们的一个成员变量,一个属性,存储在ivar_t
类型的数据结构里面。那么这个ivar_t
又是什么呢?
这也就是我们常说的Ivars
。
到这里,我们总结一下,类的属性在类中的实际结构 :
结论 :
- 类中的成员变量和属性都会存储在
class_ro_t
中的ivar_list_t
的ivars
地址上,作为ivar_list_t
的元素,成员变量和属性的类型都统一为ivar_t
。
问 :
成员变量不是在class_rw_t
中的property_array_t
中可以找到吗?为什么不是存储在class_rw_t
的property_array_t
中?
答 :
这个后面会说,大体的原因是因为class_rw_t
在编译前期是没有的,只有class_ro_t
。class_rw_t
是在运行时态的加持下,在运行时才被copy了class_ro_t
出来的。
2. 类的方法存储
2.1 类中只有属性没有方法时
经历了类的属性存储的探索,类的方法的探索就变得很容易了。
先看一下,最开始我们没有添加任何的方法的时候,class_ro_t
里面出现了baseMethodList
这样的一个元素,那看一下,它是什么。
操作 :
结果出来了3个方法???但是我明明没有定义呀,全部打印出来看。
这就很理解了,全都是属性myJDName
的方法,setter
、getter
还有一个c++
的析构函数。
2.2 当类中自定义了方法时
@interface JDPerson : NSObject
{
NSString *JDHobby;
}
@property (nonatomic,copy) NSString *myJDName;
- (void)studyWork;
+ (void)eatFood;
@end
@implementation JDPerson
- (void)studyWork {
}
+ (void)eatFood {
}
@end
现在定义一个实例方法,一个类方法。
我们继续探索属性的时候的思路,这次我就直接贴图片了。需要说明的会下面说明。
只有4个方法在class_ro_t
的method_list_t * baseMethodList
里面。
唯独缺少类方法。
截止到这里,我们可以确定
类的实例方法存储在
class_ro_t
的method_list_t * baseMethodList
里面。
2.3类方法所在
通过前几节的总结,应该可以得出这样的一个猜测 :
根据对象的实例方法存储在类中,那么类的类方法是不是元类的实例方法?万物皆对象啊,所以我们可以去找元类看看,元类里面有没有类方法。
-
找
JDPerson
的类地址
利用
ISA_MASK
计算JDPerson
元类的首地址
- 把指针从首地址移动
32
字节,变成0x00000001000081e8
,移动到JDPerson
元类的class_data_bits_t bits
上面。
- 调用
objc_class
的data
函数,获取数据的指针,并读取指针指向的内容。
- 调用
class_rw_t
的ro()
函数,拿到类的属性和方法编译后的数据存储地址指针,class_ro_t
指针,并打印指针指向的内容。
- 拿到
JDPerson
元类的method_list_t *baseMethodList
这里就发现了类方法eatFood
。
结论 :
类方法
eatFood
放在了JDPerson
的元类里面。
四、类的结构的总结
Objc中的类,本质上都是结构体
objc_object
。Class
类的本质也是objc_object
,只不过是它的子类objc_class
。Objc中的成员变量和属性全部都存储在当前类的
class_ro_t
中的ivar_list_t
类型变量ivars
里面。并且它们统一的类型都是ivar_t
。Objc中的实例方法存储在当前类的
class_ro_t
中的method_list_t
类型的变量baseMethodList
里面。Objc中的类方法存储在当前类的元类的
class_ro_t
中的method_list_t
类型的变量baseMethodList
里面。
下面上几张图,来看一下类的结构