Runtime基础元素解析

Objective-C的runtime语言使它具备了动态语言的特性,也就是平时所说的“运行时”。在runtime的基础上,可以做很多平时难以想到事,或者化简原先 较为繁杂的解决方案。

相对于静态语言,比如C的以下程序

#include 
void run()
{}
int main()
{
    return 0;
}

执行clang -c进行编译后,获取符号表nm run.o,可以得到全局唯一的符号_run,对函数run的调用直接参考链接后_run符号在代码段的地址

0000000000000010 T _main
0000000000000000 T _run

对比Objective-C的以下函数

@implementation Dog : NSObject
- (void)run
{}
@end

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

执行clang -rewrite-objc main.m将其转换成底层C++文件后可以得到

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

可以看到,对Objective-C编译前期,会将内部的方法调用,转换成调用objc_msgSend。也就是说,编译完成后,方法地址是不能确定的,需要在运行时,通过Selector进行查找,而这正是runtime的关键,也就是发送消息机制。

runtime的基本要素

如上面例子所示,在编译后[dog run]被编译器转化成了

((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));

// 假设能省略(void (*)(id, SEL))(void *)和id指针强转[实际上还是需要的]
// sel_registerName表示注册一个selector
objc_msgSend(dog, sel_registerName("run"));

将上面的情况抽取成统一的说法就是,在编译器编译后[receiver message]会被转化成以下形式

objc_msgSend(receiver, selector)

objc_msgSend是一个消息发送函数,它以消息接收者和方法名作为基础参数。

在有参数的情况下,则会被转换为

objc_msgSend(receiver, selector, arg1, arg2, ...)

消息的接收者receiver在接受到消息后,查找对应selector的实现,根据查找的结果可以进行若干种种不同的处理。

更深层的了解,需要了解下对应的数据结构

id

上文中objc_msgSend的第一个参数有个强转类型,即id。id是可以指向对象的万能指针,查看runtime源码,得知其定义如下:

typedef struct objc_object *id;

// objc_object
struct objc_object {
private:
    isa_t isa;
}

// isa_t
union isa_t
{
    Class cls;
    uintptr_t bits;
}

根据union联合的存储空间以大成员的存储空间计算性质,可以猜测isa_t的作用只是真不同位数处理器的优化,我们可以直接这样表示:

struct objc_object {
private:
    Class isa;
}

可以看出,id是一个指向objc_object结构体的指针(注意,在runtime中对象可以用结构体进行表示)。

objc_object结构体包含了Class isa成员,而isa就是我们常说的创建一个对象时,用来指向所属类的指针。因此根据isa就可以获取对应的类。

  • 注:C++中结构的作用被拓宽了,也表示定义一个类的类型,struct和class的区别就在默认类型上一个是public,一个是private,这里就直接描述为结构体了

Class

上文中,isaClass类型,而Class则是objc_class指针类型的别名:

typedef struct objc_class *Class;

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
}

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

// class_rw_t
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    union {
        method_list_t **method_lists;  // RW_METHOD_ARRAY == 1
        method_list_t *method_list;    // RW_METHOD_ARRAY == 0
    };
    struct chained_property_list *properties;
    const protocol_list_t ** protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

// class_ro_t
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;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    const property_list_t *baseProperties;
};

在上文中已经介绍过objc_object结构体,objc_class继承自结构体objc_object。可以看出objc_objectisaprivate类型成员变量,objc_class继承后无法访问,所以objc_object提供了以下两个成员函数:

Class ISA();

// getIsa内部调用ISA返回isa_t联合中cls成员
Class getIsa();

所以,对objc_class重要的成员变量进行下解释:

  • isa为指向对象对应类的指针(这里注意一点,由于类也是一个对象(单例),所以这个单例中也有一个isa指针指向类对象所属的类->metaClass,即元类)

  • superclass为指向父类的指针

  • cache用于对调用方法的缓存,类似CPU先访问L1、L2、L3缓存的目的相似,它也是推断最近调用的方法极有可能被二次调用,并将其存入cache,在二次调用时先在cache查找方法,而不是直接在类的方法列表中查找

  • properties为属性列表

  • protocols为协议列表

  • method_lists/method_list为方法列表

  • ivars为成员变量列表

  • class_ro_t结构体中存储的都是类基本的东西,比如获取'load'方法时,是从baseMethods获取相应的IMP函数实现的:

