前面两篇文章介绍了OC对象的原理,以及一些分析的思路和方法,今天开始,将开启类的原理探究。
不过在探究类的原理之前,我想补充说明一个东西
isa指针定义如下:
union isa_t {isa_t() { }isa_t(uintptr_t value) : bits(value) { }Class cls;uintptr_t bits;struct {ISA_BITFIELD; // defined in isa.h};};
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的定义:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bitstypedef uint16_t mask_t;
我们现在知道了,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;uint32_t index;......};
然后,我们打印*$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;uint32_t reserved;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中。
以上。
本文分享自微信公众号 - iOS小生活(iOSHappyLife)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4581368/blog/4951402