runtime(运行时)

本文分为4个部分

1.介绍OC和C语言之间的转换

2.介绍运行时和相关术语

3.介绍消息发送机制已及怎样找到函数实现

4.runtime的应用

一、OC和C语言之间的转换


Apple 官方对OC的定义


Objective-C is the primary programming language you use when writing software for OS X and iOS. It’s a superset of the C programming language and provides object-oriented capabilities and a dynamic runtime. Objective-C inherits the syntax, primitive types, and flow control statements of C and adds syntax for defining classes and methods. It also adds language-level support for object graph management and object literals while providing dynamic typing and binding, deferring many responsibilities until runtime


总结:OC = C+runtime+面向对象;


C语言是编译型静态语言,也就是说C语言编译完后,是不可以更改代码的结构的,如更改某个方法的实现,如调用eat(),但执行的确实run()的实现。

而OC虽然是基于C的但是因为有了runtime机制,却是编译型动态语言,这种语言可以动态的创建类,属性以及交换方法的实现,这种交换方法实现有个热门词叫Method Swizzling(俗称黑魔法)。

  runtime对OC来说,类似操作系统的功能,很多方法如kvc、kvo等功能因为有运行时才得以实现。可以说 C语言和面向对象特性是OC的身体,而Runtime才是OC的灵魂。


二、运行时和相关术语

1.运行时是什么?

运行时是用Apple提供给OC的一套用C语言编写的底层API,它属于1个C语言库,平时我们编写的OC代码最终都转换成了

运行时的C语言代码。

当在 OC 中使用方法 是这样的:

[self doSomething];

实际上会被转化为调用objc_msgSend函数,给某个对象发送消息:

objc_msgSend(self, @selector(doSomething));

佐证:1、苹果官方文档


In Objective-C, messages aren’t bound to method implementations until runtime. The compiler converts a message expression,


[receiver message]


into a call on a messaging function,objc_msgSend.


我们可以在这里查看官方文档



佐证:2 编译OC代码

    编译前


编译后


2、相关术语

我们通过 objc_msgSend来了解runtime的相关术语

它的声明是这样的:


// message.h


id objc_msgSend(id self, SEL op, ...);


那具体是怎样转换的?OC 中的类、方法、属性等在 C 语言中是怎样被表示的?下面先看下一些相关术语。

1、SEL

SEL是转换后的函数中第二个参数的类型,它的对象selector(方法选择器) ,顾名思义,是用来识别和选择要执行的 OC 方法的。它的定义如下:


// objc.h


typedef struct objc_selector *SEL;




其实它就是个映射到方法的 C 字符串,上面就是通过@selector(doSomething)来获取一个名字叫 doSomething 的selector。

2、id

id 是转换后的函数中第一个参数的类型,它在 OC 中被称为万能指针,可以指向任何类的实例。它的定义如下:


// objc.h


typedef struct objc_object *id;


struct objc_object {


Class isa; 


};