IMP objc_class::getLoadMethod()
{
rwlock_assert_locked(&runtimeLock);

const method_list_t *mlist;
uint32_t i;

assert(isRealized());
assert(ISA()->isRealized());
assert(!isMetaClass());
assert(ISA()->isMetaClass());

mlist = ISA()->data()->ro->baseMethods;
if (mlist) {
    for (i = 0; i < mlist->count; i++) {
        method_t *m = method_list_nth(mlist, i);
        const char *name = sel_cname(m->name);
        if (0 == strcmp(name, "load")) {
            return m->imp;
        }
    }
}

return nil;

}


其中先了解下`ivar_list_t`、`method_list_t`、`cache_t`的结构定义:


`ivar_list_t`的结构为:

  - `ivar_t`就是对应的成员变量

```objc
struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

method_list_t为:

  • 其中method_iterator为结构体自己构造的一个迭代器,用来访问方法,可以看到,构造的迭代器结构体中包含了method成员变量
struct method_list_t {
    uint32_t entsize_NEVER_USE;  // high bits used for fixup markers
    uint32_t count;
    method_t first;

    // iterate methods, taking entsize into account
    // fixme need a proper const_iterator
    struct method_iterator {
        uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-
        method_t* method;
    ...
    }

cache_t为:

  • 可以看出bucket_t包含了一个IMP类型的私有成员,供查找后调用实现
  • _occupied_mask分别表示实际占用的缓存_buckets总数和分配的缓存_buckets总数
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
...
}

// bucket_t
struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
...
}

上文还涉及到了一个概念metaClass元类,元类为类对象所属的类,以实例解释:

当我们调用类方法时,消息的接收者即为类,如文中一开始的代码:

Dog *dog = [[Dog alloc] init];

这里的alloc消息即发送给了Dog类,编译转换后的代码为:

Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));

我们只需要关注这一行:

  • 这里获取到的是类对象,只要再获取一次就得到了元类
// objc_getClass表示根据对象名获取对应的类
objc_getClass("Dog")

// 获取元类
objc_getClass(objc_getClass("Dog"))

关于元类,苹果提供了这么一张表:

对象-类-元类-超类

图中的实线是superclass指针,虚线是isa指针。可以看到,根元类的超类NSObject(Root class)并没有对应的超类,并且,它的isa指针指向了自己。
总结一下:

  • 每个实例对象的isa都指向了所属的
  • 每个类对象的isa都指向了所属的类,即元类,其superclass指针指向继承的父类
  • 每个元类的isa都指向了超类,即NSObject

Ivar

Ivar,我把它理解成instance variable,也就是实例变量,可以观察它的定义:

typedef struct ivar_t *Ivar;

// ivar_t
struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
    // 内存中数据对齐(如字对齐、半字对齐等)
    uint32_t alignment() {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

Ivar其实是指向ivar_t结构体的指针,它包含了实例变量名(name)、类型(type)、相对对象地址偏移(offset)以及内存数据对齐等信息。

跟多关于实例变量的剖析可以查看Objective-C类成员变量深度剖析

Method

从以下定义的结构体可以看出,Method主要住用为关联了方法名SEL和方法的实现IMP,当遍通过Method自己的定义的迭代器查找方法名SEL时,就可以找到对应的方法实现IMP,从而调用方法的实现执行相关的操作。types表示方法实现的参数以及返回值类型。

typedef struct method_t *Method;

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

SEL

SEL为方法选择器,观察下它的定义:

typedef struct objc_selector *SEL;

可以看出SEL实际是objc_selector指针类型的别名,它用于表示运行时方法的名字,以便进行方法实现的查找。因为要对应方法实现,所以每一个方法对应的SEL都是唯一的。因此它不具备C++可以进行函数重载的特性,当两个方法名一样时,会发生编译错误,即使参数不一样。

IMP

IMP的定义如下:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

可以看出IMP其实就是一个函数指针的别名,也可以把它理解为函数名。它有两个必须的参数:

  • id,为self指针,表示消息接收者
  • SEL,方法选择器,表示一个方法的selector指针
  • 后面的为传送消息的一些参数

在某些情况下,通过获取IMP而直接调用方法实现,可以直接跳过消息传递机制,像C语言调用函数那样,在一定程度上,可以提供程序的性能。

消息传递

了解完runtime中一些必要的元素,继续回到文章开头的代码:

@implementation Dog : NSObject
- (void)run
{}
@end

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

编译器将其转换成了:

  • 为了看起来简洁点,我把一些强制转换变为别名
typedef (Dog *(*)(id, SEL))(void *) MyImp;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((MyImp)objc_msgSend)((id)((MyImp)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((MyImp)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

从上面的代码可以看出,第二个objc_msgSend返回值是作为第一个objc_msgSend的首个参数的。

上文已经说过,[receiver message]会被转化成以下形式

objc_msgSend(receiver, selector, ...)

接下来看看它主要做了哪几件事情:

  • 根据receiverisa指针,获取到所属类,先在类的cache即缓存中查找selector,如果没有找到,再在类的method_lists即方法列表中查找
  • 如果没有找到selector,则会沿着下图类的联系路径一直查找,直到NSObject
  • 如果找到了selector,则获取实现方法并调用,并传入接收者对象以及方法的所有参数;没有找到时走方法解析和消息转发流程。
  • 将实现的返回值作为它自己的返回值

方法列表查找路径

除此之外,objc_msgSend还会传递两个隐藏参数:

  • 消息接收对象(self引用的对象)
  • 方法选择器(_cmd,调用的方法)

objc_msgSend找到方法实现后,会在调用该实现时,传入这两个隐藏参数,这样就能够在方法实现里面里面获取消息接受对象,即方法调用者了。

隐藏参数表示这两个参数在源代码方法的定义中并没有声明这两个参数,这两个参数是在代码编译期间,被插入到实现中的。

self和super的联系

根据上文对objc_msgSend的了解,可以解决以下代码输出一致问题

@implementation Dog : NSObject

- (void)run
{
    NSLog(@"%@", [self class]);
    NSLog(@"%@", [super class]);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];

        [dog run];
    }
    return 0;
}

输出为:

[5491:173185] Dog
[5491:173185] Dog

这是为什么呢?先来看看编译后的-run方法的情况:

static void _I_Dog_run(Dog * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class")));
}

这里面只要关注两句:

// [self class]
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))

// [super class]
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class"))

首先我们需要了解selfsuper的差异:

  • super编译标识符,告诉编译器,调用方法时,去调用父类的方法,而不是本类的方法
  • self隐藏参数,每个方法的实现第一个参数就是self

这里可以看出,编译后,经过super标识符修饰的方法调用,会调用objc_msgSendSuper函数来进行消息的发送,而不是objc_msgSend。先来了解下objc_msgSendSuper的声明:

id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

其中objc_super的定义为:

// receiver   消息实际接收者
// class      指向当前类的父类
struct objc_super { id receiver; Class class; };

结合以上信息,我们可以知道:

(__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }

就是对结构体objc_super的赋值,也就是说objc_super->receiver=self。到这里可能就有点明了了,super只是告诉编译器,去查找父类中的class方法,当找到之后,使用objc_super->receiverself进行调用。用流程表示就是:

[super class]->objc_msgSendSuper(objc_super{self, superclass)}, sel_registerName("class"))->objc_msgSend(objc_super->self, sel_registerName("class"))=[self class]

可以看出两者输出结果一致的关键就是,[self class]的消息接收者和[super class]的消息接收者一样,都是调用方法的实例对象。

方法解析和消息转发

当上文objc_msgSend处理流程中,selector没有找到时,会触发三个阶段,在这三个阶段都可以进行相关处理使程序不抛出异常:

  • Method Resolution (动态方法解析)
  • Fast Forwarding (备用接收者)
  • Normal Forwarding (完整转发)

由于实际代码中很少有看到这种操作,所以这里不做详细解释,参考这个资料即可Objective-C Runtime 运行时之三:方法与消息

参考

1.Objective-C Runtime 运行时之一:类与对象

2.Objective-C Runtime

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,125评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 721评论 0 2
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,521评论 33 466
  • 贪念 是你此生结下的缘 不知何时是何缘由你贪婪成性 不知何地你贪婪成名 贪念 是你燃烧的欲望之火 让你爱上人间这个...
    蓝色汪星人阅读 335评论 0 2