引言
上一篇讲到了内存偏移
的知识和操作,接下来内存偏移
将在本文用到具体的示例。我们对对象的探究已经了解了对象的底层结构,isa的走向和对象的继承链。本文将还原探究类内部结构的过程。
类的探索
一、寻找objc_class
在001-OC对象原理探究 - alloc这篇文章中,我们用到了objc源码环境调试。本文我们也在此基础上,探究类的结构。我们在对象的底层结构探索的时候,发现了类Class
的底层为typedef struct objc_class *Class;
也就是说,Class
是一个结构体指针
的别名。
具体寻找步骤:
1、objc源码调试环境工程,搜索Class {
,在结果中我们得到runtime.h
中的class结构:
2、这似乎就是我们要找的
Class
底层,但是看到#if !__OBJC2__
以及OBJC2_UNAVAILABLE
,才知道,整个结构体struct objc_class
并不适用于objc2
中(本文调试的环境的是objc4_818_2
)3、那么我们怎么找到正确的
Class
呢?请看框起来的注释/* Use
Classinstead of
struct objc_class **/
源码如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
在此源码中,我们还得到一个信息:OC中id类型
的底层竟然是typedef struct objc_object *id;
,这就是为什么我们在定义id类型
的变量时,不加*号
的原因。
探索源码真的能学到很多!!!
4、搜索框输入struct objc_class
,其中objc_runtime-new.h
中的结果就是我们想要的,结果如下:
二、bits
上图的objc_class
内部可知bits
在Class superclass
和cache_t cache
之后。我们调试得到bits
,需要上篇文章提到的内存偏移
来得到。因此,我们需要知道偏移了多少字节,接下来我们开始探索Class superclass
和cache_t cache
的内存大小。
1、superclass
是Class
指针类型,因此superclass
占8字节
2、cache_t
为结构体类型,其内部结构如下:(由于我们需要知道结构体内存的大小,只需要知道其成员变量的大小即可,cache_t内部的static和方法函数均不影响结构体内存大小
,因此以下源码为简化后的cache_t
):
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
3、分析
a)、_bucketsAndMaybeMask
是explicit_atomic
的泛型变量,因此实际大小为泛型
的大小,即uintptr_t
的大小,uintptr_t
的源码为:typedef unsigned long uintptr_t;
因此占8字节
。
b)、union
为共用体,内存大小为最大的成员的大小。
1)struct
中,_maybeMask
为mask_t
,源码为typedef uint32_t mask_t;
占4字节
,uint16_t
大小为2字节
。结构体最大占用内存4 + 2 + 2 = 8字节
2)_originalPreoptCache
为preopt_cache_t *
,结构体指针类型,我们知道指针类型的大小为8字节
。
3)其实换个角度来看,union
中,我们只需要看_originalPreoptCache
的大小即可知道union
占用大小为8字节
。
4、cache_t
所占内存大小为:16字节
。
到此为止,我们只需要或许到类的首地址后,将其平移isa:8 + superclass:8 + cache_t:16 = 32字节
才能得到bits
。简化后的objc_class
源码如下:
struct objc_class : objc_object {
Class ISA; //8字节
Class superclass;// 8字节
cache_t cache; // 16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
下面我们开始获取bits
:
1、QLPerson
设计如下:
@interface QLPerson : NSObject{
NSString *fullName;
}
@property (nonatomic,copy) NSString *nickName;
@property (nonatomic,assign) NSInteger age;
- (void)test1;
+ (void)test1;
@end
2、对QLPerson
类进行lldb
调试
3、p *$2->data()
为objc_class
中的方法
class_rw_t *data() const {
return bits.data();
}
image.png
但是似乎未能得到我们想要的东西。换种思路继续
4、我们继续
objc_class
向下翻找,治世之尊没有找到能看到类的属性和方法的关键词。后来看到bits后面的注释,我们要的东西是否在class_rw_t
里。点进去终于看到了
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
methods()
,properties()
,protocols()
这正是我们日思夜想的东西嘛。lldb
调试如下:
@property
声明的属性,均存在property_list_t
中,用上图的lldb
调试可以找到属性的值。但是问题来了,我们的方法
和成员变量
均未得到,它们放在哪儿呢?补充:
properties()
返回类型为property_array_t
,继承自list_array_tt
,源码如下:
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
说明:关于二维数组容器list_array_tt
的知识请移步这篇文章
5、properties()
探索结束,未能达到我们的目的,我们接着探索methods()
说明:我们用探索
properties
的方式来探索methods
最终得到了该类的实例方法
,getter setter方法
,达到部分目的,因为,我们的+(void)test2
还未出现。6、
methods()
探索结束我们仍未找到类方法
和成员变量ivar
的存储位置,我们接着往下探索,在class_rw_t
内找到一个ro()
方法,源码如下:
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
class_ro_t
内部部分源码为:
struct class_ro_t {
void *baseMethodList;
const ivar_list_t * ivars;
这个ivars
操作如下:
到此为止,我们拿到了成员变量的存储位置已经搞清楚。
7、类方法
探索,换种思路,实例方法
也叫做对象方法
。类方法
似乎与对象无关,那么它是否在元类
里呢?按照这个思路,我们对QLPerson
的元类进行上面的查找操作:
由图可得到
类方法存储在元类中
。
总结
1、探索类的结构是一个漫长而复杂的过程,有些地方卡在那里,如果不转换思路,将进入死胡同。对类的探索,应该多借鉴前辈的肩膀,偶尔用上帝视角
去解决遇到的难题。
2、存储位置:
类的首地址+0x20
得到bits
bits->data()得到class_rw_t
a)获取类的属性(@property
标记):bits
中的class_rw_t
中的properties()
b)获取成员变量(类大括号内的声明):bits
中的class_rw_t
中的ro()
c)获取实例方法(也叫对象方法-()
):bits
中的class_rw_t
中的methods()
,每一项需要加.big()
来打印
d)获取类方法(+()
):元类中的bits
->class_rw_t
->methods()
,每一项需要加.big()
来打印
3、以method_array_t
为例,结构图如下:
网友评论