类的分析
- 准备工作,我们先创建两个类继承
NSObject的LGPerson和继承LGPerson的LGStudent:
//.h文件
@interface LGPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *lg_name;
- (void)sayHello;
+ (void)sayBye;
@end
//.m文件
@implementation LGPerson
- (void)sayHello
{
}
+ (void)sayBye
{
}
@end
- 在
main.m文件中如下设置
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
LGStudent *student = [LGStudent alloc];
NSLog(@"Hello, World! %@ - %@",person,student);
}
return 0;
}
-
类结构的分析
在mian.m文件LGStudent *student = [LGStudent alloc];处打上断点运行一下项目工程,使用lldb进行调试
lldb调试过程及输出结果- 首先我们可以根据lldb命令得到
person的内存分布,我们知道0x001d8001000022dd是person的内存指针,接着使用这个指针&0x00007ffffffffff8ULL,可以获取类的相关信息$3 = 0x00000001000022d8。 - 接着
po 0x00000001000022d8来打印类的信息,我们发现结果为LGPerson; -
x/4gx 0x00000001000022d8,来打印LGPerson的内存情况,我们同样的可以拿到类的isa指针地址0x00000001000022b0; - 下一步,
p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL,我们来获取类的信息,我们得到$5 = 0x00000001000022b0类的指针地址; - 下一步:通过
po 0x00000001000022b0,发现结果还是LGPerson,这是为什么呢?这里先简单说一下这个LGPerson是元类;下面小节在详细说明元类的问题。
- 首先我们可以根据lldb命令得到
二、元类,主要有以下几点说明:
我们都知道
对象的isa是指向类,类其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类。元类是系统设置的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于`元类。元类是类对象的类,每个类都有一个独一无二的元类用来存储 类方法的相关信息。元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称。
总结:由上图的打印结果我们可以得出如下结论:对象 --> 类 --> 元类 --> NSobject, NSObject 指向自身。
三、isa走位 & 继承关系
根据上面的探索以及各种验证,对象、类、元类、根元类的关系如下图所示

isa的走向有以下几点说明:
-
实例对象(Instance of Subclass)的isa指向类(class) -
类对象(class) isa指向元类(Meta class) -
元类(Meta class)的isa指向根元类(Root metal class) -
根元类(Root metal class) 的isa指向它自己本身,形成闭环,这里的根元类就是NSObject。
superclass(即继承关系)的走向也有以下几点说明:
-
类(subClass)继承自父类(superClass) -
父类(superClass)继承自根类(RootClass),此时的根类是指NSObject。 -
根类 继承自 nil,所以根类即NSObject可以理解为万物起源,即无中生有。 -
子类的元类(metal SubClass)继承自父类的元类(metal SuperClass)。 -
父类的元类(metal SuperClass)继承自根元类(Root metal Class。 -
根元类(Root metal Class)继承于根类(Root class),此时的根类是指NSObject。
举例说明

isa 走位链
-
student的isa走位链:student(子类对象) --> LGStudent (子类)--> LGStudent(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-person的isa走位图:person(父类对象) --> CJLPerson (父类)--> CJLPerson(父元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
superclass走位链
- 类的继承关系链:
LGStudent(子类) --> CJLPerson(父类) --> NSObject(根类)--> nil - 元类的继承关系链:
LGStudent(子元类) --> CJLPerson(父元类) --> NSObject(根元类)--> NSObject(根类)--> nil
四、objc_class & objc_object
在分析objc_class & objc_object我们先引入一个问题,为什么类和对象都有isa属性呢?
我们先将main.m文件编译为main.cpp来分析一下这个问题。我们根据clang编译的c++源码可以看出NSObject的底层编译是NSObject_IMPL结构体,并且对象含有Class isa,代码如下
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
下面我们通过在objc4源码中搜索来探索objc_class & objc_object
-
objc_class在源码中搜索到两种相关源码 - 第一种已经不再使用了,并且和我们使用
Clang编译后的一样
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
} OBJC2_UNAVAILABLE;
另外一种,我们选择主要的代码进行展示如下
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
};
我们发现objc_class继承自objc_object,下面我们再看看objc_object源码,
-
objc_object源码
struct objc_object {
private:
isa_t isa;
};
总结:
- 我们根据源码发现
objc_object结构体定义了isa作为它的一个属性,objc_class继承自objc_object,所以objc_class也拥有了isa属性。 -
mian.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性。 -
NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的特性,所以对象都有一个 isa,isa表示指向,来自于当前的objc_object。 - 所以所有的对象都是以
objc_object为模板继承过来的。 - 因为
对象是 来自NSObject(OC),但是真正到底层的 是一个objc_object(C/C++)的结构体类型,所以objc_object与对象的关系是继承关系。
objc_class、objc_object、isa、object、NSObject等的整体的关系,如下图所示

类的方法
我们根据下面类的底层实现源码来探索一下,类的实例方法存储在哪里,来具体探索一下类的结构是怎样的?
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
// ......部分代码不在展示
}
- 在探索之前我们需要补充一下知识,关于
内存偏移,我们先使用实例说明一下这个内存偏移;
【普通指针】
//普通指针
int a = 10; //变量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
输出的结果
我们可以从控制台看出,a和b两个指针指向了同一片的存储着10的空间。
a、b都指向10,但是a、b的地址``不一样,这是一种拷贝,属于值拷贝,也称为浅拷贝a,b的地址之间相差4个字节,这取决于a、b的类型
【对象指针】
LGPerson *p1 = [LGPerson alloc]; // p1 是指针
LGPerson *p2 = [LGPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
输出结果

p1、p2是指针,p1是 指向[LGPerson alloc]创建的空间地址,即内存地址,p2同理&p1、&p2是 指向p1、p2对象指针的地址,这个指针 就是 二级指针
【数组指针】
//数组指针
int c[4] = {1, 2, 3, 4};
int *d = c;
NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
输出结果
-
&c和&c[0]都是取这个数组的首地址,所以``数组名等同于首地址; -
&c与&c[1]相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型可以通过首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数等于偏移量 * 数据类型字节数`;
计算类结构的内存大小
通过上面类结构的源码我们来计算一下类的大小
-isa属性:继承自objc_object的isa,占 8字节;
-
superclass属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节 -
cache属性:是cache_t结构体类型,我们应该按照计算结构体内存大小的规则来计算,而结构体指针才是8字节;下面代码可以计算出cache占16字节
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;// 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask;//是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
//省略部分代码
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
//省略部分代码
}
-
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits`;
类结构中方法分析流程
(lldb) p/x LGPerson.class
(Class) $0 = 0x00000001000022f0 LGPerson
(lldb) //平移32个字节
error: '//平移32个字节' is not a valid command.
(lldb) p/x 0x00000001000022f0 + 32
(long) $1 = 0x0000000100002310
(lldb) 获取bit
error: '获取bit' is not a valid command.
(lldb) p (class_data_bits_t *)0x0000000100002310
(class_data_bits_t *) $2 = 0x0000000100002310
(lldb) 通过结构体的data()来获取bits
error: '通过结构体的data()来获取bits' is not a valid command.
(lldb) p $2 -> data()
(class_rw_t *) $3 = 0x0000000102018a20
(lldb) 获取methods()
error: '获取methods()' is not a valid command.
(lldb) p $3.methods()
(const method_array_t) $4 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002180
arrayAndFlag = 4294975872
}
}
}
Fix-it applied, fixed expression was:
$3->methods()
(lldb) 获取list
error: '获取list' is not a valid command.
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002180
(lldb) 打印list内容
error: '打印list内容' is not a valid command.
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100000f85 "v16@0:8"
imp = 0x0000000100000d80 (KCObjc`-[LGPerson sayHello])
}
}
}
(lldb)

- 关于类的属性
(lldb) p $3.properties()
(const property_array_t) $7 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100002230
arrayAndFlag = 4294976048
}
}
}
Fix-it applied, fixed expression was:
$3->properties()
(lldb) p $7.list
(property_list_t *const) $8 = 0x0000000100002230
(lldb) p *$8
(property_list_t) $9 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
}
}
总结:类的实例方法和类的属性都存在bits中,我们发现类的类方法和类的成员变量却没有打印,我们可以思考一下,它们存在哪里呢?类的类方法会不会存在元类里面呢?下一节我们接着探索一下这个内容。