objc_object结构体中的第一个元素是isa指针,根据它可以找到对象所属的类。(但要注意在 KVO 中 isa 指针指向的是一个中间类了,kVO利用运行时机制动态的创建了一个继承被观察类的中间类,通过对中间类属性变化,通知观察对象,这个应用我们在应用部分再详细解释

3、Class

上面说的 isa 的类型是Class,而它的定义如下:


// objc.h


typedef struct objc_class *Class;


// runtime.h


struct objc_class {


Class isa; // 类同样有自己父类,指向所属的父类


Class super_class;  // 父类指针


const char *name;  // 类名


long version;


long info;


long instance_size;


struct objc_ivar_list *ivars;  // 成员变量列表


struct objc_method_list **methodLists; // 方法列表


struct objc_cache *cache;  // 缓存


struct objc_protocol_list *protocols; // 协议列表


};


可见在 Runtime 系统中,一个类还关联了它的父类指针、类名、成员变量、方法、缓存、协议。

注意到不仅表示对象的objc_object结构体中有个isa指针,表示类的objc_class结构体中也有个isa指针,这是因为在 OC 中,类本身也是一个对象(类对象)。对象的方法存储在它所属的类中,那类的方法呢?这时就需要类对象所属的类来存储类方法了,它叫meta class(元类)。对象的类、父类、元类之间的关系如下(实现是 super_class 指针,虚线是 isa 指针):

注意到所有的元类的元类都是root class(meta),而这个根元类的元类是它自己,它的父类是NSObject;NSObject 的元类也是那个根元类,但它没有父类。


由上图可知 实例对象的isa都指向自己的类对象,类对象中的superClass指向自己的父类,类对象中的isa指向了元类。

这样清晰的继承关系,将会是 runtime 消息机制发送消息的基础。


4、成员变量

其中 objc_ivar_list 是成员变量列表,定义如下:


// runtime.h


struct objc_ivar_list {


int ivar_count;


int space;


struct objc_ivar ivar_list[1];//成员变量的数组


}


struct objc_ivar {


char *ivar_name; //单个变量的名字


char *ivar_type; //变量类型


int ivar_offset; //偏移量


int space;


}


typedef struct objc_ivar *Ivar;



可见成员变量列表objc_ivar_list结构体存储着由成员变量objc_ivar结构体组成的数组,objc_ivar结构体存储着单个成员变量的名字、类型、偏移量等信息。

注意:成员变量和属性的区别,属性可以1.生成成员变量 2.声明和实现set、get方法(在分类中除外,因为分类中不能用成员变量)

5、方法

objc_method_list是方法列表,定义如下:



// runtime.h


struct objc_method_list {


struct objc_method_list *obsolete;


int method_count;


int space;


struct objc_method method_list[1];


}


struct objc_method {


SEL method_name;


char *method_types;


IMP method_imp;


}


typedef struct objc_method *Method;


可见方法列表objc_method_list结构体存储着由方法objc_method结构体组成的数组,objc_method 结构体存储着单个方法的信息:名称(SEL类型的)、参数类型和返回值类型(method_types中)和具体实现(IMP类型的)。

6、IMP

IMP(method implementation,方法实现) 的定义是:


// objc.h


typedef id (*IMP)(id, SEL, ...);


所以它其实是一个函数指针,指向某个方法的具体实现。它的类型和objc_msgSend函数相同,参数中也都包含有 id 和 SEL 类型,这是因为一个 id 和 一个 SEL 参数就能确定唯一的方法实现地址。

7、Cache

在objc_class结构体中还有个指向objc_cache结构体的指针,它的定义如下:


// runtime.h


typedef struct objc_cache *Cache


// objc-cache.m


struct objc_cache {


// 当前能达到的最大 index


uintptr_t mask;


// 被占用的槽位。因为缓存是以散列表的形式存在,所以会有空槽


uintptr_t occupied;


// 用数组表示的 hash 表


cache_entry *buckets[1];


};


typedef struct {


SEL name;


void *unused;


IMP imp;


} cache_entry;


// _uintptr_t.h


typedef unsigned long uintptr_t;


所以它用来做缓存的,用buckets数组来存储被调用过的方法。因为一个方法被调用过,那它以后有可能还会被调用,所以将其存储起来,下次要找某方法先到缓存中找,如果找到的话,免去后面的寻找过程,速度虽然仍会比直接调用函数慢一点点,但已经有很大提升。

8、属性

还有我们常用的属性其实也是结构体,它的定义如下:


// runtime.h


typedef struct objc_property *objc_property_t;


typedef struct {


const char *name;


const char *value;


} objc_property_attribute_t;


// objc-runtime-new.h


typedef struct objc_property {


const char *name;


const char *attributes;


} property_t;


typedef struct property_list_t {


uint32_t entsize;


uint32_t count;


property_t first;


} property_list_t;




所以一个property_t结构包含了属性的名称和属性字符串。与属性相关的一些方法如下:


#define newproperty(p) ((property_t *)p)


// 返回协议中的属性列表,属性个数存储在参数 outCount 中


objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)


// 返回类中的属性列表,属性个数存储在参数 outCount 中


objc_property_t *class_copyPropertyList(Class cls_gen, unsigned int *outCount)


// 返回属性列表中的属性数组,属性个数存储在参数 outCount 中


static property_t **copyPropertyList(property_list_t *plist, unsigned int *outCount)


// 返回类中的特定名字的属性


objc_property_t class_getProperty(Class cls_gen, const char *name)


// 返回某个属性的名字


const char *property_getName(objc_property_t prop)


// 返回某个属性的属性字符串


const char *property_getAttributes(objc_property_t prop)





三、介绍消息发送机制已及怎样找到函数实现

OC是一种动态语言,它把编译和链接时候要做的很多事情移到了运行时去做,例如C语言中,源代码要想通过编译,函数声明和函数实现必须都存在,它应该在编译时就把函数实现的地址和函数名称关联了。但是OC是动态语言,没有方法(面向对象称为方法)实先,只有方法声明也是可以编译通过的,方法名称和方法实现是在运行时动态绑定的如官方所述(In Objective-C, messages aren’t bound to method implementations until runtime)。

