类的分析
- 准备工作,我们先创建两个类继承
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命令得到
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 _buckets;// 是一个结构体指针类型,占8字节
explicit_atomic _mask;//是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _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 = {
= {
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 = {
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 = {
= {
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 = {
entsizeAndFlags = 16
count = 1
first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
}
}
总结:类的实例方法和类的属性都存在bits中,我们发现类的类方法和类的成员变量却没有打印,我们可以思考一下,它们存在哪里呢?类的类方法会不会存在元类里面呢?下一节我们接着探索一下这个内容。