一、简介
1.1 runtime简介
RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。
1.2 什么是runtime?
1、OC 是一个全动态语言,OC 的一切都是基于 Runtime 实现的,平时编写的OC代码, 在程序运行过程中, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者。
2、runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。
3、runtimeAPI的实现是用 C++ 开发的(源码中的实现文件都是mm),是一套苹果开源的框架。
1.3 Runtime相关头文件(#import <objc/runtime.h>)
在iOS 的sdk中 usr/include/objc文件夹下有这样几个文件:

如上图,都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。 在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。
使用时,需要导入文件,导入如:
#import <objc/message.h> //包含了一些向对象发送消息的函数
#import <objc/runtime.h> //包含了对运行时进行操作的方法
runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。 主要包括的内容往下面的第二章节看。
二、 runtime.h
2.1 runtime.h --操作对象的类型的定义
让我们来看下runtime.h,也就是头文件的一些源码内容:内容如下:
/// An opaque type that represents a method in a class definition. 一个类型,代表着类定义中的一个方法
typedef struct objc_method *Method;
/// An opaque type that represents an instance variable. 代表实例(对象)的变量
typedef struct objc_ivar *Ivar;
/// An opaque type that represents a category. 代表一个分类
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property. 代表OC声明的属性
typedef struct objc_property *objc_property_t;
// Class代表一个类,它在objc.h中这样定义的
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
通过上面的类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,对操作一个类属性和方法非常方便。例如:如果想要获取Class的name属性,可以按如下方法获取:
Class classVC = ViewController.class;
const char *cname = class_getName(classVC);
NSLog(@"class_getName==%s", cname); // 输出:ViewController
2.2 runtime.h --函数的定义
对对象进行操作的方法一般以object_开头
对类进行操作的方法一般以class_开头
对类或对象的方法进行操作的方法一般以method_开头
对成员变量进行操作的方法一般以ivar_开头
对属性进行操作的方法一般以property_开头开头
对协议进行操作的方法一般以protocol_开头
根据以上的函数的前缀 可以大致了解到层级关系
对于以objc_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。
例如:使用runtime对当前的应用中加载的类进行打印:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
for (int i = 0; i < count; i++) {
const char *cname = class_getName(classes[i]);
NSLog(@"当前工程加载的类%s\n", cname);
}
}
三、 runtime应用
3.1 发送消息
a. 发送消息方法调用的本质,就是让对象发送消息。
b. objc_msgSend,只有对象才能发送消息,因此以objc开头.
c. 使用消息机制前提,必须导入#import <objc/message.h>
这里需要注意的是,当你使用objc_msgSend的时候,工程报错的话,请按以下步骤检查:
1、确认引入了 #import <objc/runtime.h> #import <objc/message.h>
2、在项目配置文件 -> Build Settings -> Enable Strict Checking of objc_msgSend Calls 这个字段设置为 NO, 默认为YES。如下图:

