本文主要跟你们分享iOS攻城狮比较感兴趣的知识点runtime。示例代码在这里:WHRuntimeDemo 读完并理解这篇文章以后,你将掌握下面这几个问题的答案。git
- 什么是runtime运行时
- 什么是isa指针
- 什么是SEL,什么是IMP, 什么是Method
- 什么是消息机制
- runtime运行时的8种使用场景
**runtime:**Objective-C是动态语言,它将不少静态语言在编译和连接时作的事放到了运行时,这个运行时系统就是runtime。 runtime其实就是一个库,它基本上是用C和汇编写的一套API,这个库使C语言有了面向对象的能力。 静态语言:在编译的时候会决定调用哪一个函数。 动态语言(OC):在运行的时候根据函数的名称找到对应的函数来调用。github
**isa:**OC中,类和类的实例在本质上没有区别,都是对象,任何对象都有isa指针,它指向类或元类(元类后面会讲解)。数组
**SEL:**SEL(选择器)是方法的selector的指针。方法的selector表示运行时方法的名字。OC在编译时,会依据每个方法的名字、参数,生成一个惟一的整型标识(Int类型的地址),这个标识就是SEL。缓存
**IMP:**IMP是一个函数指针,指向方法最终实现的首地址。SEL就是为了查找方法的最终实现IMP。网络
**Method:**用于表示类定义中的方法,它的结构体中包含一个SEL和IMP,至关于在SEL和IMP之间做了一个映射。并发
**消息机制:**任何方法的调用本质就是发送一个消息。编译器会将消息表达式[receiver message]转化为一个消息函数objc_msgSend(receiver, selector)。函数
**Runtime的使用:**获取属性列表,获取成员变量列表,得到方法列表,获取协议列表,方法交换(黑魔法),动态的添加方法,调用私有方法,为分类添加属性。ui
概述中已经说了,runtime其实就是一个库,这个库主要作了两件事情:spa
- 封装:runtime把对象用C语言的结构体来表示,方法用C语言的函数来表示。这些结构体和函数被runtime封装后,咱们就能够在程序运行的时候,对类/对象/方法进行操做。
- 寻找方法的最终执行:当执行[receiver message]的时候,至关于向receiver发送一条消息message。runtime会根据reveiver可否处理这条message,从而作出不一样的反应。
在OC中,类是用Class来表示的,而Class其实是一个指向objc_class结构体的指针。 设计
来看一下objc_class的定义
在这里只说一下cache
Cache用于缓存最近使用的方法。一个类只有一部分方法是经常使用的,每次调用一个方法以后,这个方法就被缓存到cache中,下次调用时runtime会先在cache中查找,若是cache中没有,才会去methodList中查找。有了cache,常常用到的方法的调用效率就提升了!
你只要记住,runtime其实就是一个库,它是一套API,这个库使C语言有了面向对象的能力。咱们能够运用runtime这个库里面的各类方法,在程序运行的时候对类/实例对象/变量/属性/方法等进行各类操做。
在解释isa以前,你须要知道,在Objective-C中,全部的类自身也是一个对象,咱们能够向这个对象发送消息(调用类方法)。
先来看一下runtime中实例对象的结构体objc_object。
从结构体中能够看到,这个结构体只包含一个指向其类的isa指针。
**isa指针的做用:**当咱们向一个对象发送消息时,runtime会根据这个对象的isa指针找到这个对象所属的类,在这个类的方法列表及父类的方法列表中,寻找与消息对应的selector指向的方法,找到后就运行这个方法。
要完全理解isa,还须要了解一下元类的概念。下面咱们用类方法建立了一个字典。
这句代码把+dictionary消息发送给NSDictionary类,而这个NSDictionary也是一个对象,既然是对象,那么它也会有一个isa指针,类的isa指针指向什么呢?
为了调用+dictionary方法,这个类的isa指针必须指向一个包含这些类方法的objc_class结构体,这就引出了元类的概念。meta-class(元类)存储着一个类的全部类方法。
向一个对象发送消息时,runtime会在这个对象所属的类的方法列表中查找方法; 向一个类发送消息时,会在这个类的meta-class(元类)的方法列表中查找。
meta-class是一个类,也能够向它发送消息,那么它的isa又是指向什么呢?为了避免让这种结构无限延伸下去,Objective-C的设计者让全部的meta-class的isa指向基类(NSObject)的meta-class,而基类的meta-class的isa指针是指向本身(NSObject)。
下图中的虚线箭头表示的是isa指针,实线箭头表示的是父类。
能够看出,全部实例对象的isa都指向它所属的类,而类的isa是指向它的元类,全部元类的isa指向基类的meta-class,基类的meta-class的isa指向本身。须要注意的是,root-class(基类)的superclass是nil。
SEL又叫选择器,是方法的selector的指针。
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个惟一的整型标识(Int类型的地址),这个标识就是SEL。
两个类之间,不管它们是父子关系,仍是没有关系,只要方法名相同,那么方法的SEL就是同样的,每个方法都对应着一个SEL,因此在 Objective-C同一个类中,不能存在2个同名的方法,即便参数类型不一样也不行。像下面这种状况就会报错。
SEL是一个指向方法的指针,是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅须要比较他们的地址就能够了,因此速度上很是优秀,它的存在只是为了加快方法的查询速度。
不一样的类能够拥有相同的selector,不一样类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector寻找对应的IMP。SEL就是为了查找方法的最终实现IMP。
IMP其实是一个函数指针,指向方法实现的首地址。表明了方法的最终实现。
第一个参数是指向self的指针(若是是实例方法,则是类实例的内存地址;若是是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),省略号是方法的参数。
每一个方法对应惟一的SEL,经过SEL快速准确地得到对应的 IMP,取得IMP后,就得到了执行这个方法代码了。
Method是用于表示类的方法。
Method结构体中包含一个SEL和IMP,实际上至关于在SEL和IMP之间做了一个映射。有了SEL,咱们即可以找到对应的IMP,从而调用方法的实现代码。
当执行了[receiver message]的时候,至关于向receiver发送一条消息message。runtime会根据reveiver可否处理这条message,从而作出不一样的反应。
消息直到运行时才绑定到方法的实现上。编译器会将消息表达式[receiver message]转化为一个消息函数,即objc_msgSend(receiver, selector)。
objc_msgSend作了以下事情:
- 经过对象的isa指针获取类的结构体。
- 在结构体的方法表里查找方法的selector。
- 若是没有找到selector,则经过objc_msgSend结构体中指向父类的指针找到父类,并在父类的方法表里查找方法的selector。
- 依次会一直找到NSObject。
- 一旦找到selector,就会获取到方法实现IMP。
- 传入相应的参数来执行方法的具体实现。
- 若是最终没有定位到selector,就会走消息转发流程。
以 [receiver message]的方式调用方法,若是receiver没法响应message,编译器会报错。但若是是以performSelector来调用,则须要等到运行时才能肯定object是否能接收message消息。若是不能,则程序崩溃。
当咱们不能肯定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下
若是不使用respondsToSelector:来判断,那么这就能够用到“消息转发”机制。
当对象没法接收消息,就会启动消息转发机制,经过这一机制,告诉对象如何处理未知的消息。
这样就能够采起一些措施,让程序执行特定的逻辑,从而避免崩溃。措施分为三个步骤。
1. 动态方法解析
对象接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。
在这个方法中,咱们有机会为该未知消息新增一个”处理方法”。使用该“处理方法”的前提是已经实现,只须要在运行时经过class_addMethod函数,动态的添加到类里面就能够了。代码以下。
2. 备用接收者
若是在上一步没法处理消息,则Runtime会继续调下面的方法。
若是这个方法返回一个对象,则这个对象会做为消息的新接收者。注意这个对象不能是self自身,不然就是出现无限循环。若是没有指定对象来处理aSelector,则应该 return [super forwardingTargetForSelector:aSelector]。
可是咱们只将消息转发到另外一个能处理该消息的对象上,没法对消息进行处理,例如操做消息的参数和返回值。
3. 完整消息转发
若是在上一步仍是不能处理未知消息,则惟一能作的就是启用完整的消息转发机制。此时会调用如下方法:
这是最后一次机会将消息转发给其它对象。建立一个表示消息的NSInvocation对象,把与消息的有关所有细节封装在anInvocation中,包括selector,目标(target)和参数。在forwardInvocation 方法中将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
a. 定位能够响应封装在anInvocation中的消息的对象。 b. 使用anInvocation做为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,runtime会提取这一结果并发送到消息的原始发送者。
在这个方法中咱们能够实现一些更复杂的功能,咱们能够对消息的内容进行修改。另外,若发现消息不该由本类处理,则应调用父类的同名方法,以便继承体系中的每一个类都有机会处理。
另外,必须重写下面的方法:
消息转发机制从这个方法中获取信息来建立NSInvocation对象。完整的示例以下:
NSObject的forwardInvocation方法只是调用了
doesNotRecognizeSelector方法,它不会转发任何消息。若是不在以上所述的三个步骤中处理未知消息,则会引起异常。 forwardInvocation就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也能够像一个运输站同样将全部未知消息都发送给同一个接收对象,取决于具体的实现。
消息的转发机制能够用下图来帮助理解。
代码以下图,运用class_copyPropertyList方法来得到属性列表,遍历把属性加入数组中,最终返回此数组。其中[selfdictionaryWithProperty:properties[i]] 方法是用来拿到属性的描述,例如copy,readonly,NSString等信息。Demo
代码以下图,运用class_copyIvarList方法来得到变量列表,经过遍历把变量加入到数组中,最终返回此数组。其中[[selfclass]decodeType:ivar_getTypeEncoding(ivars[i])]方法是用来拿到变量的类型,例如char,int,unsigned long等信息。Demo
代码以下图,经过runtime的class_copyMethodList方法来获取方法列表,经过遍历把方法加入到数组中,最终返回此数组。Demo
代码以下,运用class_copyProtocolList方法来得到协议列表。Demo
下面就是runtime的重头戏了,被称做黑魔法的方法交换Swizzling。交换方法是在method_exchangeImplementations里发生的。Demo
使用Swizzling的过程当中要注意两个问题:
Swizzling要在+load方法中执行 运行时会自动调用每一个类的两个方法,+load与+initialize。 +load会在main函数以前调用,而且必定会调用。 +initialize是在第一次调用类方法或实例方法以前被调用,有可能一直不被调用。 通常使用Swizzling是为了影响全局,因此为了方法交换必定成功,Swizzling要放在+load中执行。
Swizzling要在dispatch_once中执行 Swzzling是为了影响全局,因此只让它执行一次就能够了,因此要放在dispatch_once中。
方法交换的代码以下图。
方法交换有很多应用场景,好比记录页面被点开的次数:只要在UIViewController的分类的+load中交换viewDidAppear方法,在交换的方法中添加记录代码就能够了。
我这里举一个例子,Swizzling的实际应用:
代码以下图,结合代码理解。 当网络加载不到图片时,自动添加占位图片,而且不改变图片的原始调用方法。 在UIimage分类的+load方法中用dispatch_once_t来进行方法的交换,把系统的imageNamed与本身写的wh_imageNamed进行交换,本身写的wh_imageNamed中已经进行了占位图片的处理。 在别的地方使用imageNamed来拿图片,实际上已经调用了wh_imageNamed,而且在图片不存在的时候自动放上一张占位图。 注意!本身写的交换方法中要调用[self wh_imageNamed:@"test”],须要这样写,不会形成死循环。
代码以下,运用runtime的class_addMethod来添加一个方法。Demo
添加方法的运用这里说一下两种状况:
前提:接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法)。 第一种状况是,根据已知的方法名动态的添加一个方法。 第二种状况是,直接添加一个方法。 代码以下图
因为消息机制,runtime能够经过objc_msgSend来帮咱们调用一些私有方法。Demo
使用objc_msgSend须要注意两个问题:
须要导入头文件#import <objc/message.h> 按照下图在Build Settings里设置
在分类中属性不会自动生成实例变量和存取方法,可是能够运用runtime的关联对象(Associated Object)来解决这个问题。Demo
使用 objc_getAssociatedObject 和 objc_setAssociatedObject 来作到存取方法,使用关联对象模拟实例变量。下面是两个方法的原型:
方法中的的*@selector(categoryProperty)*就是参数key,使用 @selector(categoryProperty) 做为 key 传入,能够确保 key 的惟一性。
OBJC_ASSOCIATION_COPY_NONATOMIC 是属性修饰符。
以上就是与runtime有关的一些总结,文章若是有什么不许确的地方,欢迎指出,共同进步。谢谢!
本文所述的源码在这里: WHRuntimeDemo