细说objc中的Runtime
前言
这篇文章讲解了一些runtime
的基本知识,需要有一定的objc
基础及开发经验。为了更好的阅读体验,推荐戳我的博客阅读。
从理解发送消息讲起
为什么objc
叫消息发送?为什么不叫函数调用?在objc
中,发送消息仅仅表示一种行为 ,不能理解为像C
语言中那样的函数调用。原因就是在发送消息的背后,runtime
帮我们做了非常多的事情。这样是objc
能真正成为一门动态语言的真正原因。
要学习runtime
所要掌握的几个基本概念
在开始学习runtime
之前,有几个基本的概念是必须要了解的:
SEL
SEL
是selector
在objc
中的表示类型,selector
是方法选择器,可以理解为方法的ID。而这个ID的数据结构是SEL
:
typedef struct objc_selector *SEL
其实它就是个映射到方法的C字符串,你可以用 Objc
编译器命令@selector()
或者 Runtime
系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
id
objc_msgSend
第一个参数类型为id
,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id
那么objc_object
又是啥呢:
struct objc_object {
Class isa;
};
objc_object
结构体包含一个isa
指针,根据isa
指针就可以顺藤摸瓜找到对象所属的类。
Class
之所以说isa
是指针是因为Class
其实是一个指向objc_class
结构体的指针:
typedef struct objc_class *Class;
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;
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;
-
类对象的概念在此不再表述,只说一些之前自己不懂的部分,你也可以查看我的这篇深入理解objc中的对象与类博客:
- 类对象的
isa
指针指向的是类对象的元类,每个类对象都有自己的元类。 - 类对象里存放时的实例对象的对象方法,属性,协议列表等信息。注意
objc_cache *cache
这个东西,存的是匹配信息,比如消息来到时该对象能不能处理此消息,方法对应的实现等都收纳在此中。 - 只有类对象才有
super_class
这个指针。 -
NSObject
的super_class
指针指向nil
。
- 类对象的
-
关于元类的概念:
- 元类是类对象的类对象,类对象的对象方法(即实例对象的类方法)列表就存在此处。
- 所有元类的
isa
指针指向NSObject
的元类,即根元类。super_class
指针指向NSObject
。
如下图:
对象,类,元类
Method
Method是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;
objc_method
:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。 -
method_imp
指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
-
它就是一个函数指针,这是由编译器生成的。当你发起一个
ObjC
消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP
这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。 -
你会发现
IMP
指向的方法与objc_msgSend
函数类型相同,参数都包含id
和SEL
类型。每个方法名都对应一个SEL
类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id
和SEL
参数就能确定唯一的方法实现地址;反之亦然。
runtime
做的那些事
首先需要明确的是,在objc
中,直到运行时才将消息与方法实现绑定。而这些工作都是runtime
为我们做的。
runtime
之消息转发
其实[receiver message]
会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
在平时的调用中,看起来像是objc_msgSend
返回了数据,其实objc_msgSend
从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
- 检测这个
selector
是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain
,release
这些函数了。 - 检测这个
target
是不是 nil 对象。ObjC
的特性是允许对一个nil
对象执行任何一个方法不会 Crash,因为会被忽略掉。 - 如果上面两个都过了,那就开始查找这个类的
IMP
,先从cache
里面找,完了找得到就跳到对应的函数去执行。 - 如果 cache 找不到就找一下方法分发表(即
class
里的method_list
表)。 - 如果分发表找不到就到超类的分发表去找,一直找,直到找到
NSObject
类为止。 - 如果还找不到就要开始进入动态方法解析了,后面会提到。
当然,objc_msgSend
函数只是一般情况下的调用,还有会有例如给父类发送消息,返回值是结构体而不是数值等情况,会采用其他的例如objc_msgSendSuper
函数等,在此不再叙述。
消息转发的第一步: 动态方法解析
所谓动态方法解析是发生在objc_msgSend
函数查找完所有类对象or元类方法列表后仍未找到方法实现第一个调用的方法,动态方法解析发生在消息转发之前:
+ (BOOL)resolveClassMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相应的类方法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相应的对象方法调用
在平时的开发中,最常用的情况就是声明某个属性为@dynamic
后,需要我们自己提供setter
及getter
方法时。如:
@dynamic propertyName;
添加如上关键字修饰属性表示告诉编译器我们会动态的提供存取方法,此时动态方法解析就是个不错的选择:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
第二步: 重定向
动态方法解析虽然强大,可以动态地为一个类添加方法,但它也有个很大的弊端:只能为当前类添加方法。如果我们想在消息无法解读时调用其他类的方法呢?此时就要用到重定向了。
重定向是发生在动态方法解析之后,完整的消息转发之后的。在此处我们就可以动态的将一条消息转换为其他类的调用:
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(intstanceNoImpMethod)) {
return [testClass class];
}
return [super forwardingTargetForSelector:aSelector];
}
需要注意的时,此时我们将消息转发给了testClass
这个类,就表示我们希望这个不能被当前类解读的消息中的sel
能被testClass
执行。如果testClass
类中没有这个sel
的类方法,程序一样会crash:
+[testClass intstanceNoImpMethod]: unrecognized selector sent to class 0x103122ea8
如果我们在此方法中直接返回self
或者nil
,都会跳过这个步骤直接进入完整的消息转发机制。
所以如果你想把这个消息解读为其他类的对象方法,就要返回这个类的对象,如果想解读为类方法,就要返回类对象。
同理,如果你想转发一个含有类方法的消息,就应该调用:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(intstanceNoImpMethod)) {
return [testClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
于是你可以大开脑洞:把一个类的类方法or对象方法换成另一个类的对象方法or类方法,怎么组合随你,只要注意sel
的名称一致即可。有木有感觉很酷😃
ps:我在此处混用了类与类对象,其实这俩是一个东西,如果你不能理解,戳这里:深入理解objc中的对象与类
可见,利用重定向是在objc
模拟多继承的一种方法。但是重定向和动态方法解析一样都有弊端,那就是还是不够“灵活”。消息中sel
不能被我们更换。设想这种情景:如果我们又想更换sel
,又想更换消息的接受者,此时我们该怎么做?
第三步:完整的消息转发机制
上文我们降到了如果动态方法解析失败,进入重定向,那么重定向也失败了,就来到了完整的消息转发机制:
这里遇到了些小困难,接下来在填坑吧
runtime
之健壮的实例变量
@property
大家每天都在用,但也许你不知道,runtime
在你为类添加了一个属性时,它会将这个成员变量(iva
)存放在类对象里。这和比如JAVA
等语言有着很大的不同:
objc
将实例变量当做一种存储偏移量(offset)所用的特殊变量交由类对象保管。偏移量会运行期间查找,如果类的定义变了,那存储的偏移量也就变了。这样的话,无论何时访问实例变量,都能获取到正确的值。
基于这个原因,我们才能该动态的给一个类添加属性,因为属性列表本来就是动态查找的。
8月21日更新:关于健壮的实例变量,还有这样的说法:
当一个类定义了某些成员变量后编译一次后,再次改变该类的成员变量,会导致偏移量发生改变。在有
runtime
的情况下,它会自动帮你调整偏移量,以保证不用再次编译文件。
关联对象
关联对象指的是动态的为一个对象添加变量,之前有写过介绍的短文:在分类中给类添加属性。
神奇的Method Swizzling
之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。
上一个🌰:
+ (void)load{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(cumtomMethod:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
调用:
- (void)cumtomMethod:(BOOL)animted{
[self cumtomMethod:animted];
NSLog(@"1111");
}
输出:
2016-08-18 00:39:04.658 总结测试[80016:3763455] 1111
我刚开始看这段代码也是晕的一塌糊涂,现在回想起来是没能立即SEL
与IMP
,下面是具体的调用过程:
系统调用viewWillAppear:
(SEL
) ----> 来到了customMethod
的IMP
-----> 我们自己调用customMethod:
的SEL
-----> 系统viewWillAppear:
的IMP
目前为止小弟也只是知道有这么个东西,还真没用到过这玩意。真有兴趣可以看看这篇:Objective-C的hook方案(一): Method Swizzling
8-21凌晨补充下:最近基友分享了一篇关于Method Swizzling
的应用方案,是腾讯一面提到的。有兴趣可以看下。
扯扯淡😪
这篇文章花了不少心血,也通过撰写这篇文章彻底重新认识了Runtime
这个之前小白时看都不敢看的东西。也观摩了不少大神的博客,感觉平时应该多注意这些好的资源,有时候比闷头写代码强不少😃。
在此放一下我特别喜欢的一位博主的博客:玉令天下的博客,就像引言所说,这篇文章不过是我读这为大神博客的学习笔记罢了。作为同龄的开发者,很是汗颜啊,共勉吧~~