前面两篇文章介绍了OC对象的原理,以及一些分析的思路和方法,今天开始,将开启类的原理探究。
不过在探究类的原理之前,我想补充说明一个东西
isa指针定义如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa指针分为nonpointer指针和非nonpointer指针。
非nonpointer指针没有经过优化,它里面只通过cls属性存储对应的类的地址;
nonpointer指针是经过优化的,它通过bits存储很多信息。
需要注意的是,cls和bits是互斥的:非nonpointer指针只使用到cls,而nonpointer指针只使用到bits。
我们前面也讲到,nonpointer的isa指针可以存储很多额外信息,并且其存储信息的内存布局是跟架构有关的,下面这张图可以很形象地将该布局给展示出来:
类的结构分析
类是使用Class来接收,这一点我们在开发中已经非常熟悉了。所以关于类的结构分析,我们就从Class的定义开始。
第一步,就是将OC文件编译成C++,编译完成之后打开对应的cpp文件,可以找到Class的定义,如下:
typedef struct objc_class *Class;
我们可以知道,Class是指向objc_class的指针。
那么objc_class是什么呢?我们会找到如下定义:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
......
}
我们发现,objc_class是一个继承自objc_object的结构体。我们总说万物皆对象,类也是对象,就是出自于这里。
我们还发现,objc_class中有一个隐藏的Class类型的isa指针。为什么是隐藏的呢?因为isa指针可以从父类objc_object继承而来。
属性&成员变量存储区域探究
@interface LGPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end
这里我定义了一个LGPerson类,里面有1个属性和1个成员变量。
然后在外界调用,并且打断点。我们在断点处进行分析:
上面我们查看了类LGPerson的内存段。
我们知道,类的结构如下:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
......
}
第一个变量是isa指针,第二个变量是superclass指针,他们都是Class类型,而Class的本质是结构体指针(struct objc_class *),因此,它们都是占8个字节(pointer都是占8字节)。
第三个变量是cache_t,它占多少字节的内存呢?我们来研究一下。
cache_t的结构如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
_buckets是一个结构体指针,它占用8个字节。
_mask和_occupied都是mask_t类型,我们接着来看mask_t的定义:
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
我们现在知道了,mask_t实际上是uint32_t,而Int是占4个字节。
在cache_t中,前三个变量占用的内存大小是8+4+4=16字节,后面的都是函数,函数是不占用内存的,因此cache_t所占内存大小是16字节。
第四个变量是bits,一看这个名字我们就能知道,它是存储各种信息的,因此我们就需要读到它。
通过前面的分析我们已经知道了,bits前面三个变量占用内存总大小是8+8+16 = 32字节,折算成十六进制是0X20,因此我就需要将LGPerson的类地址0x100002338向右平移32字节,也就是0x100002358。
接下来打印0x100002358
啥都没打印出来。
这里有个小知识点需要说明一下:
po表示打印对象,如果是对象的话可以使用po来打印
如果不是对象,那么就使用p来打印。
我们发现,直接打印0x100002358是打印不出来的。0x100002358是bits的首地址,也就是bits的指针,因此我们需要强转一下,如下:
现在我得到了bits的指针,那么怎么得到bits里面的值呢?
我们再复习一下objc_class的定义:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
......
}
此时我们知道,bits中有一个data()函数,因此我们就可以通过下面的方式来获取到bits里面的值:
此时的$7是(class_rw_t *)类型,我们先来看一下class_rw_t的数据结构:
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;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
......
};
然后,我们打印*$7,就能打印出对应的class_rw_t了:
现在我们来回想一下我们的问题,我们是要查看LGPerson中声明的一个成员变量和一个属性是存在什么地方。
现在说结论了,虽然下面有properties,但是属性和成员变量没有放在其中,而是存在ro中。
接下来我们就打印ro:
我们看到,ro是(class_ro_t *)类型,所以我们看一下class_ro_t的数据结构:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
然后我们查看ro的baseProperties:
LGPerson的nickName属性就存放在这里面。
再查看ro的ivars:
LGPerson的hobby变量就存放在这里面。【注意,这里的$5指的是ro】
看到这里,如果你细心的话,你会发现ivars里面的count是2,可是我当初声明的时候明明只声明了一个成员变量hobby啊,这是为啥?原因就在于我需要给属性nickName声明一个内部的成员变量,也就是_nickName:
实例方法的存储位置探究
LGPerson类的定义是这样的:
@interface LGPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end
此时获取到ro中的baseMethodList:
我们可以看到,baseMethodList中元素的类型是method_t,method_t的结构如下:
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
...
};
我们注意到,baseMethodList的打印结果中,count是3,这是为什么呢?明明我在LGPerson中没有定义任何方法啊。
其中第一个方法我们也已经看到了,.cxx_destruct是系统默认添加的方法,那么其他两个是什么呢?实际上,其他两个分别是属性nickName的setter和getter方法,如下:
现在我们手动往LGPerson类中添加两个方法:
@interface LGPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end
然后再打印ro中的baseMethodList:
我们发现,此时baseMethodList中有四个方法,分别是:sayHello、.cxx_destruct、nickName、setNickName:。
这是我就疑惑了,我自定义的sayHappy方法去哪里了?
此时,想必很多人都已经知道了,sayHappy方法是一个类方法,它存储在元类的baseMethodList里面,接下来就验证一下。
类方法存储区域的探究
x/4gx pClass 是获得类的内存结构,其第一段内存存储的是isa指针。
是打印该类对应的元类的地址
x/4gx 0x0000000100002388 是查看元类的内存结构。
元类的首地址是0x100002388,平移32字节之后是0x1000023a8,进而得到bits,然后强转为class_data_bits_t,然后获取到rw,进而得到ro。
接下来我们查看ro:
最后我们在元类的baseMethodList中找到了sayHappy方法。
这就验证了:实例对象的类方法是存在元类的baseMethodList中。
以上。