3、或者这样使用objc_msgSend也可以解决上述问题。由于objc_msgSend函数本身是无返回值无参数的函数, 所以要给它强制转换类型代码如下:
Person *person = [[Person alloc]init];
((void (*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("say"));
((void (*) (id, SEL)) (void *)objc_msgSend)(person, @selector(say));
消息机制简单使用:
一、调用对象方法的方式
// 创建SingleLoading对象load
SingleLoading *load = [[SingleLoading alloc] init];
// 调用对象方法 objectLoad
[load objectLoad];
// 本质:让对象发送消息
objc_msgSend(load, @selector(objectLoad));
二、调用类方法的方式
// 调用类方法的方式:两种
// 第一种通过类名调用 beginLoad
[SingleLoading beginLoad];
// 第二种通过类对象调用
[[SingleLoading class] beginLoad];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 本质:让类对象发送消息
objc_msgSend([SingleLoading class], @selector(beginLoad));
3.2 交换方法(method_exchangeImplementations)
交换方法实现的需求场景:
自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码的情况下,就完成了项目的改进。
交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在第一次使用的时候调用,当有分类的时候会调用多次。
示例:将控制器中的两个方法(method1、method2)实现调用结果进行替换。
导入#import <objc/runtime.h>,实现代码如下:
- (void)method_exchangeImplementations
{
// 获取method1方法地址
Method m1 = class_getInstanceMethod([self class], @selector(method1));
// 获取method2方法地址
Method m2 = class_getInstanceMethod([self class], @selector(method2));
// 交换前 方法执行执行
[self method1];
[self method2];
// 交换方法 方法执行执行
method_exchangeImplementations(m1, m2);
// 交换后
[self method1];
[self method2];
}
- (void)method1
{
NSLog(@"%s", __func__);
}
- (void)method2
{
NSLog(@"%s", __func__);
}
控制台打印信息如下:
2017-09-19 10:19:08.183 RunTime[5126:5368864] -[ViewController method1]
2017-09-19 10:19:08.183 RunTime[5126:5368864] -[ViewController method2]
2017-09-19 10:19:08.184 RunTime[5126:5368864] -[ViewController method2]
2017-09-19 10:19:08.184 RunTime[5126:5368864] -[ViewController method1]
通过上面信息,我们可以看到。已经达到效果。方法调用已经进行了交换。
3.3 类\对象的关联对象(objc_setAssociatedObject)
在开发中有时需要在对象中存放相关信息,通常做法是从要使用对象所属的类继承一个子类,然后改写子类,比如给子类添加额外的属性信息等。但是有时候类的实例是通过某种别的机制创建的,我们无法创建出自己所需要的子类。那么Runtime提供一种别的方法来解决这个问题,这就是“关联对象“。它可以为某对象关联许多其他对象,这些对象通过“键”来区分,储存对象值的时候可以指明“存储策略”,用以维护相关的内存语义。这些内存语义分别和属性中定义的相对应。

你可能需要了解关联对象相关方法:
关联对象相关方法
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy)此方法用于通过使用给定的键和策略为某对象设置关联对象值。
id objc_getAssociatedObject(id object, void *key)通过给定键从某对象中获取对应的关联对象值。
void objc_removeAssociatedObjects(id object)移除指定对象中的全部关联对象。
a:给分类添加属性
可能对于初入iOS 的开发者来说,对于类目的认知,是只能添加方法,并不能添加属性!下面我们将通过runtime给分类添加动态属性。接下来以AppDelegate拓展类目在里面增加对象。
首先我们创建分类#import "AppDelegate+runtimeTest.h",
分类的源码实现:
#import "AppDelegate+runtimeTest.h"
#import <objc/runtime.h>
static const NSString *blockKey = @"keyBlock";
@implementation AppDelegate (runtimeTest)
- (void)setBlock:(block)block {
objc_setAssociatedObject(self, &blockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (block)block {
return objc_getAssociatedObject(self, &blockKey);
}
- (void)usingBlock:(block)block {
self.block = [block copy];
}
- (void)test {
self.block(@"绑定Block成功");
}
@end
.h文件,我们是这样定义的。
#import "AppDelegate.h"
@interface AppDelegate (runtimeTest)
typedef void(^block)();
- (void)usingBlock:(block)block;
- (void)test;
@end
以上完成了分类runtimeTest的全部实现,我们将引入头文件#import "AppDelegate+runtimeTest.h"来看看我们分类实现的具体效果。
// 类\对象的关联对象 一:给分类添加属性
- (void)addMethodOne{
[((AppDelegate*)[UIApplication sharedApplication].delegate) usingBlock:^(NSString *str){
NSLog(@"str==%@",str);
}];
[((AppDelegate*)[UIApplication sharedApplication].delegate) test];
}
通过引入分类,进行调用测试。控制台打印信息如下:
2017-09-19 11:23:00.213 RunTime[5960:5399331] str==绑定Block成功
总结:通过上面信息,可以看到我们已经成功在appdelegate的类目里面把self(appdelegate对象)和block对象绑定了起来。就是在类目里面为apdelegate添加了属性。
b:给对象添加关联对象
例如在iOS 开发中我们都是用过UIAlert类,当用户要处理点击事件的时候,需要通过委托协议来实现。这时候就需要把视图和事先动作的代码分开。但是如果代码中使用多个UIAlerView的时候,还需要通过在回调中判断alertView的类型,然后再去处理响应的逻辑。要是能够是创建视图的时候,就把每个按钮响应的逻辑写好,那就简单多了。于是可以以如下方式实现:
这里创建了2个UIAlertView,用于比较。
你需要声明static const NSString *alertViewKey = @"alertViewKey";
// 类\对象的关联对象 二:对象添加关联对象
- (void)addMethodTwo {
UIAlertView *_alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"This is deprecated?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Ok", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
NSLog(@"buttonIndex==%ld,alertLeft",buttonIndex);
};
objc_setAssociatedObject(_alertView, (__bridge const void *)(alertViewKey), block, OBJC_ASSOCIATION_COPY);
[_alertView show];
}
#pragma -mark UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, (__bridge const void *)(alertViewKey));
block(buttonIndex);
}
- (IBAction)alertLeft:(id)sender {
[self addMethodTwo];
}
- (IBAction)alertRight:(id)sender {
UIAlertView *_alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"alertRight"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Ok", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
NSLog(@"buttonIndex==%ld,alertRight",buttonIndex);
};
objc_setAssociatedObject(_alertView, (__bridge const void *)(alertViewKey), block, OBJC_ASSOCIATION_COPY);
[_alertView show];
}
总结:
给对象添加关联对象,非常有用。不仅可以如上面示例。它使代码简洁舒适。也可用于多个参数的传递。比如alertView,一般传值,使用的是alertView的tag属性。我们想把更多的参数传给alertView代理。同样可以使用objc_getAssociatedObject来实现。
objc_setAssociatedObject方法的参数解释:
第一个参数id object, 当前对象
第二个参数const void *key, 关联的key,是字符串
第三个参数id value, 被关联的对象的值
第四个参数objc_AssociationPolicy policy关联引用的规则,具体规则本文上面有图解。
3.4 动态添加方法
简介:
开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
performSelector方法区别于直接调用,如果我调用一个没有定义的方法,在编译时直接调用会给报错,红色的哦。如果用的是performSelector方法,那就只会有警告(貌似早期木有)。所以,人家想问得就是,你有没有用过动态添加方法,通俗讲就是,一个方法我没有定义,直接调找不到,但是我还想运行他,你该怎么做?如下图:

