OC类结构分析

OC是一门面向对象语言,面向对象离不开对象,类,继承,类方法,实例方法,属性,实例变量,对于习惯了面向对象的同学来说,这些似乎是一门语言的天然特征,一切本该如此,不需要问为什么,但对于OC来说,一切都是可以寻根溯源的,因为它是由更底层的语言c/c++来实现这些看似本该如此的特征。现在就深入内部,看看类是如何实现的,以及类的结构。

先从一段简单的代码开始:

//一个只有一个属性和一个方法的Person类
@interface Person : NSObject
@property (copy, nonatomic) NSString* name;
- (void)doSomeThing;
@end

@implementation Person
- (void)doSomeThing {
    NSLog(@"do some thing");
}
@end
//再给他创建一个继承的子类Teacher
@interface Teacher : Person
@end

@implementation Teacher
@end
//在main中创建这个类
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"%@",p);
    }
    return 0;
}

NSLog(@"%@",p);这里打个断点,可以看到p的内存地址:

对象内存地址.png
接下来可以通过lldb调试命令看一下这个p对象的内存分布,在lldb窗口输入x p来打印p的十六进制内存信息:
打印对象内存分布.png

可以看到内存的首地址就是上面断点中看到的p对象的地址0x100606940,冒号后面就是它里面存放的内容,通过NSObject头文件定义可以知道,对象内存的首地址存放的就是他的isa数据。

//NSObject.h中NSObject的定义
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

之前分析isa结构得知isa中应当存放着一个对象的类信息,通过isa可以找到对象对应的类,下面就用lldb调试,看看由p这个实例中的isa能否找到它所属的类Person

分析isa实现源码就能知道,isaISA_MASK进行与运算,就能拿到isa中真正存放类信息的33(ARM平台)或44(x86平台)字节,下面就来验证一下,在lldb中输入x/4g p

格式化打印类内存信息.png

可以看到通过x/4g打印的p的内存内容和上面通过x p打印出来的内容是一致的,只是字节顺序是相反的。下面就来看看存放isa的前8字节和ISA_MASK相与是否能获取到p所对应的Person类,打印看看是什么结果:
isa与ISA_MASK打印16进制输出.png

可以看到,首地址的8个字节内容0x001d80010000226d与上ISA_MASK 0x0000000ffffffff8,得到一个地址0x0000000100002268,那么这个地址是什么呢,po打印下便知:
打印类信息.png

这下就一目了然了,这就是要找的类Person,这就证明了对象是可以通过isa找到它对应的类,那么紧接着是不是可以用同样的方法看看类到底是什么呢?
打印类的内存内容.png

可以看到格式化输出刚才的Person类的地址0x0000000100002268,也打印出了内存的内容,假设这段内存中的第一段地址存放的也是一个isa,也与上ISA_MASK打印下看看是什么结果:
打印类的isa.png

这次直接po Person类地址与上ISA_MASK的结果,看到依然是一个Person,这就奇怪了,难道内存中存在有两个Person类,地址还不一样吗?的确如此,一个类在OC中确实会存在两份,这就是“元类”的概念,第二个Person类是第一个Person类的元类,为什么会有元类,这就是OC实现面向对象的一种机制,一个类在OC的运行时中也是一个实例,而类中也有类方法,执行类方法时,元类就出现了,通过“类实例”的isa就可以找到元类中存放的类方法进而执行,而元类其实也是一个特殊的“实例”,它里面存放着类方法表,需要执行类方法时就去它里面查找执行。这样就实现了一个面向对象的执行链条,一个对象实例需要执行实例方法,就通过自己的isa找到类,而类在内存中也是一个“类实例”,在“类实例”中存放着实例方法表,如果一个类需要执行类方法,就通过“类实例”的isa找到“元类实例”,在“元类实例”中查找需要执行的类方法。而类和元类的创建维护全部是由编译器和运行时完成的,我们并没有感知到。
下面再来看看第二个元类Person是否也能打印打它的isa,先打印下元类的格式化内存布局:
元类内存格式化打印.png

同样,拿到前8字节的isa内容与上ISA_MASK在打印:
元类isa与ISA_MASK.png