那么问题来了,在运行时,方法是怎么被找到的呢?


使用某对象的方法,都是给这个对象发送消息,消息和方法实现直到运行时才会绑定。Runtime 系统会把使用方法转换为调用函数:

objc_msgSend(receiver, selector)

注意到此时函数多了两个参数:消息接收者、方法的 selector。这是每个方法调用时都会默认存在的隐藏参数。如果还有其他参数则是:

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

objc_msgSend 要做的事件有三件:


(1)找到 selector 对应的方法实现;


(2)调用该方法实现,并把消息接收者(如果有参数则加上那些参数)传给它;


(3)把方法实现的返回值传回去(它自己并没有任何返回值)。


其中第(1)件事的最关键的,具体过程如下:

(1)检查该selector是不是要忽略的;

(2)检查这个target是否为nil。在 OC 中给 nil 发送任何消息都不会出错,返回的结果都是 0 或 nil。

(3)开始找这个类的IMP。先在cache中找,找到则调到对应的方法实现中去执行。

(4)在cache中没找到,则在该类的方法分发表(dispatch table,即方法列表)中找,找到则执行。

(5)在该类的方法分发表中找不到,则到父类的分发表中找,再找不到则往上找,直到 NSObject 类为止。这两个过程的示意图如下:

如果找到,还会根据是否把消息传给父类、返回值是否数据结构而选择下面四个函数中的一个来调用:

// message.h

id objc_msgSend(id self, SEL op, ...)

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

void objc_msgSend_stret(id self, SEL op, ...)

void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...)

函数中的super关键字是指向objc_super结构体的指针,objc_super结构体定义如下:


struct objc_super {


// receiver 仍是 self 本身


__unsafe_unretained id receiver;


// 父类的类型


__unsafe_unretained Class super_class;


};


要注意的是如果想获取某个类的父类,要用cls->super_class或class_getSuperclass方法,而不应该用[super class],因为[super class]会变为objc_msgSend(objc_super->receiver, @selector(class)),即获得的是objc_super->receiver的类,跟[self class]的结果是一样的。

(6)动态方法解析(Dynamic Method Resolution):如果该类及其继承体系的分发表都没找到,则开始动态方法解析,这是 Runtime 系统在报错前给我们的第一次补救的机会,它会调用resolveInstanceMethod:或者resolveClassMethod:方法,所以我们可以在这两方法中分别用class_addMethod给某个类或对象的某个 selector 动态添加一个方法实现。

如在 main 函数中调用 Person 对象的一个 aMethod 方法:

Person *p = [[Person alloc] init];

[p aMethod];

它的 .h 和 .m 文件如下:


//  Person.h


#import


@interface Person : NSObject


- (void)aMethod;


@end


//  Person.m


#import "Person.h"


#import


// 要被动态添加的方法实现


void dynamicMethodIMP(id self, SEL _cmd) {


NSLog(@"dynamicMethodIMP");


}


@implementation Person


// 动态方法解析


+ (BOOL)resolveInstanceMethod:(SEL)sel {


// 如果是要被添加方法实现的 selector


if (sel  == @selector(aMethod)) {


// 给 self 的类的 sel 方法选择器动态添加方法实现 dynamicMethodIMP


class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");


// 返回 YES 后, Runtime 重新给对象发送 aMethod 消息,这次就可以找到 dynamicMethodIMP 方法实现并调用它了


return YES;


}


return [super resolveInstanceMethod:sel];


}


@end




(7)重定向:如果在上面的方法中不做处理或返回 NO,Runtime 系统在报错前还会给第二次补救机会,就是会调用forwardingTargetForSelector:方法索要一个能响应这个消息的对象,所以我们可以在这里返回另外一个能处理该消息的对象:1


//  Person.m


- (id)forwardingTargetForSelector:(SEL)aSelector {


// 如果是要被添加方法实现的 selector


if (aSelector == @selector(aMethod)) {


// 返回另外一个对象,让它去接收该消息


return [[Car alloc] init];


}


return [super forwardingTargetForSelector:aSelector];


}


上面返回的是一个 Car 对象,如果 Car 类定义如下:


//  Car.h


#import


@interface Car : NSObject


- (void)aMethod;


@end


//  Car.m


#import "Car.h"


@implementation Car


- (void)aMethod {


NSLog(@"car aMethod");


}


@end


则输出结果就是 “car aMethod”了。

