类
通常熟知定义
1、关于类
类的设计只关注三个东西:类名、属性和方法
拥有相同属性和行为的对象都可以抽象为一个类。类名是标识符的一种,需要符合规范,通常类名的第一个字母大写,且不能有下划线,如果有多个单词使用驼峰原则。在对方法进行类的划分中,一般采取的做法是谁最熟悉这个方法那么就把这个方法划分给谁。在OC中,对象对方法的调用称为消息机制,即向既定的对象发送了什么消息。
2、简单内存分析
类创建对象,每个对象在内存中都占据一定的存储空间,每个对象都有一份属于自己的单独的成员变量,所有的对象公用类的成员方法,方法在整个内存中只有一份,类本身在内存中占据一份存储空间,类的方法存储于此。
每个对象内部都默认有一个isa指针指向这个对象所使用的类。
[p eat];表示给p所指向的对象发送一条eat消息,调用对象的eat方法,此时对象会顺着内部的isa指针找到存储于类中的方法,执行。
isa是对象中的隐藏指针,指向创建这个对象的类。
类的结构底层分析
1、类的结构
先来通过源码看一下类的结构到底是设么样的。
struct objc_class : objc_object {
// Class ISA; //8 占用的空间,后面进行内存偏移读取bits的时候要用
Class superclass; //8
cache_t cache; //16 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
下边是一些类似 setData 的方法就是省略了
void setData(class_rw_t *newData) {
bits.setData(newData);
}
。。。。。。。
看完源码我们知道,类里面有这4个属性。
- Class isa
isa 隐藏属性,isa指向类的元类(meta class)继承自父类 objc_object。
isa的指针是关联对象和类
struct objc_object {
Class Nonnull isa OBJC_ISA_AVAILABILITY;
}
2 . Class superclass
父类 也就是指向继承类
3 . cache_t cache
从名字可以猜出是缓存(缓存的方法,是一个二维数组后面分析cache_t的时候具体分析)
struct cache_t {
struct bucket_t *_buckets; //8
mask_t _mask; //4
mask_t _occupied; //4
_buckets 是指针类型所以长度是8位,mask_t 查看源码可以知道是 int32 类型是4位
typedef uint32_t mask_t;
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
读取源码可看出cache 是存的方法,通过key value找到对应方法的imp
4 . class_data_bits_t bits
是个结构体,从整体分析来说这个地方是存储数据的但是具体是什么需要验证,继续分析
分析探索
1、属性
(1)、创建一个LGPerson的类,添加属性和方法
扩展知识:.m中方法没有实现
- 如果没有调用,编译时和运行时均不会有问题
- 如果调用了,编译时不会报错,运行时会崩溃
@interface LGPerson : NSObject{
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
+ (void)sayHappy;
@end
(2)、通过内存偏移得到bits
按照内存对齐原则,我们用x/4gx打印得到的首地址偏移32个字节,就应该能得出bits的内容 ,也就是bits的理论地址。这里我们强转一下(强转成class_data_bits_t *),再次打印,终于打印出class_data_bits_t的结构体
OC 中一般调用成员变量都是用 -> 例如: p * $4->ro 有些时候会有区分
struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache; // 16 不是8 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
(3)、class_rw_t
通过上边打印可以看到 $7 的类型是class_rw_t, class_rw_t为类中存储属性和方法的地方,看一下class_rw_t 的实现,返回的是bits.data(),我们这里调用一下data方法之后得出一个class_rw_t类型的指针,直接取值 p *$7,结果如下
打印得到的 (class_rw_t)$8
(4)、properties
使用p命令查看结果中properties的值,得到类型property_array_t(在源码的objc-runtime-new.h中有其定义),为一个二维数组,继续探索其内部list,进行 p $10.list
class property_array_t :
public list_array_tt<property_t, property_list_t>
{
typedef list_array_tt<property_t, property_list_t> Super;
public:
property_array_t duplicate() {
return Super::duplicate<property_array_t>();
}
};
(5)、property_list_t
得到property_list_t类型(在源码的objc-runtime-new.h中有其定义)的$11,继承自entsize_list_tt
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
下边的不重要,两个int32类型肯定不可能是存储属性的,所以继续查看first
(6)、first
查看property_list_t 继承与 entsize_list_tt 在其内部发现first方法,尝试打印p$11->first,最后找到了属性name
说明类的属性是在 class_data_bits_t bits 里面存储的
2、成员变量
1、class_rw_t
找到了属性的存储之后,没有看到成员变量,既然属性是在 class_rw_t 中存的。我们之前应该遇到过如下情况
@interface LGPerson : NSObject{
NSString *hobby;
NSString *_nickName;
}
@property (nonatomic, copy) NSString *nickName;
如果属性(nickName)和成员变量带下划线(_nickName)同名的话,那么只会有一个存在。而且属性nickName可以用self.nickName 和 _nickName。所以猜测肯定是属性在底层生成了一个待下划线的成员变量(底层分析后面会进行分析,这里不扩展)。
看一下class_rw_t 的底层源码,感觉可定是在class_ro_t * ro 中存储
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
到此我们可以知道属性和成员变量的存储地方分别是
class_rw_t
properties 属性的存储,
ro 成员变量的存储
通过名字可以判断 methods 应该存放方法继续验证
3、类的方法存储
(1)、实例方法
$2 是 class_rw_t ,读取 methods 。通过查看源码可以知道返回的存储的格式是
SEL 方法名 typer 方法类型 imp 方法对应实现
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
省略。。。。。。。
};
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
。。。。。。。。。
(lldb) p $2.methods
(method_array_t) $12 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}
(lldb) p $12.list
(method_list_t *) $13 = 0x0000000100002240
(lldb) p *$13
(method_list_t) $14 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
}
}
(lldb)
通过查看查看 methods 里面之后只有我们创建的实例方法和属性的set、get方法。那么类方法在哪呢?
重新分析一下 objc_object
1)、superclass 我们调用类方法的时候是 例如:[LGPerson sayHappy];这个样子调用的,父类名字跟子类名称都不一样,很明显不可能是在父类中。
2)、cache 看了cache_t 的源码,这个地方确实是更方法有关系,但是名称是缓存应该不是。
3)、bits bits我们刚看完 class_rw_t 的
properties 属性的存储,
ro 成员变量的存储
methods 存放类的实例方法
4)、isa 那么只剩下isa。前面我们知道isa是指向一个同名的元类,调用类方法也是用的类名,那么很有可能就是在元类中。探索一下
(2)、类方法的存储
同上x/4gx 打印出isa 的地址,然后获取元类
class_data_bits_t, ->data() - class_rw_t - methods,得到 list 数组,最后找到了我们定义的类方法sayHappy流程和查找类的实例方法一样,所以直接看结果
(lldb) p $13.list
(method_list_t *) $14 = 0x00000001000021d8
(lldb) p *$14
(method_list_t) $15 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayHappy"
types = 0x0000000100001f8b "v16@0:8"
imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
}
}
}
总结
- 类的继承和isa指向见下图
- 类 – 父类 – 基类(NSObject class)- nil
- 类 – 元类 – 元类的父类 – 基类的元类(NSObject meta class)
- 其中类的属性和成员变量都存放在类的class_rw_t结构体中
- class_rw_t
- properties 属性的存储,
- ro 成员变量的存储
- methods 类的实例方法
- class_rw_t
- 属性的定义,还伴随着成员变量以及其getter和setter的自动生成
- 类的类方法,则以实例方法的形式,存放在元类中,而元类又是继承自NSObject,形成一个闭环
扩展
指针
-
int a;
//这是一个普通的整型变量
-
int *p;
//首先从P
处开始,先与*
结合,所以说明P
是一个指针
,然后再与int
结合,说明指针所指向的内容的类型为int型
,所以P
是一个返回整型数据的指针