最后po一下这个0x00007fff8fa270f0地址:
元类isa地址po输出.png

结果是NSObject,这说明元类的isa指向了NSObject,下面就来看一个非常经典的图:
isa流程图.png

这样看就很清楚了,Person的元类最终指向了NSObject的元类,所以就有了上面NSObject的打印。如果继续打印NSObject的元类的isa,依然还会输出NSObject,因为根元类的isa指向了自己,形成了一个闭环。
这里分析了isa的走向,解决了实例方法和类方法调用的问题,那么下面还需要解决面向对象中的继承问题,如何解决类之间的继承关系,就需要另外一个成员superclass来解决。

//objc_runtime-new.h中objc_class的定义
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
....
}

通过runtime源码,能够看到一个类中,第一个8字节是isa,紧接着就是指向superclass的结构体的指针,那么我没如果打印一个“类实例”内存布局中的第二个8字节,也就找到了superclass,来验证一下上面图中所画的superclass的走向:

//实例p的内存布局
(lldb) x/4gx p
0x100606940: 0x001d80010000226d 0x0000000000000000
0x100606950: 0x0000000000000000 0x0000000000000000
//第一个8字节0x001d80010000226d与上ISA_MASK,得到Person类地址0x0000000100002268
(lldb) p/x 0x001d80010000226d & 0x00007ffffffffff8ULL
(unsigned long long) $28 = 0x0000000100002268
//打印Person类的内存布局
(lldb) x/4gx 0x0000000100002268
0x100002268: 0x0000000100002240 0x00007fff8fa27118
0x100002278: 0x0000000100495970 0x0004802c00000007
//通过Person类的isa与上ISA_MASK,得到Person元类的地址0x0000000100002240
(lldb) p/x 0x0000000100002240 & 0x00007ffffffffff8ULL
(unsigned long long) $32 = 0x0000000100002240
//打印Person元类的内存布局
(lldb) x/4gx 0x0000000100002240
0x100002240: 0x00007fff8fa270f0 0x00007fff8fa270f0
0x100002250: 0x0000000100608f40 0x0004e03500000007
//Person类的第二个8字节存放的是0x00007fff8fa27118,po一下
(lldb) po 0x00007fff8fa27118
NSObject
//Person元类的第二个8字节存放的是0x00007fff8fa270f0,po一下是NSObject元类
(lldb) po 0x00007fff8fa270f0
NSObject
//打印NSObject类的内存布局,第二个8字节是nil,说明他的父类指向空
(lldb) x/4gx 0x00007fff8fa27118
0x7fff8fa27118: 0x00007fff8fa270f0 0x0000000000000000
0x7fff8fa27128: 0x0000000100495d50 0x0001801000000003
//NSObject元类类的内存布局,第二个8字节正是NSObject类的地址
//说明他的父类指向NSObject类,和图中画的superclass走向一致
(lldb) x/4gx 0x00007fff8fa270f0
0x7fff8fa270f0: 0x00007fff8fa270f0 0x00007fff8fa27118
0x7fff8fa27100: 0x00000001004962c0 0x0004e03100000007

上面提到了objc_objectobjc_class结构体,下面就来看看这两个结构体之间是什么关系,通过runtime的源码,我们可以找到这两个结构体的定义,这里有一个需要注意的点,就是关于objc_class的定义是有两种的,为了向前兼容更老版本的OC,会有下面这样的objc_class定义:

//runtime.h中objc_class的定义
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

可以看到最下面的OBJC2_UNAVAILABLE;这个宏以及#if !__OBJC2__这个编译条件,这就意味着当OC版本不是OBJC2时,runtime就会使用这个版本的objc_class结构体。再来看看OBJC2条件下的objc_objectobjc_class

//objc_private.h中objc_object的定义
struct objc_object {
private:
    isa_t isa;
....
}
//objc_runtime-new.h中objc_class的定义
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
....
}

