再看 Runtime

October 6, 2018   

runtime 这个老生常谈的话题。Google一下会有不下10几篇的详细介绍 runtime 原理的文章。最近项目中因为埋点上报功能用到了,感觉相当强大。现站在巨人的肩膀上,整理些我认为比较重要的几点吧。

之前一直以为 runtime 是 OC 的特有功能,因为 swift 是一门静态语言,它从发明之出就是为了解决 OC 的运行时在决定对象的真实类型而造成的不安全。直到利用 Method Swizzling 交换了 UIViewControllerviewWillAppearviewWillDisappear 方法,实现页面进入和离开时的留存时长上报功能时,才知道原来 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 指向它自己本身。

所以当一个实例对象调用方法对象方法时过程为:

  1. 先通过自身 isa 指针知道自己的类对象,看看类对象上有没有需要调用的方法;
  2. 如果类对象没有,则通过类对象的 super_class 指针 找到类对象的父类对象,看看有没有需要调用的方法;
  3. 如果还没找到,则重复第二步,一直找到 NSObject 类时还没找到,则抛出方法找不到的错误,如果找到就返回给调用实例,并在 每个经过的 struct_cache 结构体对象(本质上是一个散列表)里缓存一份,以便下次调用,提高效率。

类对象的类方法方法调用过程:

  1. 类对象通过 isa 指针找到自己的元类对象(meta class),看自己的元类对象中有没有需要调用的方法;
  2. 如果没有,则通过元类对象的 isa 指针找到元类对象的父类,找有没有该方法;
  3. 直到找到 NSObject 的元类对象,NSObject 元类的父类为自己,所以最后都会找到 NSobject。找到则返回给调用者,没有就抛出方法找不到。

结论

  • 对象方法保存在类对象中
  • 类方法保存在元类对象中

OC 的方法调用本质上就是消息发送,这也是这门语言从 SmallTalk 继承过来的特性。

所有的方法调用最终都会转成

objc_msgSend(id, SEL);

这就是 OC 经典的消息转发过程,大致可以分为三个阶段

  1. 消息发送阶段
  2. 动态解析阶段
  3. 消息转发阶段

消息发送阶段时,过程大致如下

  • 如果方法列表已经排序了,则利用二分查找,否则顺序查找
  • 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的详细说明

参考链接


comments powered by Disqus