• RunTime 概述
  • RunTime消息机制
  • RunTime交换方法
  • RunTime消息转发
  • RunTime关联对象
  • RunTime实现字典与模型互转

1.RunTime 概述

我们在面试的时候,经常都会被问到这么个问题:为什么说OC是一门动态的语言???其实也就是想知道你对runtime的了解程度。

•Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
•RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
•Runtime基本是用C和汇编写的,并且它是开源代码,由苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。
•使用运行时,需要导入头文件 #import <objc/runtime.h>,并且xcode5之后,苹果不建议使用底层方法,如果想要使用运行时,需要关闭严格检查objc_msgSend的调用,BuildSetting->搜索msg 改为NO,否则主动调objc_msgSend函数会报错。
 

2.RunTime消息机制

1.消息机制是运行时里面最重要的机制,OC中任何方法的调用,本质都是发送消息。eg:

当我们实例化这个对象时:MyClass *object = [[MyClass alloc] init];  就会调这个实例化方法:[object showUserName];

我们大概来看一下它的底层实现:

•在编译阶段,[object showUserName] 会被编译器转化为: objc_msgSend(object, "showUserName"),相当于一种“发消息”的行为。
• 在运行阶段,执行到上述的objc_msgSend这个函数时,函数内部会到object对应的内存地址,寻找showUserName这个方法的地址并执行。如果找不到,就会抛一个“unknown selector sent to instance”的异常。
 

证明过程:

在终端用命令打开此类文件所在的文件夹,继续写入命令:clang -rewrite-objc MyClass.m(把oc代码转写成

c/c++代码,我们常用它来窥探OC的底层实现),不一会在原来的同一目录下会多出一个 MyClass.cpp 文件

双击打开,可以看到 init 方法已经被编译器转化为下面这样:

objc_msgSend 函数被定义在 objc/message.h 目录下,其函数原型是:

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

该函数有两个参数,一个 id 类型(消息接收对象),一个 SEL 类型(方法的selector)。

@selector (SEL):是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的地址,地址中存放着方法的名字。

对于一个类中,每一个方法对应着一个SEL。所以一个类中不能存在2个名称相同的方法(有歧义。。。),即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

歧义解释:- (void)go {}       + (void)go {} 这两个方法可以共存(我们知道,这两个方法的名字都是go)。

我个人的理解是:当我们向一个对象或一个类发送消息时,runtime都会根据方法名去这个对象所属的这个类的方法列表中查找方法,而方法列表的外层应该是一个字典,根据所传的接收消息对象不同,查找的方法列表也不同。

objc_msgSend([MyClass class], @selector(go));

objc_msgSend([[MyClass alloc] init], @selector(go));

Id :是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。

Class vclass = NSClassFromString(@"ViewController");

id vc = [[vclass alloc] init];

objc_object 结构体定义如下:

struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

这就是我们通常所说的对象,这个结构体只有一个成员变量 isa,对象可以通过 isa 指针找到其所属的类。isa 是一个 Class 类型的成员变量,那么 Class 又是什么呢?如下:

•·Class 也有一个 isa 指针,指向其所属的元类(meta)。
•·super_class:指向其超类。
•·name:是类名。
•·version:是类的版本信息。
•·info:是类的详情。
•·instance_size:是该类的实例对象的大小。
•·ivars:指向该类的成员变量列表。
•·methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。
•·cache:Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
•·protocols:指向该类的协议列表。

经过以上的讲述,我们大概可以了解到,当调用一个方法时,其运行过程大致如下:

•底层调用 [p performSelector:@selector(eat)] 方法,编译器在将代码转化为 objc_msgSend(p, @selector(eat))把方法的调用者和方法选择器当做参数传递过去。
•首先,检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
•如果target != nil,方法的调用者会通过 isa 指针来找到其所属的类,然后在 cache 中查找该方法,找得到就跳到对应的方法去执行。
•若 cache 中未找到,再去 methodList 中查找。若能找到,则将 method 加入到 cache 中,以方便下次查找,并通过 method 中的函数指针跳转到对应的函数中去执行。
•若 methodlist 中未找到,则去 superClass 中查找,一直找到 NSObject 类为止。若能找到,则将 method 加入到 cache 中。
•如果还是查不到,则会执行消息转发,抛出异常。
 
我们来看看底层调用方法的几个实例(分别是无参无返回,有参无返回,有参有返回):
 

3.RunTime交换方法

应用场景:当系统自带的方法功能不够,需要给系统自带的方法扩展一些功能时。

eg:实现image添加图片的时候,自动判断image是否为空,如果为空则提醒图片不存在。

有以下三种比较好的解决方法:

1.自定义类, 重写系统自带的imageName:方法,这种方法虽然可以实现,但是它的弊端就是必须要使用自己的类,依赖性强。