可以看到objc_class是继承了objc_object的,在c++中结构体也是可以继承的,这也就意味着objc_class中也有一个isa。这也就解释了,为什么所有实例、类及元类都会有isa,并且是自身内存布局中的第一个8字节。紧接着后面是cache_t类型,在后面就是class_data_bits_t bits;,但是却没有看到方法列表,属性列表等这些类必须的信息,那么从哪里找到这些类的信息呢,isasuperclass必然不会存储这些信息,cache是个cache_t类型,可以看到他的内部定义:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
   ....省略
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
   ....
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
....
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
....
};

可以看到cache_t内部也没有我们需要的方法列表属性列表,那么就要看看class_data_bits_t bits;中的内容了,要想查看class_data_bits_t bits;的内容可以借助内存偏移来打印这个位置的内存内容,但是如何知道这个bits在内存中的哪个位置呢,这就要看他前面的几个成员分别占用多少内存:
1、isaisa_t联合体,看源码可以知道占8字节
2、Class superclass;是指向objc_class结构体的指针,也占8字节
最后只要计算出cache_t cache;的大小,就可以知道class_data_bits_t bits;所在的内存位置了,下面就分析一下cache_t cache;的大小。

cache_t结构体定义中,首先可以看到三个宏定义的判断:

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
....
#else
#error Unknown cache mask storage type.
#endif

那么这三个分支会走哪一个呢,当然通过后面的分析也可以暴力的认为除了最后的#else其他分支都不影响cache_t所占内存大小,但是这样总是不太好,先看看这些宏都是什么含义,在objc-config.h最下面可以看到这几个宏定义:

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3

#if defined(__arm64__) && __LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

从这些定义中,我们大致能明白这里应该是在判断系统平台和cpu架构,根据不同的平台和cpu来决定cache_t结构体存储的方式,刨根问底,__arm64____LP64__又是什么意思呢?看下面:

Most C++ compilers have a predefined macro containing the version number of the compiler. Programmers can use preprocessing directives to check for the existence of these macros in order to detect which compiler the program is compiled on and thereby fix problems with incompatible compilers.

大致的意思是大部分C++编译器会预先定义一些宏,包含编译器的版本信息,这样开发者就可以通过过程检测,来检查这些宏是否存在,进而判断哪个编译器被用来编译,这样就可以解决一些编译器不兼容的问题。(来自《Calling conventions for different C++ compilers and operating systems》,Last updated 2012-02-29,作者:By Agner Fog. Copenhagen University College .)
不同版本C++编译器宏定义.png

Unfortunately, not all compilers have well-documented macros telling which hardware platform and operating system they are compiling for. The following macros may or may not be defined:

不幸的是,不是所有的编译器都被很好地宏定义来被告知他们所编译的目标平台是什么硬件平台和操作系统,下面的宏可能被定义也可能没有:
不同硬件平台宏定义.png
不同操作系统宏定义.png

可以看到__LP64__Linux 64 bit平台的的宏定义,而__arm64__应该是针对arm移动平台64位cpu的宏定义,从这两个线索我们就可以判断cache_t结构体的三个分支是如何走的了,根据我们的调试环境,是MAC系统,非__arm64__平台,所以就会进入CACHE_MASK_STORAGE_OUTLINED这个判断分支,我们就根据这个分支的情况去计算cache_t所占的内存大小:

//只看第一个分支中的内容即可
struct cache_t {  
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED  
    explicit_atomic<struct bucket_t *> _buckets;  //*指针, 占8字节
    explicit_atomic<mask_t> _mask;  //mask_t 是unsigned int 的别名, 占4字节
    .....
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
}

其中explicit_atomic是个模板结构体,它的长度由struct bucket_t *类型确定,是一个指向结构体的指针,所以是8字节,同样_mask的长度由mask_t确定,它的定义是typedef uint32_t mask_t;,是一个nsigned int占4字节,剩下的静态成员不计算在结构体内部,因为编译后的静态变量存储在静态区,最后还有两个成员_flags_occupied,为什么要计算_flags,因为有#if __LP64__的宏判断,我们调试的环境是MAC系统,和Linux都是类Unix系统,有许多相似之处,而且是64位平台,所以会有这个判断,要计算uint16_t _flags;的长度,uint16_t定义为typedef unsigned short uint16_t;,所以_flags是2字节,最后_occupied也是2字节,最终计算struct cache_t共占用8+4+2+2 = 16字节,后面我们可以同过lldb调试来验证。
那么要想知道objc_classclass_data_bits_t bits;中的内容, 只需通过类的首地址, 偏移isa+superclass+cache的长度,共平移32字节就可以得到了:

