Runtime源码 —— 方法加载的过程

在上一篇文章中分析过类的结构体,是这个样子的:

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

那一篇主要是分析isa的源码,这些字段并没有深究,这一篇就来深入研究一下。我还是会先对源码进行分析,再结合例子进行验证。

从字面上来看,前两个字段的意思是很容易理解的:

  • Class superclass
    父类的指针。
  • cache_t cache;
    一个缓存,官方文档注释写明了用于缓存指针和虚表。但这个缓存是如何起作用在后续的文章中再讲。

下面就来重点看一看这个不太看得懂的字段:

  • class_data_bits_t bits
    注释中讲了这个字段实际上就是class_rw_t *加上自定义的rr/alloc标志,rr/alloc标志是指含有这些方法:retain/release/autorelease/retainCount/alloc等。

那么就来看看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;
}

除开明显的方法,属性,协议等字段,这个结构体中有一个奇怪的字段,const class_ro_t *ro;这个结构体是这样定义的:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    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;
}

感觉这两个结构体还是比较类似的,这时候合理猜测一下,rw应该是指readwrite,ro是指readonly。也就是说在可读可写的结构体中存放了一个只读的结构体,而且这两个结构体很相似。

结合oc是一门动态语言再猜测一下,class_ro_t是不是存放在编译期就确定的信息,class_rw_t用来存放在运行期添加的信息呢?只能通过代码来验证一下。

注:这里面还有一些很关键的字段,比如instanceStart,instanceSize。本文不做扩展。

例子

首先把之前的TestObject扩展一下,添加一个hello方法:

// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject

- (void)hello;

@end

// TestObject.m
#import "TestObject.h"
@implementation TestObject

- (void)hello {
    NSLog(@"hello");
}

@end

接着获取一下TestObject在内存中的地址:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%p", [TestObject class]);
    }
    return 0;
}

输出:0x100001168

只要代码不变,这个类在内存中的地址就不会变

这个时候在void _objc_init(void)添加一个断点:


1.png

然后利用上面的地址就可以看一看这个时候class_ro_t的内容。

(lldb) p (objc_class *)0x100001168
(objc_class *) $0 = 0x0000000100001168
// 根据objc_class的结构,isa:8字节,superclass:8字节,cache:16字节
// 所以偏移32字节来获取class_data_bits_t
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $1 = 0x0000000100001188
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001000010e8
// 在这个时候,class_rw_t实际上是class_ro_t,后面会验证
(lldb) p (class_ro_t *)$2
(class_ro_t *) $3 = 0x00000001000010e8
(lldb) p *$3
(class_ro_t) $4 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x0000000100000f99 "TestObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000000000000
}

可以看到class_ro_t结构体中name和baseMethodList已经有内容了,可以打印一下看看是不是TestObject类中的hello方法:

(lldb) p $4.baseMethodList
(method_list_t *) $5 = 0x00000001000010c8
(lldb) p $5->get(0)
(method_t) $6 = {
  name = "hello"
  types = 0x0000000100000fb0 "v16@0:8"
  imp = 0x0000000100000eb0 (debug-objc`-[TestObject hello] at TestObject.m:13)
}

没有问题,从name可以看出就是hello方法。method_t结构体也非常简单:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
}

types那一串乱七八糟的字符串可以参考苹果的文档:Type Encodings

扯远了,刚刚验证了在编译期,类的相关信息会存放到class_ro_t中,那么看看运行期是如何把信息添加到class_rw_t中的。这时候就需要看看这个方法了:static Class realizeClass(Class cls)

为什么会突然跳到这个方法,中间过程有些复杂,概括来说就是在研究void _objc_init(void)方法时,通过这么个路径(省略函数签名)map_2_images() -> map_images_nolock() -> _read_images() -> realizeClass(),看到了这个方法,主要是看到了这个方法的注释:

* realizeClass
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller

这里面提到的read-write data指的就是class_rw_t了,当然实际方法的调用栈并不是上面的路径。在realizeClass方法中添加一个断点:


2.png

左侧可以看到方法的调用栈,切换到4那一步:


3.png

可以看到是因为在main函数中打印log时调用了class方法,才一步步进入了realizeClass方法。

猜测:某个类的realizeClass方法是在类被首次调用的时候才会调用。

关于方法的调用,或者说是消息的转发,并不是本文的重点,下一篇讲消息转发机制的时候再具体说。下面看看realizeClass方法是如何实现的:

static Class realizeClass(Class cls)
{
    const class_ro_t *ro;
    class_rw_t *rw;
    ...
    ro = (const class_ro_t *)cls->data();
    // Normal class. Allocate writeable class data.
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    ...
    methodizeClass(cls);

    return cls;
}

删掉了不少代码,只看关键部分,首先看到这一步:

  • ro = (const class_ro_t *)cls->data();
    验证了之前我们的猜想,在这个时候,class_rw_t实际上class_ro_t,所以有这一步强转。
  • rw->ro = ro;
    把ro赋值给rw中的ro字段
  • cls->setData(rw);
    最后把rw赋值回去,这一步完成之后rw和ro就被正确的设置了,但rw中的方法、属性、协议列表还是空的。
  • methodizeClass(cls);
    这一步会把ro中的方法、属性、协议拷贝到rw中。另外会把此类所有的category中附加的方法、属性、协议也拷贝进去。oc之所以能在运行时做各种事情,其实都是基于runtime的这些支持。

看看关键的methodizeClass(cls)方法是如何实现的:

static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    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_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}

这个方法结构还是很清楚的,就是通过attachLists()方法把ro中的内容拷贝到了rw中,最后通过attachCategories()方法把分类中的内容也添加进去,这里就不再深挖了。

在methodizeClass()方法结束后,rw中的方法、属性、协议就有内容了,用代码验证一下:

(lldb) p (objc_class *)0x100001168
(objc_class *) $5 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $6 = 0x0000000100001188
(lldb) p $6->data()
(class_rw_t *) $7 = 0x00000001008022e0
// 之前在这里强转成class_ro_t,现在这个时候已经不需要了,直接获取属性
(lldb) p $7->methods
(method_array_t) $8 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000010c8
      arrayAndFlag = 4294971592
    }
  }
}
// method_array_t结构体中有如下的方法来获取method_list_t的二维数组
(lldb) p $8.beginCategoryMethodLists()[0][0]
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "hello"
      types = 0x0000000100000fb0 "v16@0:8"
      imp = 0x0000000100000eb0 (debug-objc`-[TestObject hello] at TestObject.m:13)
    }
  }
}

可以看到这个时候hello方法已经存在了。

总结

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

推荐阅读更多精彩内容