04 类&类结构分析

我们都知道类是Class类型的,那么Class究竟是什么,里面存放着什么信息呢?今天我们通过objc的源码来看下Class究竟是什么。

objc.h文件的一开始我们就找到了关于Class的定义

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

由定义可以看到Class就是指向objc_class类型变量的一个指针。那么objc_class又是什么呢?

// objc_class的定义仅节选了属性部分,全部源码请自行下载objc源码查看。
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

从上述代码可知objc_class是一个继承自objc_object的一个结构体,内部有4个变量

  • Class类型的ISA:ISA是由objc_object结构体继承过来的,其中存储的是类的信息,具体情况可以查看03 isa探究
  • Class类型的superclass:superclass指向父类,存储了Class的继承关系。
  • cache_t类型的cache:cache是方法缓存列表,存储了最近调用的方法。
  • class_data_bits_t类型的bits:bits中存储了类的详细信息,包括成员变量,属性,方法等。

objc_object就是一个简单的结构体,内部只有一个Class类型的变量,其他什么都没有。

首先,由上面objc_class的定义就知道,objc_class的内部有一个isa指针,而我们又知道isa其实是实例对象指向自己类的一个指针,也就证明了其实Class本身也是一个实例对象,Class也有归属的类,这种类我们称为元类

我们今天的分析就从类的isa的走向superclass的走向开始。为此我们准备两个类:SLPerson继承SLPerson的SLTeacher
首先我们分析isa的走向:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // ISA_MASK        0x00007ffffffffff8ULL
        SLTeacher *teacher = [SLTeacher alloc];
        NSLog(@"");
    }
    return 0;
}

我们在NSLog处打上断点,使用lldb调试指令

(lldb) x/4gx teacher // 打印teacher的内存分布,第一个8个字节代表的其isa指针
0x101274ef0: 0x011d80010000838d 0x0000000000000000
0x101274f00: 0x0000000000000000 0x0000000000000000
(lldb) p 0x011d80010000838d & 0x00007ffffffffff8ULL  // 我们将teacher的isa 和 ISA_MASK进行&运算
(unsigned long long) $9 = 4295000968 // 打印结果得到SLTeacher类
(lldb) po $9
SLTeacher

(lldb) x/4gx $9  // 接下来打印SLTeacher类的内存分布,第一个8个字节代表的其isa指针
0x100008388: 0x0000000100008360 0x0000000100008338
0x100008398: 0x000000010174b240 0x0002802c00000003
(lldb) p 0x0000000100008360 & 0x00007ffffffffff8ULL  // 我们将SLTeacher类的isa 和 ISA_MASK进行&运算
(unsigned long long) $10 = 4295000928
(lldb) po $10  // 打印结果得到SLTeacher元类
SLTeacher

(lldb) x/4gx $10  // 接下来打印SLTeacher元类的内存分布,第一个8个字节代表的其isa指针
0x100008360: 0x000000010036a0f0 0x0000000100008310
0x100008370: 0x00000001016057b0 0x0002e03500000003
(lldb) p 0x000000010036a0f0 & 0x00007ffffffffff8ULL  // 我们将SLTeacher元类的isa 和 ISA_MASK进行&运算
(unsigned long long) $11 = 4298547440
(lldb) po $11  // 打印结果得到NSObject根元类
NSObject

(lldb) x/4gx $11    // 接下来打印NSObject根元类的内存分布,第一个8个字节代表的其isa指针
0x10036a0f0: 0x000000010036a0f0 0x000000010036a140
0x10036a100: 0x000000010127eea0 0x0003e03100000007
(lldb) p 0x000000010036a0f0 & 0x00007ffffffffff8ULL  // 我们将NSObject根元类的isa 和 ISA_MASK进行&运算
(unsigned long long) $12 = 4298547440
(lldb) po $12  // 打印结果还是得到NSObject根元类
NSObject

接着我们分析superclass的走向

// 首先我们打印NSObject根元类的superclass,我们知道superclass位与类的内存分布的第二个8个字节,即x/4gx结果的第二个内存地址
(lldb) po 0x000000010036a140  // 打印得到NSObject根元类的superclass还是NSObject,那这个NSObject到底是根元类,还是根类?
NSObject

(lldb) x/4gx 0x000000010036a140  // 我们接着打印0x000000010036a140的内存分布
// 可以看到此NSObject的isa又指向了NSObject根源类,同时此NSObject的superclass为nil,由此可知此为NSObject根类
0x10036a140: 0x000000010036a0f0 0x0000000000000000  
0x10036a150: 0x00000001012767e0 0x0002801000000003
(lldb) po 0x0000000100008338  // 同时我们打印SLTeacher类的superclass是SLPerson类
SLPerson

(lldb) x/4gx 0x0000000100008338  // 再打印SLPerson类的内存分布,得到SLPerson类的superclass是NSObject根类
0x100008338: 0x0000000100008310 0x000000010036a140
0x100008348: 0x0000000100362370 0x0000802400000000

由上面的lldb调试结果,我们可以验证苹果一副非常经典的superclass和isa走位图。下图的红色线条部分我们已经证明了,黑色线条部分大家感兴趣的可以自己证明下。

superclass&isa走位图

分析了类的isa和superclass之后我们先不看cache,这是一个方法缓存列表,为了加快方法的查找速度的,其实有或者没有都不会影响程序的运行。我们都知道类中除了isa和superclass信息之外还有很多我们申明的属性,成员变量以及方法,那么这些信息存储在什么地方呢?因为objc_class一共就只有4个变量,因此剩下的这些信息就只能存储在bits中。