//打印Person类内存布局
(lldb) x/4gx Person.class
0x1000020e8: 0x00000001000020c0 0x0000000100334140
0x1000020f8: 0x000000010032e440 0x0000801000000000
//查看首地址0x1000020e8是否为Person类
(lldb) po 0x1000020e8
Person
//将0x1000020e8偏移32位,16进制进位后变成0x100002108,并强转为class_data_bits_t *打印,拿到bits首地址
(lldb) p (class_data_bits_t *)0x100002108
(class_data_bits_t *) $2 = 0x0000000100002108

上面的打印结果中显示了如何通过内存偏移拿到我们想要的class_data_bits_t bits内存地址,接下来就要看看class_data_bits_t bits中的内容了,如何分析他内部存放了哪些东西,先看看他的结构体定义:

struct class_data_bits_t {
    ....
public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ....
};

省略掉非关键的代码,可以看到结构体内部定义了一个data()方法,返回一个class_rw_t的指针,用lldb打印看看这个class_rw_t是什么:

//打印0x0000000100002108指针内容,得到class_data_bits_t结构体
(lldb) p *$2
(class_data_bits_t) $3 = (bits = 4301869812)
//执行class_data_bits_t结构体data()方法,获取到成员class_rw_t的指针
(lldb) p $3->data()
(class_rw_t *) $5 = 0x00000001006952f0
  Fix-it applied, fixed expression was: 
    $3.data()
//打印class_rw_t指针的内容,可以得到
(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975600
  }
  firstSubclass = Teacher
  nextSiblingClass = NSUUID
}
(lldb) 

通过lldb打印,可以看到class_data_bits_t结构体的data()方法返回了class_rw_t类型的数据,其中包含了类的很多关键信息,有firstSubclass就是他的子类Teacher。我们再来看看class_rw_t的内部结构:

//objc_runtime_new.h中class_rw_t定义,非关键代码已略
struct class_rw_t {
    ....
    Class firstSubclass;
    Class nextSiblingClass;

private:
   ....
    const method_array_t methods() const {
        ....
    }

    const property_array_t properties() const {
        ....
    }

    const protocol_array_t protocols() const {
        ....
    }
};

这里面就很清楚的看到methods()properties()protocols(),顾名思义,这里就是存放类方法表,属性表以及协议的地方。下面看看是否能用lldb打印出Person类的方法表和他的属性列表:

//拿到Person类的class_rw_t
(lldb) p *$5->data()
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975616
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
//调用class_rw_t的methods()方法
(lldb) p $6.methods()
(const method_array_t) $24 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020c8
      arrayAndFlag = 4294975688
    }
  }
}
//得到method_array_t,获取method_array_t的list首地址指针
(lldb) p $24.list
(method_list_t *const) $25 = 0x00000001000020c8
//打印method_list_t中的内容,可以看到第一个就是doSomeThing方法
(lldb) p *$25
(method_list_t) $26 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "doSomeThing"
      types = 0x0000000100000f8a "v16@0:8"
      imp = 0x0000000100000dd0 (KCObjc`-[Person doSomeThing])
    }
  }
}
//再来打印propertylist看看Person类的属性
(lldb) p $6.properties()
(const property_array_t) $27 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002158
      arrayAndFlag = 4294975832
    }
  }
}
//打印property_array_t的list指针
(lldb) p $27.list
(property_list_t *const) $28 = 0x0000000100002158
//打印list的内容,可以看到Person类声明的属性name
(lldb) p *$28
(property_list_t) $29 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
  }
}
(lldb)

总结:在新版本的OC源码中(本文使用的是objc4-781),通过上面的分析,我们验证了类的isa走向,以及类的superclass走向,结构体objc_class的类信息放在了class_data_bits_t bits;class_rw_t中,里面存放了类的firstSubclassmethods()、properties()、protocols()等关键信息。

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