2.给UIImage添加一个分类, 改变系统类的实现,给系统的类添加方法的时候调用(每次使用都需要导入头文件,并且如果项目比较大,之前使用的方法全部需要更改)。

3.使用runtime的交互方法,给系统的方法添加功能. 具体实现 : 添加一个分类 --> 在分类中提供一个自定义判空增加新功能的方法 --> 将这个方法的实现和系统自带的方法的实现交互.

交换方法的本质其实是交换两个方法的实现,即调换le_imageNamed:和imageName:方法,达到调用le_imageNamed:其实就是调用imageNamed:方法的目的。

那么首先需要明白方法在哪里交换,因为以后都是使用自己定义的方法取代系统的方法,所以,当程序一启动,就要求能使用自己定义的功能方法。我们一般在 + (void)load 方法里实现交换方法 (当程序启动的时候就会调用该方法,换句话说,只要程序一启动就会调用load方法,整个程序运行中只会调用一次)

•注意:交换方法时 le_imageNamed:方法中就不能再调用imageNamed:方法了,因为调用imageNamed:方法实质上相当于调用 le_imageNamed:方法,会循环引用造成死循环。

4.RunTime消息转发

在方法调用的时候,如果没有找到方法就会转向消息转发(拦截调用)。
拦截调用是指,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的五个方法来处理。

+ (BOOL)resolveClassMethod:(SEL)sel;

+ (BOOL)resolveInstanceMethod:(SEL)sel;

- (id)forwardingTargetForSelector:(SEL)aSelector;

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

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

  • 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
  • 第二个方法和第一个方法相似,只不过处理的是实例方法。
  • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • 第四个方法是获取方法签名进入下一步,进行消息转发
  • 第五个方法将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

eg:   Monkey *monkey = [[Monkey alloc] init];

((void (*) (id, SEL)) objc_msgSend) (monkey, sel_registerName("fly"));  (猴子是不可能有飞的天赋的,除非它是孙猴子。。。)

"v@:"的含义请看官方文档:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

消息转发的作用主要是处理异常,下面来看一个实例:

NSMutableArray *arrM = [NSMutableArray array];

NSString *str = @"”;

[arrM addObject:str];   这行代码会导致程序崩溃,因为数组添加的对象不能为空。解决方法:

总结:可以利用category + runtime + 异常的捕获写一个防止崩溃的框架,已经有大神在做了,具体请看:https://github.com/chenfanfang/AvoidCrash

5.RunTime关联对象

应用场景:当你准备用一个系统的类或者是你写的类,但是这个类并不能满足你的需求,你需要额外添加一个属性。
一般解决办法要么是extends(继承),要么使用category(类别)。
但基本不推荐使用extends,主要是耦合性太强,主要使用category。
我们都知道,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明, 这时候,runtime的关联属性就能发挥它的作用了。

注意:使用property的时候,同时重写set get方法会报错。复写了get和set方法之后@property默认生成的@synthesize就不会起作用了,这也就意味着你的类不会自动生成出来实例变量了,你就必须要自己声明实例变量。

设置关联对象使用

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

•·object 是源对象
•·value 是被关联的对象
•·key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象
•·policy 是一个枚举值,表示关联对象的行为,从命名就能看出各个枚举值的含义
 

获取关联对象使用

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)

要删除某一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可。

移除源对象中所有的关联对象

OBJC_EXPORT void objc_removeAssociatedObjects(id object)

eg:为view类添加一个点击手势(主要实现它的子类(UILabel 、UIImageView可以像button一样,有自己的点击方法)

6.RunTime实现字典与模型互转

其中,获取属性的方法可以让我们拿到各个类的私有属性,让后利用kvc赋值,加以应用。

归档解档:

常规的归档解档方法,可是当model的属性很多时,这样写就有点尴尬了。。。

利用runtime实现批量归档解档:

我们在使用一些json转model的第三方框架(JSONModel,MJExtension等)时,它们的底层都是利用runtime去实现的:

字典转模型的时候:

1.根据字典的 key 生成 setter 方法

2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)

模型转字典的时候:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性

2.调用 property_getName 获取属性名称

3.根据属性名称生成 getter 方法

4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)

