|
网站内容均来自网络,本站只提供信息平台,如有侵权请联系删除,谢谢!
本文主要分析类与类结构
类的分析
类的分析主要是分析 isa 的走向与继承关系
准备
- 创建两个类
- 1.继承自 NSObject 的 LGPerson
@interface LGPerson : NSObject{ NSString *hobby;}@property(nonatomic,copy)NSString * lg_name;- (void)sayHello;+ (void)sayBye;@end@implementation LGPerson- (void)sayHello{ }+ (void)sayBye{ }@end
@interface LGTeacher : LGPerson@end@implementation LGTeacher@end
- 在 main 中定义两个类的对象 person & teacher
int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... LGPerson *person = [LGPerson alloc]; LGTeacher *teacher = [LGTeacher alloc]; NSLog(@"Hello, World! %@ %@",person,teacher); } return 0;}元类
首先我们通过案例的 lldb 引出元类
- 在 main 中的 LGTeacher 处加一个断点,运行
- 开启 lldb 模式,调试过程如下
根据调试过程中,我们产生了一个疑问,为什么po 0x0000000100008568结果也是 LGPerson?
- 0x0000000100008568地址是类中的 isa 的指针地址,是 LGPerson 类的类的指针地址,我们把 LGPerson 类的类称为元类
- 所以打印为 LGPerson的根本原因就是元类导致的
元类的说明
- 我们都知道对象的 isa 指向类,类其实也是一个对象,可以称为类对象,他的 isa 指向苹果定义的元类
- 元类是系统给的,其定义和创建都是由编译器完成的,在这个过程中,类的归属来源于元类
- 元类是类对象的类,每个类都有独一无二的元类用来存储类方法相关信息
- 元类本身是没有名称的,由于和类相关联,所以使用了和类一样的名称
下面通过 lldb 探索元类的走向,也就是 isa 的走位,如下图所示,可以得出一个关系链,
对象-->类-->元类-->NSObject-->NSObject 本身
总结
从图中可以看出
- 对象的 isa 指向类(类对象)
- 类的 isa 指向元类
- 元类的 isa 指向根元类(NSObject)
- 根元类的 isa 指向他自己
NSObject到底有几个
从图中可以看出,最后的根元类是 NSObject,这个 NSObject 和我们平时开发过程中的NSObject 是同一个嘛?
以下用两种方法可以验证
方式 1: lldb 命令
从图中可以看出,最后 NSObject 类的元类也是 NSObject,和上面 LGPerson 的根元类是同一个,所以可以得出结论:内存中只存在一份根元类 NSObject,根元类的元类指向他自己
方式 2:代码验证
通过三种不同的方式获取类,看他们的打印地址是否相同
Class class1 = LGPerson.class; Class class2 = object_getClass([LGPerson alloc]); Class class3 = [LGPerson alloc].class; NSLog(@"啦啦啦 %p %p %p",class1,class2,class3);以下是运行结果
从结果看出,打印的地址都是同一个,所以得出结论:NSObject 在内存中只有一份,根元类也一样只有一份
[面试题]类存在几份
由于类的信息在内存中只存在一份,所以类对象只有一份
注明的 isa 走位图
isa 走位
- 实例对象(Instance of subclass)的 isa 指向类(class)
- 类(class)对象的 isa 指向元类(meta class)
- 元类(meta class)的 isa 指向根元类(Root meta class)
- 根元类(Root meta class)的 isa 指向它本身,形成闭环,这里的根元类就是 NSObject
superclass 走位(继承关系)
- 类之间的继承关系
- 类(subclass)继承自父类(superclass)
- 父类(superclass)继承自根类(rootclass),根类是指 NSObject
- 根类(NSObject)继承自 nil,所以根类(NSObject)可以理解为万物之起源
- 元类也存在继承关系
- 元类(sub metaclass)继承自父元类(super metaclass)
- 父元类(super metaclass)继承自根元类(NSObject)
- 根元类(NSObject)继承自根类(NSObject)
- 注意:实例对象之间没有继承关系,类之间才存在继承关系
objc_class & objc_object
isa 走位理清楚了,又来了一个新问题,为什么对象和类都有 isa 属性?这里就该提到两个结构体:objc_class & objc_object
下面在这两个结构体的基础上,对上述问题进行探索
在上一篇底层 7中,从 clang 编译过的main.m 文件,可以看到以下 c++代码
- 其中 class 是 isa 指针的类型,是由 objc_class 类型定义的
- 而 objc_class 是一个结构体,所有的 class 都是以 objc_class 为模板创建的
struct NSObject_IMPL { Class isa;};typedef struct objc_class *Class;
- 在 objc 源码中查找 objc_class 的定义,
在源码中的定义可以看出,objc_class 是继承自 objc_object 的
- 在 objc 源码中搜索 objc_object
- 以下是编译后的 main.cpp 定义
struct objc_object { Class _Nonnull isa __attribute__((deprecated));};objc_class 和 objc_object 有什么关系?
- 结构体objc_class继承自objc_object,其中 objc_object 也是一个结构体,而且有一个 isa属性,所以 objc_class也拥有了 isa 属性
- main.cpp 底层编译文件中,NSObject 的 isa在底层是由 class 定义的,其中 class 的底层编码来自于objc_class 类型,所以 NSObject也拥有了 isa 属性
- NSObject 是一个类,用它来初始化一个实例对象 objc,objc 满足 objc_object的特性(有 isa属性),主要是因为isa 是由 NSObject 从 objc_obclass 继承过来的,而 objc_class 继承自 objc_object,objc_object 有 isa 属性,所以对象都有一个 isa,isa 表示指向,来自于当前的 objc_object
- objc_object 是当前的根对象,所以所有的对象都拥有 isa 属性
objc_object 与对象的关系
- 所有对象都是以 objc_object 为模板继承过来的
- 所有对象都来自于 NSObject,但是其底层是一个 objc_object 的结构体类型
所以 objc_object 与对象的关系是继承关系
总结
- 所有对象,类,元类都有 isa 属性
- 所有对象都是由 objc_object 继承来的
- 概括:万物皆对象,万物皆来自于 objc_object,有以下两点结论
- 所有以 objc_object 为模板创建的对象,都有 isa 属性
- 所有已 objc_class 为模板创建的类,都有 isa 属性
- 在结构层面可以理解为,上层 oc 与底层的对接
- 下层是由结构体定义的模板 objc_class objc_object
- 上层是通过底层模板创建的一些类型,比如 LGPerson
objc_object objc_class NSObject isa整体关系如下
类结构分析
主要分析类信息中主要存储哪些内容
补充知识:内存偏移
在分析类结构之前,需要了解内存偏移,因为类信息访问时,需要用到内存偏移
[普通指针]
int a = 10; int b = 10; NSLog(@"%d -- %p",a,&a); NSLog(@"%d -- %p",b,&b);打印结果如下
- a,b 都指向 10,但是 a 和 b 的地址不一样, 这属于值拷贝,也叫深拷贝
- ab 之间的地址相差 4 字节, 取决于 ab 的类型
其地址指向如图所示
[对象指针]
LGPerson *p1 = [LGPerson alloc]; LGPerson *p2 = [LGPerson alloc]; NSLog(@"%@ --- %p",p1,&p1); NSLog(@"%@ --- %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个字节,主要是因为存储的数据类型
- 可以通过首地址加偏移量,取出数组中其他元素,其中偏移量是数组的下标,内存中的首地址实际移动的字节数,等于偏移量*数据类型字节数
探索类信息中有哪些内容
在探索类信息之前,我们并不知道类信息中有哪些内容,我们可以先得到类的首地址,然后通过内存平移取出里面的值
根据前面提到的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 //....方法部分省略,未贴出}
- isa属性,继承自objc_object的isa,占8字节
- superclass属性:是class类型,class由objc_object定义,是一个指针,占8字节
- cache属性:简单的从cache_t无法看出具体内存占用,而cache_t是一个结构体类型,而结构体类型的内存大小是由内存属性决定的,(结构体指针才是8字节)
- bits属性:只有首地址经过前面3个属性内存大小总和的平移,才能获取到bits
计算cache类的内存大小
进入cache类,cache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为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; //是指针,占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个字节
- 计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12
- 情况1:if流程
- buckets是struct bucket_t *类型,是结构体指针类型,占8字节
- mask是mask_t类型,而mask_t是unsigned int类型,占4字节
- 情况2:else if流程
- maskAndBuckets:是一个uintptr_t类型,他是一个指针,占8字节
- mask_unused:而mask_t是一个uint32_t别名,占4字节
- _flags是uint16_t类型,uint16_t是unsigned short的别名,占两个字节
- _occupied是uint16_t类型,uint16_t是unsigned short的别名,占两个字节
总结:最后计算出的cache类的内存大小为 12 + 2 + 2 = 16字节
获取bits
- 获取类的首地址有两种方式
- 通过p/x LGPerson.class直接获取首地址
- 通过x/4gx LGPerson.class打印LGPerson内存布局
- 其中data()获取数据,是由objc_class提供的方法
- 从打印结果可以看出,bits存储的信息,其类型是class_rw_t ,也是一个结构体类型,但是我们还没看到属性列表,方法列表等信息
探索属性列表,即property_list
通过查看class_rw_t源码实现,可以看到内部提供了获取,方法列表,属性列表,协议列表的方法
在获取bits的基础上,通过class_rw_t提供的方法,来获取属性列表
- propertoes()方法是由calss_rw_t提供的,返回的是一个property_array_t类型
- 由于list类型是property_list_t,是一个指针,所以通过p *$来取值,同时证明了bits里面存储了属性列表property_array_t
问题:探索成员变量的存储
由此可以得出,property_list中并没有成员变量,属性与成员变量的区别就是有没有set get方法,如果有就是属性,没有则是成员变量
探索方法列表,即method_list
在LGPerson增加两个方法,一个类方法,一个实例方法
@interface LGPerson : NSObject@property(nonatomic,copy)NSString * name;@property(nonatomic,copy)NSString * nickName;- (void)sayHello;+ (void)sayBye;@end@implementation LGPerson- (void)sayHello{ }+ (void)sayBye{ }@end
- 通过p $4.methods,获得具体的方法列表的list结构,其中methods方法也是class_rw_t提供的方法
新问题的探索 (探索成员变量的存储)
通过查看objc_class的类结构,发现bits里面的class_rw_t,除了property_list,method_list,protocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看他的定义,发现其中有一个ivar_list_t * ivars;属性,所以我们猜测,成员变量就存在于ivars里面
(lldb) p/x LGPerson.class(Class) $0 = 0x0000000100008240 LGPerson(lldb) p (class_data_bits_t *)0x0000000100008260(class_data_bits_t *) $1 = 0x0000000100008260(lldb) p $1->data()(class_rw_t *) $2 = 0x0000000100755860(lldb) p *$2(class_rw_t) $3 = { flags = 2148007936 witness = 1 ro_or_rw_ext = { std::__1::atomic = { Value = 4295000200 } } firstSubclass = nil nextSiblingClass = NSUUID}(lldb) p $3.ro(const class_ro_t *) $4 = 0x0000000100008088 Fix-it applied, fixed expression was: $3.ro()(lldb) p $3.ro()(const class_ro_t *) $5 = 0x0000000100008088(lldb) p *$5(const class_ro_t) $6 = { flags = 388 instanceStart = 8 instanceSize = 24 reserved = 0 = { ivarLayout = 0x0000000100003ec9 "\U00000002" nonMetaclass = 0x0000000100003ec9 } name = { std::__1::atomic = "LGPerson" { Value = 0x0000000100003ec0 "LGPerson" } } baseMethodList = 0x0000000100003e70 baseProtocols = nil ivars = 0x00000001000080d0 weakIvarLayout = 0x0000000000000000 baseProperties = 0x0000000100008118 _swiftMetadataInitializer_NEVER_USE = {}}(lldb) p $6.ivars(const ivar_list_t *const) $7 = 0x00000001000080d0(lldb) p *$7(const ivar_list_t) $8 = { entsize_list_tt = (entsizeAndFlags = 32, count = 2)}(lldb) p $8.get(0)(ivar_t) $9 = { offset = 0x0000000100008210 name = 0x0000000100003f0d "_name" type = 0x0000000100003f60 "@\"NSString\"" alignment_raw = 3 size = 8}(lldb) p $8.get(1)(ivar_t) $10 = { offset = 0x0000000100008214 name = 0x0000000100003f13 "_nickName" type = 0x0000000100003f60 "@\"NSString\"" alignment_raw = 3 size = 8}(lldb) p $8.get(2)Assertion failed: (i < count), function get, file /Users/dev1852/Downloads/学习demo/objc4-818-master/objc4-818.2/runtime/objc-runtime-new.h, line 625.error: Execution was interrupted, reason: signal SIGABRT.The process has been returned to the state before expression evaluation.(lldb)
- class_ro_t中的结构如下所示,想要获得ivars,需要首地址平移48字节
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;#ifdef __LP64__ uint32_t reserved;#endif union { const uint8_t * ivarLayout; Class nonMetaclass; }; explicit_atomic name; // With ptrauth, this is signed if it points to a small list, but // may be unsigned if it points to a big list. void *baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; // This field exists only when RO_HAS_SWIFT_INITIALIZER is set. _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];//省略}结论
- 通过{}定义的成员变量,会存储在类的bits属性中,通过bits -> data -> ro -> ivars获取成员列表,除了字节定义的成员变量,还有属性生成的成员变量
- 通过property生成的属性,存在于 bits -> data -> property_list 获取属性列表,其中只包含属性
问题:探索类方法的存储
由此可知,methods_list中只有实例方法,没有类方法,那么问题就是,类方法存储在哪里。
在前面我们提到了元类,类的isa就是指向元类,元类是用来存储类相关信息的,所以猜测,类方法是否存储在元类的bits当中,可以通过lldb命令验证一下
通过图中元类列表打印结果,可以得到以下结论
- 类的实例方法存储在类的bits属性中
- 类的类方法存储在元类的bits属性中
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
免责声明
1. 本论坛所提供的信息均来自网络,本网站只提供平台服务,所有账号发表的言论与本网站无关。
2. 其他单位或个人在使用、转载或引用本文时,必须事先获得该帖子作者和本人的同意。
3. 本帖部分内容转载自其他媒体,但并不代表本人赞同其观点和对其真实性负责。
4. 如有侵权,请立即联系,本网站将及时删除相关内容。
|