类与对象的关系
关于类和对象的关系,对于它的理解可能停留在对象是类创建(alloc,new)
出来的这种很浅层的关系上,如果要深挖出背后的秘密,还是得从地址和内存入手,接下来就一步步探索。
step1:
创建一个继承于NSObject
的LBHPerson
类
//.h 文件
@interface LBHPerson : NSObject
@end
//.m 文件
#import "LBHPerson.h"
@implementation LBHPerson
@end
step2:
在main.m
在导入#import "LBHPerson.h"
头文件,并创建一个person
对象,并打上断点
step3:
通过lldb
输出person
相关信息
lldb命令在内存对齐 一文中讲过
通过isa
获取获取类信息
在isa与类关联的原理 一文中讲过
0x001d8001000081d1 & 0x00007ffffffffff8ULL
继续通过
step3
中类的isa
获取类信息得到的是什么呢?
step4:
通过类的isa
获取对应的类(元类)
我们会发现po 0x0000000105d84c70
与po 0x0000000105d84c48
得到的结果是一样的,都是LBHPerson
,两个不同的地址指向同一个类,0x0000000105d84c48
是类0x0000000105d84c70
的isa
通过获取类信息
得到的,我们称之为元类
。
元类的说明
我们知道 对象的isa
指向类
,其实类
也是一个对象
,可以称为类对象
,其isa
的位域指向苹果定义的元类
-
元类
是系统
给的,其定义
和创建
都是由编译器
完成,在这个过程中,类的归属来自于元类
-
元类
是类对象
的类
,每个类都有一个独一无二的元类用来存储 类方法的相关信息。 -
元类
本身是没有名称
的,由于与类相关联,所以使用了同类名一样的名称
step5:
继续通过元类
的isa
指向 根元类
元类
的isa
指向根元类NSObject
step6:
根元类
的isa
指向?
根元类
的isa
指向自己
可以得出一个关系链:对象
--> 类
--> 元类
--> NSObject
, NSObject 指向自身
一个类在内存中会不会存在多份?
以LBHPerson
为例
通过对象person
的isa
获取到的类信息地址
与LBHPerson.class
获取到的地址是相同的,这意味一个类在内存中不会存在多份,这个结论是否正确呢?我们来验证一下:
way1:
通过几种获取类对象的方式
Class class1 = [LBHPerson class];
Class class2 = [LBHPerson alloc].class;
Class class3 = object_getClass([LBHPerson alloc]);
NSLog(@"\n%p-\n%p-\n%p", class1, class2, class3);
运行结果
三种方式获取到的类对象地址相同,即类在内存中只有一份
isa走位与继承关系图
isa关系
-
示例对象
的isa指针类对象
-
类对象
的isa指向元类对象
-
元类对象
的isa指向根元类
-
根元类
的isa指向它自己本身
,从而形成了闭环
继承关系
-
类对象的继承关系
-
类
继承于它的父类
-
父类
继承它的父类
... - 直到找到
根类
,也就是NSObject
-
NSObject
则继承于nil
,这也就是所有的根源,即无中生有
-
-
元类的继承关系
-
子类的元类
继承与父类的元类
-
父类的元类
继承它的父类的元类
... - 直到找到
根元类
- 而
根元类
则是继承于NSObject
-
【注意】
实例对象之间没有继承关系,类之间有继承关系
举例说明
新建一个LBHTeacher
类继承与LBHPerson
,在main
函数中实例化
//.h
@interface LBHTeacher : LBHPerson
@end
//.m
#import "LBHTeacher.h"
@implementation LBHTeacher
@end
//main
LBHPerson *person = [LBHPerson alloc];
LBHTeacher *teacher = [LBHTeacher alloc];
它们对应的isa
走位和继承图
-
isa 走位链(两条)
teacher的isa走位链:
teacher(子类对象)
-->LBHTeacher(子类)
-->LBHTeacher(子元类)
-->NSObject(根元类)
-->NSObject(根元类,即自己)
person的isa走位链:
person(父类对象)
-->LBHPerson(父类)
-->LBHPerson(父元类)
-->NSObject(根元类)
-->NSObject(根元类,即自己)
-
superclass继承链(两条)
类的继承关系链:
LBHTeacher(子类)
-->LBHPerson(父类)
-->NSObject(根类)
-->nil
元类的继承关系链:
LBHTeacher(子元类)
-->LBHPerson(父元类)
-->NSObject(根元类)
-->NSObject(根类)
-->nil
对象的本质
对象的本质其实就是结构体,而编译到底层会发现包含一个objc_class
结构体类型的变量
为什么
对象
和类
都有isa
属性呢?
在在isa与类关联的原理 一文中使用clang
编译过main.m
文件,从编译后的c++文件中可以看到如下c++源码:
//LGPerson的底层编译
struct LBHPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
在c++底层OC层面的继承
实际上是子类结构体
包含一个父类结构体
作为成员变量
我们对c++源码进一步进行分析
step1:
在objc4源码中 搜索objc_class
通过源码可以看出 objc_class
其实就是继承自 objc_object
。
step2:
在objc4源码中 搜索objc_object
objc_class
与objc_object
有什么关系?
- 结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性 - mian.cpp底层编译文件中,
NSObject
中的isa
在底层是由Class
定义的,其中class
的底层编码来自objc_class
类型,所以NSObject
也拥有了isa
属性
objc_object
与对象
的关系 (百度面试题)
- 对象继承于
objc_object
,而objc_class
继承于objc_object
,所以对象继承与objc_object
总结
- 所有的
对象
+类
+元类
都有isa
属性 - 所有的
对象
都是由objc_object
继承来的 - 简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:- 所有以
objc_object
为模板 创建的对象
,都有isa
属性 - 所有以
objc_class
为模板,创建的类
,都有isa
属性
- 所有以
- 在结构层面可以通俗的理解为
上层OC
与底层
的对接:- 下层是通过
结构体
定义的模板
,例如objc_class、objc_object - 上层是通过底层的模板创建的 一些类型,例如LBHPerson
- 下层是通过
objc_class
、objc_object
、isa
、object
、NSObject
的关系如图所示:
类结构的分析
探索类结构
时,我们并不是很清楚里面有些什么,但是我们可以通过类
得到一个首地址
,然后通过地址平移
去获取里面所有的值
。
前面我们已经在objc4找到objc_class
源码
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; //Class 类型 8字节
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//....方法部分省略,未贴出
}
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
对应objc_class
的结构图
-
isa
属性:继承自objc_object
的isa
,是一个指针,占8字节
-
superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针,占8字节
-
cache
属性:从类型cache_t
目前无法得知,而cache_t
是一个结构体类型,结构体的内存大小
需要根据内部的属性来确定
,而结构体指针才是8字节
-
bits
属性:只有首地址
经过上面3个属性的内存大小总和的平移
,才能获取到bits
计算cache大小
进入cache_t
的定义
// 结构体字节大小,看里面的成员变量,而大部分都是 static 和方法都不存在结构体里面
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;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
// 方法代码过多,自动省略
...
};
可以得到cache
占16字节
bits
bits的位置
objc_class
中前三个属性的大小为:8+8+16=32,所以想获取bits中的信息可以通过首地址偏移32字节获得。
获取bits内容
先看下bits的结构
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
// 代码过多,自动省略
...
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// 代码过多,自动省略
...
};
class_rw_t
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
// 省略过多的代码
...
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->methods;
} else {
return method_array_t{v.get()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->properties;
} else {
return property_array_t{v.get()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->protocols;
} else {
return protocol_array_t{v.get()->baseProtocols};
}
}
};
可以看出,我们可以通过class_rw_t
结构体中提供的methods()
、properties()
、protocols()
获取到对应的方法、属性和协议。
既然知道可以获取到对应的方法、属性和协议,那就给LBHPerson
类添加一些方法和属性
//.h
{
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
- (void)test1;
+ (void)test2;
//.m
- (void)test1
{
NSLog(@"%s",__func__);
}
+ (void)test2
{
NSLog(@"%s",__func__);
}
接下来我们通过lldb
调试来查找对应的class_data_bist_t bits
,查看相应的信息。
- 其中的
data()
获取数据,是由objc_class
提供的方法
获取属性列表
-
p $3.properties()
命令中的properties
方法是由class_rw_t
提供的,方法中返回的实际类型为property_array_t
- 由于
list
的类型是property_list_t
,是一个指针,所以通过p *$5
获取内存中的信息,同时也证明bits中存储了property_list
,即属性列表
在获取第二个属性时出现报错,数组越界,我们定义的
nickName
呢?
这类补充下成员变量、实例变量、属性的区别?
成员变量
:在{ }
中所声明的变量都是成员变量(实例变量是一种特殊的成员变量)
实例变量
:成员变量的一种,由类声明的对象
属性
:@property
修饰,编译器会自动生成setter和getter方法
获取成员变量
在class_rw_t
结构体中有个ro()
可以获取成员变量
通过lldb
调试来查找成员变量
class_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro
的首地址
平移48字节
struct class_ro_t {
uint32_t flags; //4
uint32_t instanceStart;//4
uint32_t instanceSize;//4
#ifdef __LP64__
uint32_t reserved; //4
#endif
const uint8_t * ivarLayout; //8
const char * name; //1 ? 8
method_list_t * baseMethodList; // 8
protocol_list_t * baseProtocols; // 8
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//方法省略
}
【总结】
- 通过
@property
定义的属性,也会存储在bits
属性中,通过bits
-->data()
-->properties()
-->list
获取属性列表,其中只包含属性 - 通过
{}
定义的成员变量,会存储在类的bits
属性中,通过bits
-->data()
-->ro()
-->ivars
获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量
方法列表 methods()
通过lldb
调试来查找方法列表
通过打印的count = 4
可知,存储了4个方法,可以通过p $7.get(i)
内存偏移的方式获取单个方法,i
的范围是0-3
我们定义的类方法
+ (void)test2
好像遍历并没有找到,它存储在哪呢?
类方法的存储
在methods list
中并没有找到类方法
, 那类方法存储在哪里?下面我们来分析下:
前面有分析元类
,元类
是用来存储类
的相关信息
的,我们大胆猜测一下:类方法存储在元类的bits中呢?通过lldb
命令来验证我们的猜测:
根据打印我们可以知道猜测是正确的,类方法存储在元类中。
【总结】
-
类
的实例方法
存储在类的bits
属性中,通过bits
-->methods()
-->list
获取实例方法列表; -
类
的类方法
存储在元类的bits
属性中,通过元类bits
-->methods()
-->list
获取类方法列表。
类的结构功能
名称 | 类型 | 功能 |
---|---|---|
isa | 指针 | 指向元类的指针 |
superclass | 指针 | 指向当前类的父类 |
cache | 结构体 | 用于缓存方法的,用于加速方法的调用 |
bits | 结构体 | 存储类的方法、属性、协议等信息的地方 |