October 6, 2018
runtime
这个老生常谈的话题。Google一下会有不下10几篇的详细介绍 runtime 原理的文章。最近项目中因为埋点上报功能用到了,感觉相当强大。现站在巨人的肩膀上,整理些我认为比较重要的几点吧。
之前一直以为 runtime 是 OC 的特有功能,因为 swift 是一门静态语言,它从发明之出就是为了解决 OC 的运行时在决定对象的真实类型而造成的不安全。直到利用 Method Swizzling 交换了 UIViewController
的 viewWillAppear
和 viewWillDisappear
方法,实现页面进入和离开时的留存时长上报功能时,才知道原来 swift 也是可以使用 runtime 的。因为 Controller 继承自 NSObject
, 底层是一个 OC 对象。
那什么时候能用 OC 的 runtime 呢?
结论是:
凡是继承自 NSObject 的类都可以使用 Runtime。
用了这么多年的 NSObject 的,它到底是什么?
struct NSObject_IMPL {
Class isa;
};
可以看出,NSObject 本质上就是一个结构体,里面有一个 isa
的类对象。在看看这个 Class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
原来 isa
就是一个指向 objc_class
结构体的指针,顺着在看看 objc_class
结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
上面有一个关键信息 isa,每一个 objc_class 结构体中都一个 isa。用来找到该对象的类或元类。引用一张非常经典的图
可以看出
- 实例对象的 isa 指向 类对象
- 类对象的 isa 指向 元类对象,元类的对象的 isa 指向根元类,根元类的 isa 指向它自己本身。
所以当一个实例对象调用方法对象方法时过程为:
- 先通过自身 isa 指针知道自己的类对象,看看类对象上有没有需要调用的方法;
- 如果类对象没有,则通过类对象的
super_class
指针 找到类对象的父类对象,看看有没有需要调用的方法; - 如果还没找到,则重复第二步,一直找到 NSObject 类时还没找到,则抛出方法找不到的错误,如果找到就返回给调用实例,并在 每个经过的
struct_cache
结构体对象(本质上是一个散列表)里缓存一份,以便下次调用,提高效率。
类对象的类方法方法调用过程:
- 类对象通过 isa 指针找到自己的元类对象(meta class),看自己的元类对象中有没有需要调用的方法;
- 如果没有,则通过元类对象的 isa 指针找到元类对象的父类,找有没有该方法;
- 直到找到 NSObject 的元类对象,NSObject 元类的父类为自己,所以最后都会找到 NSobject。找到则返回给调用者,没有就抛出方法找不到。
结论:
- 对象方法保存在类对象中
- 类方法保存在元类对象中
OC 的方法调用本质上就是消息发送,这也是这门语言从 SmallTalk 继承过来的特性。
所有的方法调用最终都会转成
objc_msgSend(id, SEL);
这就是 OC 经典的消息转发过程,大致可以分为三个阶段
- 消息发送阶段
- 动态解析阶段
- 消息转发阶段
消息发送阶段时,过程大致如下
- 如果方法列表已经排序了,则利用二分查找,否则顺序查找
- receiver 通过 isa 指针找到 receiverClass
- receiverClass 通过 super_class 指针找到父类
如果方法还没找到,就回来进入第二个阶段,动态解析过程,大致流程为
-
动态解析时可以根据
SEL
名来为一个方法添加具体实现- (void)boo; + (BooL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(foo)) { Method method = class_getInstanceMethod(self, @selector(boo)); class_addMethod(self, sel, method_getImplentation(method), method_getTypeEncoding(method)); return YES; } return [super resolveInstanceMethod: sel]; }
或根据方法签名来添加
+ (BooL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(foo)) { class_addMethod(self, sel, (IMP)boo, "v@:"); return YES; } return [super resolveInstanceMethod: sel]; }
其中 v 代表方法的返回为 void,@ 代表 id 类型参数, : 代表 SEL 类型参数
如果动态解析阶段返回NO,则不会进入第三个消息转发阶段,默认返回 [super resolveInstanceMethod: sel]
的执行结果,进入消息转发阶段
如果走到最后都没找到合适的方法接收者,则会抛出 unrecognized selector sent to instance
的错误。
这里感谢很大家对runtime的详细说明