我们先来看下bits变量,它是class_data_bits_t类型的,我们来到class_data_bits_t的定义。其中有一个uintptr_t类型的变量bits,同时有一个data函数可以获取bits中的相关信息,返回class_rw_t类型的指针

struct class_data_bits_t {
    // ...
    // Values are the FAST_ flags above.
    uintptr_t bits;
    // ...
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

class_rw_t结构体的定义中我们看到了几个函数,分别是

  • 获取class_ro_t类型指针的函数ro(),在class_ro_t的定义中我们也看到了ivars(成员变量),以及属性列表,方法列表和协议列表;
  • 获取方法列表的函数methods()
  • 获取属性列表的函数properties()
  • 获取协议列表的函数protocols()
struct class_rw_t {
    // ...
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }
    // ...
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
    // ...
};
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // 方法列表
    void *baseMethodList;
    // 协议列表
    protocol_list_t * baseProtocols;
    // 成员变量
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    // 属性列表
    property_list_t *baseProperties;
};

为此我们在SLPerson中添加一些成员变量,属性和方法。

@interface SLPerson : NSObject
{
    int height;
    double weight;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)sayHello;
- (void)sayGoodBye;
+ (void)say666;

@end

然后我们还是将程序运行起来,将程序断点到NSLog的地方,因为这是底层代码的原因,所以之前的所有变量我们都是通过取内存地址的方式来取值,bits变量我们也是通过这种方式取值,isa和superclass都是8个字节,cache_t的大小在iOS上是16字节(计算细节不讲了,感兴趣的可以私信),所以bits变量位于首地址偏移32字节的位置,即首地址的倒数第二位+2。接下来我们还是要通过lldb进行调试~

(lldb) x/8gx SLPerson.class
0x1000083c0: 0x0000000100008398 0x000000010036a140
0x1000083d0: 0x0000000100362370 0x0000803400000000
0x1000083e0: 0x000000010152f4e4 0x000000010036a0f0
0x1000083f0: 0x0000000100008398 0x0000000100362370
(lldb) p (class_data_bits_t *)0x1000083e0
(class_data_bits_t *) $1 = 0x00000001000083e0
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000000010152f4e0

通过上面的lldb调试我们获取到了class_rw_t的指针,接下来我们先获取属性列表

(lldb) p $2.properties()
(const property_array_t) $3 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100008220
      }
      arrayAndFlag = 4295000608
    }
  }
}
  Fix-it applied, fixed expression was: 
    $2->properties()
(lldb) p $3.begin()
(list_array_tt<property_t, property_list_t, RawPtr>::iterator) $4 = {
  lists = 0x00000001005e47f0
  listsEnd = 0x00000001005e47f8
  m = {
    entsize = 16
    index = 0
    element = 0x0000000100008228
  }
  mEnd = {
    entsize = 16
    index = 2
    element = 0x0000000100008248
  }
}
(lldb) p (property_t *)0x0000000100008228
(property_t *) $5 = 0x0000000100008228
(lldb) p $5[0]
(property_t) $6 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $5[1]
(property_t) $7 = (name = "age", attributes = "Ti,N,V_age")
(lldb) p $5[2]
(property_t) $8 = (name = "", attributes = "")

我们通过properties()函数成功拿到了SLPerson的两个属性name和age。看看我们能不能拿到成员变量呢。

(lldb) p $2->ro()
(const class_ro_t *) $9 = 0x00000001000080a0
(lldb) p $9->ivars
(const ivar_list_t *const) $10 = 0x0000000100008198
(lldb) p $10->begin()
(const entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop>::iterator) $11 = {
  entsize = 32
  index = 0
  element = 0x00000001000081a0
}
(lldb) p (ivar_t *)0x00000001000081a0
(ivar_t *) $12 = 0x00000001000081a0
(lldb) p $12[0]
(ivar_t) $13 = {
  offset = 0x0000000100008370
  name = 0x0000000100003ee6 "height"
  type = 0x0000000100003f73 "i"
  alignment_raw = 2
  size = 4
}
(lldb) p $12[1]
(ivar_t) $14 = {
  offset = 0x0000000100008378
  name = 0x0000000100003eed "weight"
  type = 0x0000000100003f75 "d"
  alignment_raw = 3
  size = 8
}
(lldb) p $12[2]
(ivar_t) $15 = {
  offset = 0x0000000100008380
  name = 0x0000000100003ef4 "_age"
  type = 0x0000000100003f73 "i"
  alignment_raw = 2
  size = 4
}
(lldb) p $12[3]
(ivar_t) $16 = {
  offset = 0x0000000100008388
  name = 0x0000000100003ef9 "_name"
  type = 0x0000000100003f77 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $12[4]
(ivar_t) $17 = {
  offset = 0x0000000200000010
  name = 0x0000000100003e83 "name"
  type = 0x0000000100003e88 "T@\"NSString\",C,N,V_name"
  alignment_raw = 16032
  size = 1
}
(lldb) p $12[5]
(ivar_t) $18 = {
  offset = 0x0000000100003ea4
  name = 0x0000002800000081 ""
  type = 0x0000000000000028 ""
  alignment_raw = 0
  size = 0
}

我们通过ivars的属性拿到了成员变量的列表,但是其中name多出一个定义,暂时还不知道是什么原因,方法列表的获取咱们后面再讲。

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

推荐阅读更多精彩内容