小白看过来runtinme的更多相关文章

  1. 写给小白看的入门级 Java 基本语法,强烈推荐

    之前写的一篇我去阅读量非常不错,但有一句留言深深地刺痛了我: 培训班学习半年,工作半年,我现在都看不懂你这篇文章,甚至看不下去,对于我来说有点深. 从表面上看,这句话有点讽刺我的文章写得不够通俗易懂的 ...

  2. 写给小白看的Mysql事务

    1 为什么需要事务 在网上的很多资料里,其实没有很好的解释为什么我们需要事务.其实我们去学习一个东西之前,还是应该了解清楚这个东西为什么有用,硬生生的去记住事务的ACID特性.各种隔离级别个人认为没有 ...

  3. 写给小白看的 JavaScript 异步

    某天突然写了个方法要从后台调用数据,显示在前台页面,但是输出结果总是空 undefined,得不到数据.多方找资料才发现,原来是入了 JS 异步的 “坑”. 我们常常听到单线程.多线程.同步.异步这些 ...

  4. 给小白看的KMP算法

    浅谈KMP算法: (大部分人的KMP写法都是不一样的) 一: 先给大家推荐一个讲kmp特别好理解的一个博客:阮一峰 二: 再给大家介绍一点相关概念: 栗子:  P串: ABCBD 前缀:A,AB,AB ...

  5. 353 stars Java项目!Java小白必看!austin介绍 【第一话】

    有好几个群友问我为什么最近更新变慢了.工作忙是一方面,另一方面是我更新文章的动力确实下降了.近大半年一直在更新的<对线面试官>系列,到现在已经40篇了. 说实话,当时我更新该系列有很大一部 ...

  6. 偏前端--之小白学习本地存储与cookie

    百度了很多都是讲的理论,什么小于4kb啊之类的,小白看了一脸懵逼复制到html中为什么没效果!!哈哈.我来写一个方便小白学习. 贴图带文字描述,让小白也运行起来,然后自己再去理解... 1. cook ...

  7. ComicEnhancerPro 系列教程二十:用“文件比较”看有损、无损

    作者:马健邮箱:stronghorse_mj@hotmail.com 主页:http://www.comicer.com/stronghorse/ 发布:2017.07.23 教程二十:用“文件比较” ...

  8. 小白学linux命令

    小白是景女神全栈开发股份有限公司的一名财务实习员工,经过3个月的实习期,小白是过五关斩六将啊!终于成为了公司的一名正式员工,而且收到了景总亲自发来贺喜的邮件:“欢迎你加入大家庭,公司也本着员工全面发展 ...

  9. 再也不学Threadlocal了,看这一篇就忘不掉了(万字总结)

    为什么要学习ThreadLocal呢?因为面试官经常问,而且在线程中使用它可以给我们提供一个线程内的本地局部变量,这样就可以减少在一个线程中因为多函数之间的操作导致共享变量传值的复杂性,说白了,我们使 ...

随机推荐

  1. django RESTful设计方法

    1. 域名 应该尽量将API部署在专用域名之下. https://api.example.com 如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下. https://example.org ...

  2. 如何把ppt写好

    前言 这里不是总结ppt如何写的美观漂亮,而是总结写整体的结构,如何修辞 整体结构 前后呼应 就像做作文一样,前后呼应,产生共鸣和联想,切记过于离散 抽象化 不要把自己做的东西写小了,要有一定的抽象度 ...

  3. 牛客小白月赛13 小A的最短路(lca+RMQ)

    链接:https://ac.nowcoder.com/acm/contest/549/F来源:牛客网 题目描述 小A这次来到一个景区去旅游,景区里面有N个景点,景点之间有N-1条路径.小A从当前的一个 ...

  4. java基础 ----- 程序的调试

    --- -- 什么是程序调试 当程序出错时,我们希望可以这样 逐条语句执行程序   -----    观察程序的执行情况 ------  发现问题   -----  解决问题 但是,程序一闪就运行结束 ...

  5. c# post方法亲测可用

    /// <summary> /// Post接口方法 /// </summary> /// <param name="requestUri">& ...

  6. nginx访问502 gateway,*1 connect() failed (111: Connection refused) while connecting to upstream

    安装好nginx,php环境后,配置虚拟主机,结果访问后就报502 gateway,查看日志文件后,显示错误如下: 2019/04/29 16:24:39 [error] 19433#19433: * ...

  7. github上用golang写的项目

    1.moby/moby docker的新马甲 2.kubernetes/kubernetes 分布式容器管理 3.grafana/grafana 一个可视化面板,有漂亮的仪表盘,多种数据来源,适合做系 ...

  8. 关于PHP架构师进阶的一些思考

    相信大家都有感觉,就是当程序员写业务写了几年后,就会有想进阶的想法,技术方面当然就是架构师了,然后具体从哪些方面丰富自己才能个达到目的呢?大部分人可能会很迷茫,当然也包括我, 最近和很多大牛交流了一些 ...

  9. 关于jquery的选择器中的空格问题

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. Altium Designer 16 问题解决

    1:同一个工程中,不同原理图里的网络标号不能关联起来 解决--->   选择  工程->工程参数->网络识别符范围 -> GLOBAL 2:PCB中影藏显示相应Net的飞线 解 ...