这是我参与更文挑战的第6天,活动详情查看: 更文挑战
引言
在iOS开发中几乎每一个对象都是类的实例,今天我们就来分析一下类在内存中是以什么样的结构存在的;
实例对象、Class、MetaClass
Class VS MetaClass的区别
- 首先查看以下调试代码
#import "GCWorker.h"
@interface GCPerson : NSObject
@end
@implementation GCPerson
@end
int main(int argc, const char * argv[]) {
GCPerson *person = [[GCPerson alloc] init];
NSLog(@"%@",person);
return 0;
}
- 通过LLDB进行调试,查看isa的相关信息
(lldb) x/4gx person
0x10303da70: 0x001d800100008179 0x0000000000000000
0x10303da80: 0x72626956534e5b2d 0x74696c7053746e61
(lldb) p/x 0x001d800100008179 & 0x00007ffffffffff8
(long) $1 = 0x0000000100008178
(lldb) po 0x0000000100008178
GCPerson
(lldb) x/4gx 0x0000000100008178
0x100008178: 0x0000000100008150 0x00007fff90c33118
0x100008188: 0x00000001030498f0 0x000680100000000f
(lldb) p/x 0x0000000100008150 & 0x00007ffffffffff8
(long) $3 = 0x0000000100008150
(lldb) po 0x0000000100008150
GCPerson
(lldb)
- 通过
实例对象获得实例对象的isa,通过isa & isa_mask获得Class对象; - 通过
Class对象获得Class对象的isa,通过isa & isa_mask获得另一个Class对象; - 步骤1和步骤2得到的地址分别是
0x0000000100008150和0x0000000100008178,但都指向GCPerson,我们猜测,实例对象的isa指向Class,Class的isa指向的MetaClass; - 通过machoview 查看
__DATA段中的__objc_classrefs,可以看到当前macho文件中的Class信息;查看__DATA段中的__objc_classlist,可以看到当前macho文件中的Class对象信息; 在symbol Tables中的*Symbols中可以看到Class和Metaclass信息如下图所示;最终验证我们的猜测是正确的;
Class和MetaClass的数量
class,MetaClass会不会和实例对象一样可以无限开辟?经过以下代码验证实际上不是的,class和metaclass是编译期器生成的,在启动阶段的pre-main阶段通过dyld加载进内存的,只有一份;如下图所示
总结
实例对象的isa指向Class,Class的isa指向MetaClass;实例对象可以无限开辟,Class和MetaClass只有一份;
isa走位 和superClass继承链
isa走位
- 运行以下代码
GCPerson *person = [[GCPerson alloc] init];
NSLog(@"%@",person);
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%@",obj);
- 在LLDB中进行以下调试
自定义对象的isa走向
NSObject的isa走向
- 经过分析,
自定义对象相关的isa的走位如下
自定义实例对象的isa指向自定义Class;自定义Class的isa指向自定义MetaClass;自定义MetaClass的isa指向NSObject的MetaClass;
- 经过分析
NSObject相关的isa走位如下
NSObject实例对象的isa指向NSObject的Class;NSObject的class指向NSObject的MetaClass;NSObject的MetaClass指向自身;
- 结果如图所示
superClass继承关系
- 执行以下调试代码
void testNSObject(void) {
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// GCPerson元类
Class pMetaClass = object_getClass(GCPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);
// GCTeacher -> GCPerson -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(GCPerson.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@ - %p",tsuperClass,tsuperClass);
// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}
- 查看输出信息
0x102c04ae0 实例对象
0x7fff90c33118 类
0x7fff90c330f0 元类
0x7fff90c330f0 根元类
0x7fff90c330f0 根根元类
2021-06-19 23:41:10.223618+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c330f0
2021-06-19 23:41:10.223670+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c330f0
2021-06-19 23:41:10.223713+0800 isa走位Demo[33613:662699] (null) - 0x0
2021-06-19 23:41:10.223748+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c33118
- 经过分析Class对象的继承关系如下
GCTeacher Class的SuperClass指向GCPerson Class;GCPerson Class的SuperClass指向NSObject Class;NSObject Class的SuperClass指向nil;
- 经过分析MetaClass对象的继承关系如下
GCTeacher MetaClass的SuperClass指向GCPerson MetaClass;GCPerson MetaClass的SuperClass指向NSObject MetaClass;NSObject MetaClass的SuperClass指向NSObject Class;
- 结果如图所示
isa和superClass结合
当isa和superClass结合后,就是下面这幅经典图了,此图一出无须多言!
类结构分析
前置知识:内存地址偏移读取数据
我们都知道要读取内存中的数据必须找到数据对应的内存地址,对于不同类型的数据存储,查找存储数据的内存地址方式也是不同的,常见的数据地址查找有以下几种;
- 普通基础数据类型的指针指向的是数据的地址,数据直接存在地址上;
- 对象数据类型的指针指向的是对象在堆上的存储时的首地址,可以通过内存地址偏移来获取对象内部的数据;
调试代码如下所示
@interface GCPerson : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *address;
@end
@implementation GCPerson
@end
// OC 类 结构 首地址 - 平移一些大小 -> 内容
GCPerson *person1 = [[GCPerson alloc] init];
person1.name = @"person1";
person1.address = @"SuZhou";
NSLog(@"%@ -- %p",person1,&person1);
GCPerson对象在堆上的地址为0x0000000100426040,那么偏移8个字节即为第一个属性name的地址,偏移16个字节即为第二个属性address的地址;
验证代码如下所示
(lldb) p &person1
(GCPerson **) $32 = 0x00007ffeefbff4b8 //对象在栈上的地址
(lldb) p person1
(GCPerson *) $33 = 0x0000000100426040 //对象在堆上的地址
(lldb) x/gx 0x0000000100426040
0x100426040: 0x001d800100008225
(lldb) po (Class)0x001d800100008225
GCPerson
(lldb) x/gx 0x0000000100426040+8 //偏移8个字节为第一个属性name的地址
0x100426048: 0x0000000100004070
(lldb) po (NSString *)0x0000000100004070
person1
(lldb) x/gx 0x0000000100426040+8+8 //偏移16个字节为第一个属性name的地址
0x100426050: 0x0000000100004090
(lldb) po (NSString *)0x0000000100004090
SuZhou
- 数组类型的指针指向的是数组在堆上的存储时的首个元素的地址,可以通过内存地址偏移来一次获取数组其他索引位置的数据;需要注意的是如果是数组指针类型直接+1即可,系统会根据数组内部的数据类型宽度作为本次相加的步长; 调试代码如下所示
// 数组指针
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);
for (int i = 0; i<4; i++) {
int value = *(d+i);
NSLog(@"%p",(d+i));//当前内存位置的地址值
NSLog(@"%d",value);//当前内存位置的数据
}
NSLog(@"指针 - 内存偏移");
验证代码如下所示
2021-06-20 13:20:06.276616+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0 - 0x7ffeefbff4c0 - 0x7ffeefbff4c4
2021-06-20 13:20:06.277084+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0 - 0x7ffeefbff4c4 - 0x7ffeefbff4c8
2021-06-20 13:20:06.277164+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0
2021-06-20 13:20:06.277226+0800 isa走位Demo[36200:835553] 1
2021-06-20 13:20:06.277274+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c4
2021-06-20 13:20:06.277319+0800 isa走位Demo[36200:835553] 2
2021-06-20 13:20:06.277363+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c8
2021-06-20 13:20:06.277428+0800 isa走位Demo[36200:835553] 3
2021-06-20 13:20:06.277473+0800 isa走位Demo[36200:835553] 0x7ffeefbff4cc
2021-06-20 13:20:06.277516+0800 isa走位Demo[36200:835553] 4
2021-06-20 13:20:06.277621+0800 isa走位Demo[36200:835553] 指针 - 内存偏移
class对象的内存布局
- 为了了解
class的内部存储信息,我们首先查看OBJC源码来确定class对象的成员变量等信息,我们已经知道isa是Class类型的。Class类型是objc_class *,objc_class是一个结构体,并且所有的Class底层实现都是objc_class。 - 先搜索
struct objc_class发现在runtime.h、objc-runtime-old.h、objc-runtime-new.h这三个类都有实现,因为现在使用的都是objc2,所以可以排除掉runtime.h中的实现,最终从objc-runtime-new.h可以找到结构体内部除了从父结构体objec_object继承来的isa之外还有superclass、cache_t、class_data_bits_t等成员变量;还有getclass、setclass等方法;
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
Class getSuperclass() const {
}
void setSuperclass(Class newSuperclass) {
}
...
...
}
- objc源码还有一个
__has_feature(ptrauth_calls),__has_feature()主要作用是判断当前编译器是否支持某个功能;ptrauth_calls是指身份验证针对arm64e架构;使用A12或更高版本的A系列处理器的设备支持arm64e;相当于一个架构判断,从iPhoneXS开始的设备都支持这个结构; - 阅读源码之后我们发现结构体
objc_class的内部首个成员是占8个字节的Class ISA,其次是占8个字节的Class superclass和 结构体类型的cache_t cache,然后是struct class_data_bits_t类型的bits;
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;// 范型真正的大小是<uintptr_t> 无符号长整型 是8字节;
union {
struct {
explicit_atomic<mask_t> _maybeMask;//uint32_t //2个字节
#if __LP64__
uint16_t _flags;//uint16_t2个字节
#endif
uint16_t _occupied;//uint16_t2个字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;//指针类型8个字节
};
chache所占的内存空间,按照结构体内存计算是16字节,那么最终的bits的位置就是class本身的内存地址偏移8+8+16个字节即可获得;
class_data_bits_t bits信息
-
首先经过分析我们得出
struct objc_class内部的数据结构如下图所示: -
注意list_array_tt 相当于数组 存储了很多property_list_t,property_list_t又是一个数组存储了很多property_t
基于这张图上的数据结构信息,我们可以依次递进取值最终找到对应的
property
类的property探究
通过LLDB调试,逐步获取struct objc_class内部的数据, 可以看到GCPerson的property信息,调试代码代码如下;
(lldb) x/4gx GCPerson.class //步骤1
0x100004568: 0x0000000100004540 0x0000000100354140
0x100004578: 0x00000001007889b0 0x0001802000000003
(lldb) p/x 0x100004568 + 0x20 //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004588
(lldb) p (class_data_bits_t *)$1 //步骤3
(class_data_bits_t *) $2 = 0x0000000100004588
(lldb) p $2->data() //步骤4获取data
(class_rw_t *) $3 = 0x0000000100788970
(lldb) p *$3 //重要步骤5还原数据
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294983960
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.properties() //步骤6
(const property_array_t) $6 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100004210
}
arrayAndFlag = 4294984208
}
}
}
(lldb) p $6.list //步骤7获取list
(const RawPtr<property_list_t>) $7 = {
ptr = 0x0000000100004210
}
(lldb) p $7.ptr //步骤8获取ptr
(property_list_t *const) $8 = 0x0000000100004210
(lldb) p *$8 //重要步骤9还原数据
(property_list_t) $9 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
(lldb) p $9.get(0) //步骤10
(property_t) $10 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb)
类的method探究
通过与上一步骤类似的方法,逐步获取struct objc_class内部的数据, 最终可以看到GCPerson的method信息了,调试代码代码如下;
(lldb) x/4gx GCPerson.class //步骤1
0x100004630: 0x0000000100004608 0x0000000100354140
0x100004640: 0x00000001006fa530 0x0001803000000003
(lldb) p/x 0x100004630 + 0x20 //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004650
(lldb) p (class_data_bits_t *)$1 //步骤3
(class_data_bits_t *) $2 = 0x0000000100004650
(lldb) p $2->data() //步骤4获取data
(class_rw_t *) $3 = 0x00000001006fa4e0
(lldb) p *$3 //重要步骤5还原数据
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294984056
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.methods() //步骤6
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000041c0
}
arrayAndFlag = 4294984128
}
}
}
(lldb) p $5.list //步骤7获取list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
ptr = 0x00000001000041c0
}
(lldb) p $6.ptr //步骤8获取ptr
(method_list_t *const) $7 = 0x00000001000041c0
(lldb) p *$7 //重要步骤9还原数据
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 5)
}
(lldb) p $8.get(0).big()// 步骤10 获取下标为0的方法,并通过big()打印方法;
(method_t::big) $9 = {
name = "sayHello"
types = 0x0000000100003eda "v16@0:8"
imp = 0x0000000100003aa0 (KCObjcBuild`-[GCPerson sayHello] at main.m:26)
}
(lldb)
步骤10返回的是method_t类型不能直接打印,需要通过其内部的结构体big才可以看到相关信息;
成员变量探究
- 经过阅读源码,发现通过
class_rw_t* data() const获取的class_rw_t里面有一个const class_ro_t *ro() const方法,可以获取class_ro_t信息,在class_ro_t里面又有ivars、const char *getName() const等一系列方法和成员,如下图所示
2. 基于这个信息 我们是否可以返照获取
methods和properties的相关方法获取ivar呢?经过尝试是可以的,代码如下;
(lldb) x/4gx GCPerson.class //步骤1
0x100004630: 0x0000000100004608 0x0000000100354140
0x100004640: 0x0000000102b2bda0 0x0001803000000003
(lldb) p/x 0x100004630 + 0x20 //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004650
(lldb) p (class_data_bits_t *)$1 //步骤3
(class_data_bits_t *) $2 = 0x0000000100004650
(lldb) p $2->data() //步骤4获取data;
(class_rw_t *) $3 = 0x0000000102b2bd50
(lldb) p *$3 //重要步骤5还原数据
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294984056
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.ro() // 步骤6获取ro
(const class_ro_t *) $5 = 0x0000000100004178
(lldb) p $5.ivars //步骤7获取ivars
(const ivar_list_t *const) $6 = 0x0000000100004240
Fix-it applied, fixed expression was:
$5->ivars
(lldb) p $5->ivars->get(1) //步骤8获取ivars的首个元素ivar_t
(ivar_t) $8 = {
offset = 0x00000001000045d8
name = 0x0000000100003e67 "job"
type = 0x0000000100003ee2 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $5.name //步骤9获取name
(const explicit_atomic<const char *>) $9 = {
std::__1::atomic<const char *> = "GCPerson" {
Value = 0x0000000100003e3e "GCPerson"
}
}
(lldb)
类方法变量探究
- 我们已经知道
instance是由class实例化得到的,class是由MetaClass实例化得到的,instance的方法存储在class中,那么class的方法是否也类似的存储与MetaClass当中呢?我们尝试进行了以下分析
@interface GCPerson : NSObject
{
NSString *_hobby;
NSString *job;
}
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *address;
- (void)sayHello;
+ (void)sayHi;
@end
@implementation GCPerson
- (void)sayHello {
NSLog(@"hello");
}
+ (void)sayHi {
NSLog(@"Hi");
}
@end
GCPerson *p1 = [[GCPerson alloc] init];
Class cla = object_getClass(p1);
Class metaClass = object_getClass(p1.class);
NSLog(@"%p, %p ",cla,metaClass);
- 这里我们创建了一个包含类方法的
GCPerson,同是获取了MetaClass;
3.在LLDB中模仿获取对象方法的步骤来获取MetaClass中的method,最终成功找到了类方法;
(lldb) x/4gx metaClass //步骤1获取metaClass的地址
0x100004638: 0x00000001003540f0 0x00000001003540f0
0x100004648: 0x000000010034b360 0x0000e03100000000
(lldb) p/x 0x100004638 + 0x20 //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004658
(lldb) p (class_data_bits_t *)$1 //步骤3
(class_data_bits_t *) $2 = 0x0000000100004658
(lldb) p $2->data() //步骤4获取data
(class_rw_t *) $3 = 0x0000000100719a60
(lldb) p *$3 //重要步骤5
(class_rw_t) $4 = {
flags = 2684878849
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294983992
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff884e2cd8
}
(lldb) p $4.methods() //步骤6 获取methods
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100004180
}
arrayAndFlag = 4294984064
}
}
}
(lldb) p $5.list //步骤7获取list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
ptr = 0x0000000100004180
}
(lldb) p $6.ptr //步骤8获取ptr
(method_list_t *const) $7 = 0x0000000100004180
(lldb) p *$7 // 重要步骤9还原数据
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $8.get(0).big() //步骤10通过big打印method的信息
(method_t::big) $9 = {
name = "sayHi"
types = 0x0000000100003ed2 "v16@0:8"
imp = 0x0000000100003a10 (KCObjcBuild`+[GCPerson sayHi] at main.m:30)
}
(lldb)
至此,我们已经找到了对象的Ivar和类方法的存储位置了;