四、iOS-类的结构分析

一、类的探究

我们从上一篇文章iOS中的isa分析对象的时候已经过渡到类了,提到了 对象,类,元类,根元类等概念,那么接下来我们就一起来探索一下 类 到底是什么。

在开始探究之前,先补充一下内存偏移的概念,主要是为了更好理解后面的类的结构体。

int c[4] = {1,2,3};           // 这里先定义一个int数组 c
int *d   = c;                   // 然后定义一个指针d指向 c
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
打印结果:0x7ffeefbff4a0 - 0x7ffeefbff4a0 - 0x7ffeefbff4a4 - 0x7ffeefbff4a8
打印结果:                 0x7ffeefbff4a0 - 0x7ffeefbff4a4 - 0x7ffeefbff4a8
看这里我们会发现 数组c 的地址 和 c[0] 是同一个地址, 而指针d也是等于 数组c的首地址
并且通过指针d+1,d+2 也能找到数组相应的元素,所以说通过指针偏移可以指向接下来连续的内存地址。 
1、类的结构分析

我们都知道,所有的类都是继承于NSObject,那NSObject本身不就是一个类吗?下面先结合源码来看一下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

通过NSObject点击来

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

这里我们可以看到 NSObject里面仅有一个Class isa,那么这个Class又是什么,继续点击去

typedef struct objc_class *Class;
typedef struct objc_object *id;

这里我们可以看到Class是一个结构体,也就是之前说到的,类的本质就是一个结构体。而objc_class 又是继承自objc_object, 这也说明了我们常说的万物皆对象。

  • NSObject本身是一个类,在底层实现就是objc_class。
  • objc_object是c的结构类型,NSObject是OC的类型,NSObject就是对objc_object的封装。

objc_object和objc_class关系图如下:
1_1.png

2、类的属性、方法、成员变量、协议...分析

上面我们知道了类的结构是什么样的,那么类里面具体都包含了一些什么内容呢,下面我们就来分析一下objc_class

struct objc_class : objc_object {
    // Class ISA;             // 8字节
    Class superclass;         // 8字节
    cache_t cache;             // formerly cache pointer and vtable   16字节
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    class_rw_t *data() {        // 这里是一个class_rw_t *指针
        return bits.data();
    }
..........这里还有200多行代码,先省略了,大概都是初始化数据的一些函数
}

1.第一个属性 Class ISA 被注释掉的,意思就是从父类继承过来的,我们进入objc_object里面可以看到,占用8个字节。

struct objc_object {
private:
    isa_t isa;
    ...省略
}

2.第二个属性Class superclass父类,占用8个字节。
3.第三个属性cache_t cache一个结构体,顾名思义是一些缓存的信息,总共占用16个字节,这个cache_t会在另一篇文章中会专门做分析(待更新、、、)

struct cache_t {
    struct bucket_t *_buckets;  //指针占用8字节
    mask_t _mask;               // int32  占用4字节
    mask_t _occupied;           //占用4字节
 ...省略...
};

4.第四个属性bits是什么?这里我们来看一下。

typedef unsigned long           uintptr_t;
struct class_data_bits_t {
    // 相当于 unsigned long bits; 占64位
    // bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t)
    uintptr_t bits;
... 省略...
}

从这里可以看到bits应该就是一个64位的数据段,那么里面存了什么数据呢,还要继续往下分析。
在class_data_bits_t bits的注释:class_rw_t * plus custom rr/alloc flags,意思是class_data_bits_t就相当于class_rw_t * 加上rr/alloc标志。它提供了data()方法返回class_rw_t *指针。
而在bits后面就紧接着声明了一个 class_rw_t * 指针,通过bits.data() 返回,接下来就来看看这个bits.data()

 class_rw_t *data() {
     // 这里的bits就是class_data_bits_t bits;
     return bits.data();
 }
 class_rw_t* data() {
     // FAST_DATA_MASK的值是0x00007ffffffffff8UL
    //(lldb) p/t 0x00007ffffffffff8    打印二进制 看一下
    //(long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000
     // bits和FAST_DATA_MASK按位与,实际上就是取了bits中的[3,47]位
     return (class_rw_t *)(bits & FAST_DATA_MASK);
 }

那么这个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;
....省略....
}

在这个class_rw_t结构体中我们惊奇的发现这里有methods(方法)、properties(属性)、protocols(协议)这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我们就用代码来验证下。

@interface Person : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end

先定义一个Person类,里面有 属性:nickName 成员变量:hobby 对象方法:sayHello 类方法:sayHappy
然后我们通过lldb指令打印查看,结合上面的分析,来看看这几个成员都存储在了什么地方


在找bits的时候是通过内存偏移方法来找到,这也就是开头先补充的内存偏移的概念。 因为在objc_class的结构中,isa占8字节,superclass占用8字节,cache占用16个字节,将cls的地址偏移32个字节即0x20便是bits的地址。

这里我们就找到了class_rw_t 结构体,接下来继续查看methods,properties,protocols这几个数组,来看看我们要找的方法、属性、协议等是不是在里面



通过以上打印可以看到,在class_rw_t中找到了我们所定义的nickName属性、对象方法sayHello、nickName的setter/getter方法,但是成员变量hobby和类方法sayHappy都没有找到。
此时再从class_rw_t找一找其他线索,发现有一个class_ro_t *ro的东西,这是一个常量结构体指针,那么我们要找的成员变量和类方法会不会在这里呢,点进去看一下

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;
    }
};

