Runtime是OC里面非常重要的一个概念,它是OC的底层实现,也正是因为Runtime,OC成为一个动态语言,并且拥有了面向对象的能力。这篇文章,将详细说明Runtime的各种知识,并且能够实际运用。
-
什么是Runtime
Runtime即运行时,也就是程序在运行的时候做的事情。在iOS中,有一套底层的C语言API,它是iOS内部核心之一,我们编写的OC代码,底层都是基于它实现的,OC语言在编译后,都是Runtime形式的C语言代码。
学习Runtime可以使我们更加清楚地了解OC语言的底层实现,从而可以运用它去实现很多OC语言实现不了的功能(比如给Category添加属性)。
-
类和对象
在没有接触Runtime之前,我们对OC的类和对象只有概念上的理解,并不知道它本质上是什么。现在我们来看看它们的底层定义,我们先从我们经常使用的id
入手,在<objc/objc.h>
中,我们找到这一段定义:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
这里说明id
是一个指向objc_object
结构体的指针,而注释又说,这个指针指向一个类的实例对象,所以我们知道了,objc_object
结构体就代表了一个类的实例对象:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
objc_object
这个结构体里面,只有一个isa
,这个isa
的类型是Class
,并且是不能为空的。顾名思义,这个肯定就是这个对象的类型了。
然后我们再去找找Class
又是什么东西:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
这里我们可以看到,Class
是一个指向objc_class
结构体的指针。所以我们知道了,isa
是一个指向objc_class
的指针,即通过它可以找到一个对象的类。所以,id
类型其实就是一个指向任意类型实例的指针。接着,我们再去看看objc_class
是什么东西:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
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;
我们先从第一行可以得知,OC的类里面,也有一个isa
指针,而这个指针指向了这个类的类型,由此我们可以推断出来,其实OC的类本质上也是一个对象,因为它也有自己的类。
在OC里面,每一个类的isa
指针都指向它的元类,最终指向NSObject
,NSObject
的元类是它自己。而NSObject
的父类则是nil。这张图很好的说明了isa
和super_class
的区别:
接着,我们可以看到,一个类的结构体里面,还有它的父类,类名,版本号,类信息,变量大小,变量列表,方法列表,方法缓存,协议列表这些东西。
这里也解释了为什么前面的
objc_object
里面只有一个isa
指针,是因为只要有了这个指针,就能够找到这个类里面的所有方法和属性。看了这些,我们就对OC的类和对象有了更深刻的理解。接下来,我们再去探究一下OC是怎么调用方法的。
-
OC的消息发送
OC的方法调用底层是给某个对象发送某个方法。并且,在编译的时候并没有确定具体调用哪个方法,只有在运行时才能确定。我们来验证一下,首先随便创建一个空的命令行项目:
然后创建一个Person类,然后写一个空的方法:
.h
@interface Person : NSObject
- (void)run;
@end
.m
- (void)run {
}
然后打开终端,cd到刚刚创建的main.m
文件所在的文件夹下,执行clang -rewrite-objc main.m
这时候,就可以在文件夹里面看到一个main.cpp
文件,打开,拉到最下面,就可以看到这一段代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return 0;
}
可以看到我们刚刚写的方法编译过之后全部变成了objc_msgSend
的方式,甚至alloc
和init
方法也变成了这样。这就说明OC方法的底层调用都是objc_msgSend
实现的,而objc_msgSend
就是消息发送。同时,也验证了OC底层就是用Runtime实现的C语言代码。
接下来我们来研究一下这个objc_msgSend
。要使用它,得首先导入#import <objc/message.h>
,然后就可以使用了。但是,我们打出来这个发现没有任何参数提示:
这是因为苹果公司不建议我们这么用了。我们可以在
Build Setting
里面,找到改成
NO
,再回去敲入objc_msgSend
,就可以看到提示了:具体的参数含义是:
id _Nullable self
:id
类型我们前面知道,它可以指向任意OC对象,这地方就代表着给谁发消息,也就是调用谁的方法。...
:三个点代表参数列表/可扩展参数。SEL _Nonnull op
:SEL
又是什么呢?到这里,我们就得提一下OC里面的方法了。老规矩,我们先去找定义,在<objc/runtime.h>
中:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
这里我们可以看到,OC的方法里面,有三个东西,一个是SEL
,一个是char *
,一个是IMP
,这个char *
我们知道是C语言的字符串,这个地方是一组描述方法的参数类型的字符数组,后面我们会详细了解这个东西,这里先不管它。SEL
这个地方通过命名我们可以看出来是方法名,IMP
我们就完全看不出了,它们具体是怎么定义的呢?还是在<objc/objc.h>
中:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// 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
这里,我们可以看到,IMP
其实就是一个指向方法实现的指针,而SEL
则是一个objc_selector
结构体,源码中我们找不到SEL
的定义,经过查阅资料得知,它完全可以理解为一个char *
,也就是说,其实它就是方法名的字符串,也就是一个方法的标签。
知道这些以后,我们就可以用消息发送来改写以前的OC代码,比如:
前面的Person
类的run
方法的调用:
objc_msgSend(p, @selector(run));
为了方便测试,我们给run
方法写一个简单的实现:
- (void)run {
NSLog(@"跑了");
}
然后运行一下:
完美!
甚至Person
类对象的声明都可以用发送消息的方式来完成(解耦合):
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));
再运行一下,一样可以得到之前的结果,这里就不贴图了,跟上面那个图一样。
Runtime为我们提供了直接通过类名获取类的函数:
objc_getClass(char * _Nonnull name);
和得到一个SEL
的函数:
sel_registerName(const char * _Nonnull str);
这样我们可以继续改进之前的代码:
id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;
一样可以运行得到结果。
到了这一步,是不是就跟我们之前看到的编译后的OC代码一样了?我们甚至不需要导入Person.h
头文件,就可以直接获取创建它的实例,并且执行方法,完成了解耦。
-
OC的消息转发(message forwarding)
看了上面的一些代码,不知道你有没有考虑过一个问题。发送消息的时候,我们只需要填一些字符串参数之类的就可以了,完全不知道有没有这个方法,如果没有这个方法会发生什么事情呢?
接下来,我们做个试验:
直接crash掉了。调用方法时,如果在方法在对象的类继承体系中没有找到,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。这就涉及到以下4个方法:
- 动态方法解析(dynamic method resolution)
首先会调用+ resolveInstanceMethod:
(对应实例方法)或+ resolveClassMethod:
(对应类方法)方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
我们这里测试一下,增加一个wahaha
方法的实现,看看是否可以顺利运行,首先,我们在Person.m
中导入#import <objc/message.h>
,然后,利用
class_addMethod(Class _Nullable __unsafe_unretained cls,SEL _Nonnull name,IMP _Nonnull imp, const char * _Nullable types)
来增加一个方法及实现,其中,第一个参数填self
,第二个参数填wahaha
的SEL,可以用@selector(wahaha)
,也可以用之前用过的sel_registerName("wahaha")
,第三个参数需要一个imp,我们知道IMP是指向方法实现的指针,这里我们可以用imp_implementationWithBlock(id _Nonnull block)
来实现,最后一个参数我们之前也见过,就是方法定义里面的的method_types,这个东西该怎么写呢?我们先去查一下官方文档:
types
An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
这里面说明这个是一组描述方法的参数类型的字符数组,并且,每个方法都有两个被隐含的参数,一个是self
(代表当前对象),一个是_cmd
(代表当前对象的SEL),所以第二个和第三个字符必须是@
和:
,而第一个字符是返回值,所以一个没有参数的方法,它的types
就是"v@:"
。至于什么类型对应什么字符,可以去上面的链接中找。所以我们这里可以直接用"v@:"
。
//如果增加了方法并返回YES,就会重新发送消息并处理,返回NO,则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == sel_registerName("wahaha")) {
class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
NSLog(@"wahaha");
}), "v@:");
}
return YES;
}
运行结果:
怎么样,是不是很神奇?如果上面返回NO,则会进入完整的消息转发机制(full forwarding mechanism),这里又分为两个步骤:
- 快速消息转发 (Fast Forwarding)
这个时候,如果实现了- forwardingTargetForSelector:
方法,系统就会进入该方法继续处理消息,这个方法的作用是把之前没办法处理的消息转发给别的对象去处理:
//返回一个对象继续处理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [Dog new];
}
return nil;
}
这里我们新建了一个Dog
类,实现了wahaha
方法,所以我们直接返回一个Dog
的实例,最后运行结果如上,这里就不贴图了。
- 普通消息转发(Normal Forwarding)
如果上一步也没有对消息进行处理,则会进入最后一步,这里涉及到两个方法。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil
,程序会Crash掉,并抛出unrecognized selector sent to instance
异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation
对象并调用-forwardInvocation:
方法。我们同样在这里对之前的消息进行处理一次:
//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Dog *dog = [Dog new];
if ([dog respondsToSelector:sel]) {
[anInvocation invokeWithTarget:dog];
}
}
以上就是OC消息传递过程中发生的事情,利用这些我们可以在很多地方对一个消息做处理。但是我们该怎么选择呢?
- 动态方法解析:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
- 快速消息转发:其他对象,使用范围更广,不只是限于原来的对象。
- 普通消息转发:它一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
同时需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
通过消息发送,我们其实已经对Runtime做了一个简单的运用了。接下来,我们再多一些探讨。
-
Runtime可以做什么?
- 在程序运行的时候动态添加一个类
- 在程序运行的时候动态的修改一个类的属性和方法
- 在程序运行的时候遍历一个类的所有属性
Runtime有很多方法,可以在文档中一一查看,不同功能的方法通过前缀区分,比如说class_
就是对类的操作,objc_
就是对对象的操作,等等,都比较好理解。
了解这些以后,我们再回头看之前提过的一个问题,就是给Category增加属性。
我们先看一下,如果直接给Category增加属性会发生什么,我们给Person
类创建一个Play
分类,然后添加一个属性:
@interface Person (Play)
@property (nonatomic, strong) NSString *gameName;
@end
但是我们使用的时候会发现,直接就Crash了:
我们现在应该就能明白,这是因为找不到Getter,同时也没有Setter,我们可以给它添加这两个方法,但是我们在给它添加的时候,发现在Category里面根本就没有
_gameName
这个变量,所以没办法像别的类那样直接添加,这时候,我们就可以利用Runtime的objc_setAssociatedObject
和objc_getAssociatedObject
来实现:
- (NSString *)gameName{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setGameName:(NSString *)gameName {
objc_setAssociatedObject(self, @selector(gameName), gameName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
再次运行,就可以正常使用这个属性了:
通过以上简单的例子,我们可以看得出来,Runtime是OC代码的底层实现,所以很多OC代码不支持的事情,我们都可以通过Runtime自己去实现,具体在什么场景下使用要根据实际需求做具体分析。接下来,我会用Runtime和之前Block种提到的相关技术自己实现KVO,并且进行一些改造。另外,本篇文章的代码可以在我的github上查看,点击前往。