前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC类的原理(一)

OC类的原理(一)

作者头像
拉维
发布2021-10-20 14:58:39
4900
发布2021-10-20 14:58:39
举报
文章被收录于专栏:iOS小生活iOS小生活

前面两篇文章介绍了OC对象的原理,以及一些分析的思路和方法,今天开始,将开启类的原理探究。

不过在探究类的原理之前,我想补充说明一些东西。

首先来了解下isa指针的定义,如下:

代码语言:javascript
复制
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是互斥的(因为isa是union,而union中元素是互斥的):非nonpointer指针只使用到cls,而nonpointer指针只使用到bits

我们前面也讲到,nonpointer的isa指针可以存储很多额外信息,并且其存储信息的内存布局是跟架构有关的,下面这张图可以很形象地将该布局给展示出来:

接下来再来了解下指针和内存平移的概念。

来看这么一段代码:

代码语言:javascript
复制
      int c[4] = {1,2,3,4};
      int *d   = c;
      NSLog(@"%p - %p - %p - %p",&c, &c[0], &c[1], &c[2]);
      NSLog(@"%p - %p - %p",d,d+1,d+2);

      for (int i = 0; i<4; i++) {
          int value =  *(d+i);
          NSLog(@"%d",value);
      }

打印结果如下:

可以看到,&c和&c[0]的结果是一样的,这说明数组的第一个元素的地址就是该数组的地址

&c[0]、&c[1]和&c[2],相邻元素的地址之间相差4个字节,而数组c的元素类型是Int,int类型就是占4个字节,也就是说,数组的相邻元素之间的地址差值就是该数组元素类型的大小

&c[0]与d、&c[1]与d+1、&c[2]与d+2的地址都是相等的,这是为啥呢?

&c[0]表示的是数组c中第一个元素的地址;而*d表示的是数组c的值,因此d表示的是数组c的地址,也就是数组c中第一个元素的地址。因此&c[0]与d的值是一样的。

而d+1表示的是数组c中第二个元素的地址(即由地址d向后面平移一个元素的大小),d+2表示的是数组c中第三个元素的地址,因此d、d+1、d+2的差值可不是1,而是数组c中元素类型的大小,在这里是4个字节(int类型)。

d前面加*表示的是取地址d的值,因此*(d+i)表示就是数组c中第i个元素的值

类的结构分析

类是使用Class来接收,这一点我们在开发中已经非常熟悉了。所以关于类的结构分析,我们就从Class的定义开始。

第一步,就是将OC文件编译成C++,编译完成之后打开对应的cpp文件,可以找到Class的定义,如下:

代码语言:javascript
复制
typedef struct objc_class *Class;

我们可以知道,Class是指向objc_class的指针

那么objc_class是什么呢?我们会找到如下定义:

代码语言:javascript
复制
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继承而来。

属性&成员变量存储区域探究

代码语言:javascript
复制
@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickName;

@end

这里我定义了一个LGPerson类,里面有1个属性和1个成员变量。

然后在外界调用,并且打断点。我们在断点处进行分析:

上面我们查看了类LGPerson的内存段。

我们知道,类的结构如下:

代码语言:javascript
复制
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的结构如下:

代码语言:javascript
复制
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的定义:

代码语言:javascript
复制
#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的定义:

代码语言:javascript
复制
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的数据结构:

代码语言:javascript
复制
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的数据结构:

代码语言:javascript
复制
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类的定义是这样的:

代码语言:javascript
复制
@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickName;

@end

此时获取到ro中的baseMethodList:

我们可以看到,baseMethodList中元素的类型是method_t,method_t的结构如下:

代码语言:javascript
复制
struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    ...
};

我们注意到,baseMethodList的打印结果中,count是3,这是为什么呢?明明我在LGPerson中没有定义任何方法啊。

其中第一个方法我们也已经看到了,.cxx_destruct是系统默认添加的方法,那么其他两个是什么呢?实际上,其他两个分别是属性nickName的setter和getter方法,如下:

现在我们手动往LGPerson类中添加两个方法:

代码语言:javascript
复制
@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中

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档