通过上面的信息,我想大家应该明白了,其实这东西跟懒加载差不多,这个问题虽然我认为是吃饱撑的,但是它存在的真实意义在于:当硬件内存过小的时候,如果我们将每个方法都直接加到内存当中去,但是几百年不用一次,这样就造成了浪费,就跟懒加载一样,我方法定义好,但是只有当你用的时候我猜加载你,这个呢就必须用到runtime的一些知识了。就是performSelector:以及动态添加方法。
a. 实现动态添加方法
(1)我们新建一个类TestTransmitView。为了编译通过,我们需要通过performSelector开头的方法调用 TestTransmitView这个类的方法show:
如下:
//MARK: 动态添加方法
- (void)TestTransmit {
TestTransmitView * view = [[TestTransmitView alloc] init];
[view performSelector:@selector(show:) onThread:[NSThread currentThread] withObject:@"动态添加方法" waitUntilDone:YES];
}
(2)如果TestTransmitView类没有实现show:方法那么将调用TestTransmitView的类方法+(void)resolveInstanceMethod:方法,覆盖这个方法。它里面判断一下要调用的方法时候和我在这个类里想调用的方法时候一致,if(一致)用class_addMethod这个c语言函数创建一个方法,这个方法:
第一个参数就是你要添加方法的那个类的class类对象,
第二个参数就是传递过来的sel,
第三个是一个函数的入口名称,这个函数实际上是内部内容就是添加方法的内部内容,
第四个参数是上一个参数--函数的参数要数,第一个v代表这个函数的返回值为void,如果返回对象类型就是@,后面的@:@分别代表后续的三个参数,其中Sel类型的用:表示,具体请参考苹果开发文档。其实除了_cmd大家可能不熟悉之外,其他的基本上可以参考NSLog这个函数了,NSString就是用@表示。
别忘了调用super方法,至于调用class后一行需要返回一个bool值,据验证,无论返回NO还是YES,dynamic_show函数都会被调用。实现代码如下:
@implementation TestTransmitView
// 实现动态添加方法
void dynamic_show(id self,SEL _cmd,id param1)
{
NSLog(@"sssssssssssssssssssssssssssssssssssssssss实例方法 参数:%@",param1);
}
/**
* 当调用了没有实现的方法没有实现就会调用
*
* @param sel 没有实现方法
*
* @return 如果方法被发现并添加return:Yes 否则NO
*/
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(show:)) {
// class_addMethod([self class],sel,(IMP)dynamic_show,"v@:@");
return NO;
}
return [super resolveInstanceMethod:sel];
}
b. 实现消息转发
接着上面的代码写,如果我把上面的class_addMethod函数调用这一行注释掉程序立马crash,如果想程序不crash,那么就需要接着询问消息改怎么处理,很显然当前类是没有没有办法接着寻找这个方法了,那么我们就需要转给其他类来处理,就需要实现methodSignatureForSelector:这个方法了这个类告诉我们时候有处理这个消息的类,如果返回不为空,那么就来到这个方法forwardInvocation:这个方法怎么处理我就不多说,直接上代码:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL sel = [anInvocation selector];
Delegate * delegate = [[Delegate alloc] init];
if ([delegate respondsToSelector:sel]) {
[anInvocation invokeWithTarget:delegate];
}
else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [Delegate instanceMethodSignatureForSelector:aSelector];
}
而Delegate类,只需要含show:方法即可:如下
@implementation Delegate
-(void)show:(NSString *)str
{
NSLog(@"转发给Delegate的方法");
}
@end
小结: 动态添加方法开发使用在开头简介处已做说明。大致内容就以上这些。
面试题:有没有使用performSelector ?其实主要想问你有没有动态添加过方法。
总结: 关于runtime的在开发过程中运用的基础部分,大致就是以上第3部分的几大块。当然,runtime还有很多神奇之处等待着大家的开发!
附:
你可能需要上述示例 demo在这里
网友评论