iOS Runtime

简书内容都是个人的知识点整理和笔记。

iOS Runtime理解是我们每一个iOS开发者在深刻掌握Objective-C这门语言的必经之路。在深入学习Runtime之前我们需要清楚认识一件事:Objective-C是一门动态语言,它将类的类型和数据变量的类型都是在运行时确定的。
作为最典型的运行时机制,Objective-C代码在程序运行的过程中都转换成了Runtime中的C语言代码。

1.定义

在进一步了解Runtime之前,我们需要首先掌握一些基础定义。

Class

如下面代码所示Objective-C中Class类是一个指向objc_class结构体的指针。

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *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;

所有类对象都有一个名为“isa”的指针,这个指针指向一个objc_class结构体,objc_class结构体中包含存储了变量列表、方法列表、遵守的协议列表等等信息。

Meta Class

我们在描述Class时指出所有类对象都有一个指向objc_class结构体名为“isa”的指针。而这个结构体中又存在着一个isa指针,这个指针指向的就是MetaClass,元类。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表objc_method_list中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
这种关系由如下图片所示:


消息发送

id

id是我们在编程中广泛用到的数据类型,它的实质是一个指向类实例的指针,只是一个指针。

/// A pointer to an instance of a class.
typedef struct objc_object *id;

SEL

在Objective-C编译时,根据每一个方法生成一个整型标识地址,这个标识就是SEL。本质上SEL只是一个指向方法的指针。

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

IMP

IMP是一个指向方法实现的指针。在定义中我们可以看见一个方法的指针由方法的实例指针和标志地址构成。我们在上面提到SEL只是一个指向方法的指针,它的存在是为了加快方法的查询速度,这个查到的对象就是IMP。

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

2.消息发送

在Objective-C中,一个对象调用方法,是因为接收到了一个消息时,然后开始进入正常的方法调用流程。方法调用的本质,就是让对象发送消息。消息直到运行时才会与方法实现进行绑定。
在运行时,默认情况下编译器会将消息表达式转化为一个消息函数的调用,即objc_msgSend。如果object无法响应message消息时,编译器会报错。但如果是以perform的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

我们来看一看objc_msgSend函数

objc_msgSend(void /* id self, SEL op, ... */ )

这个函数将消息接收者和方法名作为其必须参数,将方法其余参数作为可选参数与实现方法进行绑定。首先它找到SEL对应的方法实现,然后根据接收者的类来找到方法的确切的实现,即前文我们提到的IMP指针。之后将接收者对象及方法的所有参数传给它,最后实现返回的值作为它自己的返回值。
当一个对象接收到方法时objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector,一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程。其流程如下图所示:


方法调用流程

特别注意的是:当消息发送给一个对象时首先从运行时系统缓存使用过的方法中寻找。这就是我们前文叙述的objc_class结构体中的objc_cache的使用。

消息转发

在消息发送中我们提到,当对象无法接收到消息时,就会走消息转发流程。消息转发流程的目的就是让我们告诉对象如何处理未知的消息,而在默认情况下,对象无法接收到消息时程序便会崩溃,就是我们最常见的:

unrecognized selector sent to instance xxxxxxx

但在系统发出警告之前,Runtime会发送给对象一条"forwardInvocation"消息,该条消息里面含有一个囊括了所有方法细节的NSInvocation类对象作为参数。你可以通过实现"forwardInvocation:"方法转发给其他对象,或是用其他方法规避这个错误。
为了转发这个消息"forwardInvocation:"需要完成这两件事:
(1)确定这条消息转发的对象;
(2)将消息和参数发送到选中的对象。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

消息转发与多重继承

通过消息转发,我们将接收消息的对象与转发对象建立起了某种关系,表面上看任然是原对象在处理消息,但这种方式更像是一种对多重继承的模拟。
一个对象响应消息的转发通过模仿“继承”的方法实现定义在另一个类,如下图所示:


Forwarding

Warrior对象通过实现forwardInvocation:将调用negotiate方法的消息转发给Diplomat对象,最后Diplomat对象调用了negotiate方法。从代码上讲,最终是
Diplomat实现了对negotiate方法的调用,但也像是Warrior继承了Diplomat对象从而实现了对方法的调用。

动态方法解析

有些时候你希望可以动态的提供一些方法接口,就像是@dynamic修饰词一样,由我们自己来生成setter和getter方法。在这里我们可以通过实现resolveInstanceMethod:或resolveClassMethod:方法分别为实例和类方法的给定的选择题动态地提供实现。

+ (BOOL)resolveInstanceMethod:(SEL)sel;

+ (BOOL)resolveClassMethod:(SEL)sel;

消息转发和动态方法是正交的,所谓正交是指一个类有机会在转发机制启动之前动态地解析一个方法。

备用接收者

在进行消息转发前,如果已知一个对象实现了方法,那么在进行消息转发之前我们还可以直接指定该对象为备用的消息接受者。
指定一个备用的消息接受者使用的方法是:

- (id)forwardingTargetForSelector:(SEL)aSelector;

该方法在消息转发前给你提供了一个更快更轻量的方案。

总结:当一个对象无法接收某一消息时,就会启动消息转发,但在这之前我们可以采取一些措施,避免程序的崩溃。我们可以将这个步骤归纳为:
(1)动态方法解析
(2)备用接收者
(3)完成转发


消息转发

3.关联对象

我们在 iOS 开发中经常需要使用类别(Category),为已经存在的类扩展方法,但当我们想要添加某些属性时一般都会去继承这个类,我们也可以使用Runtime的关联属性去实现。
关联对象我们主要适用以下三个方法:

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
objc_removeAssociatedObjects(id _Nonnull object);

设置关联

objc_setAssociatedObject这个方法的作用是通过key和关联策略为给定对象设置关联值。

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);
//object: 被设置关联值得对象。
//key: 关联值的Key
//value: 关联值
//policy: 关联策略

其中关联策略objc_AssociationPolicy包含如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /** 指定关联对象的弱引用。 */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定对关联对象的强引用,非原子操作*/
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /** 指定关联的对象的拷贝,非原子操作*/
    OBJC_ASSOCIATION_RETAIN = 01401,       /**指定对关联对象的强引用*/
    OBJC_ASSOCIATION_COPY = 01403          /** 指定关联的对象的拷贝*/
};

对于如何选择关联策略和我们平时设置属性选择assign,strong,copy以及atomic和nonatomic是一样的。

atomic和nonatomic区别用来决定编译器生成的getter和setter是否为原子操作。atomic提供多线程安全,是描述该变量是否支持多线程的同步访问,如果选择了atomic 那么就是说,系统会自动的创建lock锁,锁定变量。nonatomic禁止多线程,变量保护,提高性能。

获取关联

objc_getAssociatedObject这个方法是通过key获取某被关联对象的关联值。

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//object: 同objc_setAssociatedObject
//key: 同objc_setAssociatedObject

移除关联

objc_removeAssociatedObjects移除被关联对象的所有关联值,注意,是所有!

这个是本文章DEMO的地址:Github

参考资料

Objective-C Runtime Programming Guide - Message
Objective-C Runtime Programming Guide - Message Forwarding
Objective-C Runtime Programming Guide - Type Encodings
详解Objective-C的isa与meta-class
iOS运行时(Runtime)详解+Demo
iOS~runtime理解

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

推荐阅读更多精彩内容