OC类的原理探究(一)

ぐ巨炮叔叔 提交于 2021-02-14 16:20:24

对象原理探究(一)

对象原理探究(二)


前面两篇文章介绍了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存储很多信息。

需要注意的是,clsbits是互斥的:非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#elsetypedef 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是打印不出来的。0x100002358bits的首地址,也就是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


以上。

本文分享自微信公众号 - iOS小生活(iOSHappyLife)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!