(8)消息转发:如果在上一步中不做处理或者返回 nil 或 self,则 Runtime 系统会在报错前给我们最后一次补救机会。系统会先调用methodSignatureForSelector:方法,在该方法返回一个包含了消息的描述信息的方法签名(NSMethodSignature对象),并用此方法签名去生成一个NSInvocation对象,然后调用forwardInvocation:方法并把刚生成的NSInvocation对象作参数传进去。我们就可以重写forwardInvocation:方法,在这里将消息转发给其他对象:

// 获取一个方法签名,用于生成 NSInvocation 对象


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {


NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];


if (!signature) {


signature = [[Car new] methodSignatureForSelector:aSelector];


}


return signature;


}


- (void)forwardInvocation:(NSInvocation *)anInvocation {


// 如果另一个对象能响应该方法


if ([[Car new] respondsToSelector:[anInvocation selector]]) {


// 则让另一个对象来响应该方法


[anInvocation invokeWithTarget:[Car new]];


} else {


[super forwardInvocation:anInvocation];


}


}


尽管消息转发的效果类似于多继承,让一个对象看起来能处理自己不拥有的方法,但 NSObject 类不会将两者混淆。如上面的例子,[p respondsToSelector:@selector(aMethod)]的结果还是NO。

PS: 上面调用的方法顺序也可以这样获得:在程序启动之后暂停,然后在 gdb 中输入这个命令:call (void)instrumentObjcMessageSends(YES),再运行,则发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。如新建一个Teacher类,给它的实例发送一条错误的消息:



Teacher *teacher = [[Teacher alloc] init];


[teacher aMehtod];




四、runtime的应用

1.归档和解档(NSCoding)

利用runtime函数 遍历模型的所有属性

代码如下图

归档代码


接档代码



调用代码


结果




优势:如果模型对象比较多,不用一个个复制

注意:因为runtime是C语言文件,它的内存不属于ARC管理因此涉及到copy等方法时要手动管理内存。


2.Method Swizzing

这个就是俗称“黑魔法”,其本质在于交换两个函数的IMP,从而达到调用A方法但是执行的却是B方法,其具体实现和原理参考这篇OC中hook方案(一):method swizzling 文章



3.关联属性

提示:MJRefresh 里面的UIScrollerView 本身就是利用关联属性在category 里面添加 header 和 footer的。


我们还可能希望给某些常用的类添加 category,但 category 是只能添加方法而不能添加存储属性的。或者更严谨的说是分类不允许添加成员变量,因为属性的作用就是

1.生成下划线成员变量

2.声明和实现set 和get方法

因为属性不能添加成员变量所以就不能实现添加属性

现在我们可以用 Runtime 来间接在 category 添加属性了,如在给 UIButton 的 category 中添加一个属性作回调:


//  UIButton+Extension.h

#import <UIKit/UIKit.h>

typedef void (^CallbackBlock)();

@interface UIButton (Extension)

@property (copy, nonatomic) CallbackBlock callback;

@end


//  UIButton+Extension.m

#import "UIButton+Extension.h"

#import <objc/runtime.h>

const void *yg_callbackKey = @"yg_callbackKey";

@implementation UIButton (Extension)


- (void)setCallback:(CallbackBlock)callback {

    // 设置关联属性

    objc_setAssociatedObject(self, yg_callbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);

}

- (CallbackBlock)callback {

    // 获取关联属性

    return objc_getAssociatedObject(self, yg_callbackKey);

}


@end

这样就可以把 callback 当做按钮的属性来用了:


//  ViewController.m

#import "ViewController.h"

#import "UIButton+Extension.h"


@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIButton *button;

@end


@implementation ViewController


- (void)viewDidLoad {

    [super viewDidLoad];


    // 设置按钮的 callback “属性”的内容

    self.button.callback = ^{

        NSLog(@"button callback");

    };

    // 获取并执行按钮的 callback “属性”

    self.button.callback();

}


@end

我们常用的第三方库中有很多也是这样用的,如 SDWebImage 会用这样的方法来存储传进来的图片的 URL:


// UIImageView+WebCache.m

- (void)sd_setImageWithURL:(NSURL *)url

          placeholderImage:(UIImage *)placeholder

                  options:(SDWebImageOptions)options

                  progress:(SDWebImageDownloaderProgressBlock)progressBlock

                completed:(SDWebImageCompletionBlock)completedBlock {

    [self sd_cancelCurrentImageLoad];

    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    ...

}

- (NSURL *)sd_imageURL {

    return objc_getAssociatedObject(self, &imageURLKey);

}


参考文章

Objective-C 的 runtime 特性与小蝌蚪找妈妈

Objective-C Runtime

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容