进来一看发现,这里竟然跟class_rw_t看起来差不多,同样有方法、属性、协议列表,而且还有一个ivars 列表,那么这个ivars会不会就是成员变量列表呢。接下来继续用lldb指令来查看

果然,我们定义的成员变量hobby是在class_ro_t里面的。同样在baseMethodList、baseProperties里面也找到了我们所定义的属性和对象方法,这里就不截图了。
此时我们来总结一下:

1.在class_rw_t里面存放的有 methods、properties、protocols
2.在class_ro_t里有baseMethodList、baseProtocols、baseProperties、ivars
3.class_ro_t这个结构体是通过const定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
4.成员变量不生成setter/getter方法,并且存在class_ro_t的ivars里面。
5.此时还有一个类方法sayHappy没有找到。

通过以上分析我们大概可以知道,类的属性、成员变量、方法、协议等信息存在什么位置了。但是class_rw_t和class_ro_t为什么会存了一些相同的信息呢?这就需要我们进一步的分析了。

3、类的信息是如何存储的

通过前面的分析我们知道了,objc_class结构中的data()方法可以返回类的信息,那么我们便可以通过setData(class_rw_t *newData)这个方法追本溯源找到了setData的调用这realizeClass方法。

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    // 如果类已经实现了,直接返回
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    // fixme verify class is not in an un-dlopened part of the shared cache?
    // 编译期间,cls->data指向的是class_ro_t结构体
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw结构体已经被初始化(正常不会执行到这里)
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 正常的类都是执行到这里
        // Normal class. Allocate writeable class data.
        // 初始化class_rw_t结构体
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        // 赋值class_rw_t的class_ro_t,也就是ro
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        // cls->data 指向class_rw_t结构体
        cls->setData(rw);  
        ...
    };

我们看到其实最开始的时候cls->data是指向class_ro_t的,然后才会把class_ro_t设置到class_rw_t中。那么在realizeClass之前,在class_rw_t中一定是没有类的相关信息的。

如上图所示我们在realizeClass里面下断点,这个时候在class_rw_t中并没有类的相关信息,而在class_ro_t中却可以找到类的相关信息。是因为在这之前class_data_bits_t *data 指向的是一个 class_ro_t * 指针。

但是我们前面也分析了class_rw_t结构,是可以拿到类的相关信息的,这是因为执行了methodizeClass方法。methodizeClass方法就是向class_rw_t中添加类的方法列表、协议列表、属性列表,包括category的方法。

static void methodizeClass(Class cls)
{
    ...
    // Install methods and properties that the class implements itself.
    // 将class_ro_t中的methodList添加到class_rw_t结构体中的methodList
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    // 将class_ro_t中的propertyList添加到class_rw_t结构体中的propertyList
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    // 将class_ro_t中的protocolList添加到class_rw_t结构体中的protocolList
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    // 添加category方法
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
  
    ...
}

在methodizeClass这个方法里面,将ro中的类的属性、对象方法,遵守的协议,category方法都添加到了class_rw_t中。这样就如我们前面所分析的那样,在class_rw_t结构中可以拿到类的相关信息了。由此就形成以这样的一个结构。

小结:

  • 在realizeClass方法之前,class_data_bits_t *data 指向的是一个 class_ro_t * 指针。所以在class_rw_t中找不到类的方法、属性以及协议。
  • 在realizeClass方法会通过methodizeClass方法将类的方法、属性、协议总class_ro_t中添加到class_rw_t中。
  • class_rw_t结构体中的ro是一个class_to_t类型的常量结构体指针,所以在realizeClass方法之后ro中的内容便不可修改,我手动添加的方法也只是修改了class_rw_t中的方法列表中。
  • 类的成员变量存储在class_to_t结构体中,而不是class_rw_t结构体中。
    类的类方法在类的元类中,对象方法才在本类中。

此时此刻我们就把class_rw_t 和 class_ro_t 存储类信息的过程探索的差不多了。
但是不是还有个东西没找到呢? 类方法sayHappy啊,这个还没找到,怎么能happy呢。既然我们在类里面没有找到sayHappy,那么我们想一下它会存到哪里呢?结合我们的iOS中的isa分析,猜想它会不存到元类里面去了,那就去元类找找看

从上图打印来看,我们在元类里面找到了类方法sayHappy,证明了类方法是存在元类里面的。此刻我们所生命的方法、属性、成员变量已经全部找到了,也大概了解了类的结构以及类的成员信息都存在哪里。

总结

  • 类的本质是一个struct objec_class:objc_object结构体, 万物皆对象,类也是一个对象。
  • 属性会自动生成setter/getter方法,成员变量不会。并且属性在编译之后会生成带有_的成员变量存储在ivars里面。
  • 类的对象方法存在本类当中,而类方法存在元类中

补充

上面是通过通过源码的方式来分析了一下类,其实也可以通过分析c++代码的方式来探究,Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。

我们可以通过clang命令获取main.m的C++代码
在工程目录中的main.m文件目录下进入到终端,输入如下命令

clang -rewrite-objc main.m -o main.cpp

该命令会将main.m编译成C++的代码,然后打开main.cpp文件就可以查看了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容