关于Runtime的那些事

Runtime的那些事

前言

今天我们一起来聊聊Runtime的那些事。作为一名iOS开发者,如果不知道Runtime,就如同Java工程师不知道JVM、C工程师不知道指针一样尴尬。

当然,掌握Runtime,也是一名UI工程师进阶为高级工程师的必经之路。鲁迅曰,不想当厨师的司机不是一名好工程师(鲁迅:我说的?黑人问号)。哈哈开个玩笑,接下来咱们正式进入主题,跟上步伐,玩的愉快!

切入点

研究一件事情,我比较喜欢先寻找切入点,一方面这样会比较符合人的认知心理,更容易找到问题本源,另一方面,这样也是为了避免盲目探究,最后偏离了方向,忘记了初心。
那么,尖锐的问题来了,Runtime知识看起来是一个非常庞杂的体系,哪里入手呢?
切入之前,我们得先了解下,Runtime是什么?一起来看看官方怎么说:

// 下面一段来自官方文档
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, 
and as such is linked to by all Objective-C apps.

// 下面一段来自官方编程指南
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. 
Whenever possible, it does things dynamically. 
This means that the language requires not just a compiler, 
but also a runtime system to execute the compiled code. 
The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

上述资料分别摘自官方文档与官方编程指南,文章底部有链接。

按照官方的解释,Runtime是一个运行时库,为OC语言提供动态属性的支持,OC语言为了保证其强大的动态能力,所以把编译期、链接期的尽可能多的决议推迟了到了运行时,所以除了编译器,还需要一个运行时系统。而Runtime就是这个运行时系统,相当于是OC语言的的操作系统,这也是OC语言的运行机制。所以到这里,我们对Runtime的认识明晰了很多。

我们知道,OC语言是由Smalltalk语言演化而来【可以阅读下面的'扩展知识'小节】,是一种消息型的语言,即该语言采用“消息结构”而非“函数调用”来实现的。“消息结构”的语言运行时所执行的代码是由运行环境决定的,而“函数调用”的语言则有编译器决定。而结合前面官方对Runtime的定义,我们可以很明确,是Runtime运行时系统支撑起了OC语言的动态性的。

所以我们的研究切入点一下就找到了,那么就从OC语言的消息机制说起。

OC语言的消息机制

我们可以通过OC语言的消息机制为切入点,窥探一下Runtime的一些细节。
我们通过clang命令,将OC代码重写为C++,可以看到更底层一些的实现细节。

// clang命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
// OC代码
ZKPerson *person = [[ZKPerson alloc] init];
[person run];

// C++代码
ZKPerson *person = ((ZKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((ZKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZKPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));

通过上面的探索,我们看到了一个有趣的现象:OC中的方法调用,其实都被转换为了objc_msgSend的调用。
苹果的Runtime相关代码是开源的,我们可以通过阅读源码,追踪objc_msgSend的调用流程,一步一步了解消息机制的本质。
当我们在源码工程中搜索objc_msgSend的时候,发现并不像上次研究RunLoop那么顺利。原来这个方法没有在出现在objc-runtime-new.mm文件中,而是在汇编文件中。我们选择与真机设备arm64架构匹配的汇编文件研究:objc-msg-arm64.s。由于汇编语言的函数会默认将高级语言的函数名加上_前缀,所以我们在这个文件中搜索_objc_msgSend。找到了函数实现:

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

这个函数不算长,即使不太懂汇编,我们也大概能明白这个函数的意思:

  • 首先做一些边界检测,然后通过GetClassFromIsa_p16 p13获取isa指针,我们知道,通过isa指针可以找到类对象或元类对象,后面小节会对isa详细介绍。
  • 找到isa后,调用CacheLookup NORMAL,这个方法是个.macro宏,具体代码我就不贴了,毕竟不是重点,我介绍下这个方法中做的事情:
    • 首先是在这个类的方法缓存列表中查找是否有这个方法,如果命中直接调用。
    • 如果没有命中缓存,则调用CheckMiss ==> __objc_msgSend_uncached ==> MethodTableLookup ==> __class_lookupMethodAndLoadCache3,最后这个方法的实现是在objc-runtime-new.mm中,而这个方法直接做个参数透传,调用了lookUpImpOrForward函数,而这个函数,就是OC语言的消息机制的核心实现。我们一起来看下:
消息发送机制:

这个函数主要做了如下几件事情

  • 对该类和其父类的内存结构进行重新修正组装,为class_rw_t数据开辟内存空间(因为需要在class_rw_t这个数据结构中存储分类动态添加的方法、属性和协议等数据),通过调用methodizeClass对该类的方法列表、属性列表和属性列表进行修正,即把该类的分类中的这些内容添加到该类的内存结构中。
if (!cls->isRealized()) {
    realizeClass(cls);
}

经过上一步,该类和其父类的内存结构进行修正,其方法列表、属性列表和协议列表都是最新最全的了。接下来就可以真正进入消息发送流程了。

  • 首先在该类中查找。

    • 查找该类的方法缓存列表。如果命中则直接done,也即是将IMP返回。
    // Try this class's cache.
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    
    • 在该类的方法列表中查找。如果查找到了,则把该方法保存在该类的方法缓存列表中,再将IMP返回给外部。
    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }
    
  • 如果在该类中没有找到,则通过superClass指针向父类查找。
    查找逻辑同上,先从缓存中查找,没找到再从方法列表中查找。这里有个细节要注意,如果在父类中找到了该方法,不管是在父类的方法缓存列表中找到的还是在父类的方法列表中找到的,找到后都要把该方法缓存在该类cls中的方法缓存列表中去,方便下次调用。

    // Try superclass caches and method lists.
      {
          unsigned attempts = unreasonableClassCount();
          for (Class curClass = cls->superclass;
               curClass != nil;
               curClass = curClass->superclass) {
              
              // Superclass cache.
              imp = cache_getImp(curClass, sel);
              if (imp) {
                  if (imp != (IMP)_objc_msgForward_impcache) {
                      // Found the method in a superclass. Cache it in this class.
                      log_and_fill_cache(cls, imp, sel, inst, curClass);
                      goto done;
                  }
                  else {
                      // Found a forward:: entry in a superclass.
                      // Stop searching, but don't cache yet; call method 
                      // resolver for this class first.
                      break;
                  }
              }
              
              // Superclass method list.
              Method meth = getMethodNoSuper_nolock(curClass, sel);
              if (meth) {
                  log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                  imp = meth->imp;
                  goto done;
              }
          }
      }
    
  • 以上的是常规的消息发送流程。如果最终找到NSObjectRootClass还是没有找到,则启动了消息转发流程。

Tea time
消息转发机制:
(1)解铃还须系铃人,先征询接收者能不能动态添加方法。即+resolveInstanceMethod

运行期组件会首先问该方法所属的类:是否能动态添加方法来处理当前这个未知的选择子,即动态方法解析。当这个对象收到无法解读的消息后,会调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

其返回值是Boolean类型,表示这个类是否能新增一个实例方法来处理未知消息。如果未知方法为类方法,会询问resolveClassMethod来处理未知类消息。

使用这个方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就行了。比如CoreData框架中的NSManagedObjects对象的属性用@dynamic修饰,表示这些属性的存取方法会在编译期自动添加,在编译期会以ORM的方式模型映射到数据库进行数值存取生成Setter和Getter。

这个阶段非常重要,我们在实际编程中,runtime动态添加方法也多是在这个阶段进行的。常见的写法如下:

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    if (...) {
        class_addMethod(self, selector, IMP, ...);
    }
}
(2)如果接收消息的类没有通过阶段1来动态增添方法来处理未知消息,那么运行时组件就会问当前接收者:你既然不想处理这个消息,那能不能把这条消息转发给其他接收者来处理。这时当前接收者灵机一动,哦隔壁老王应该可以响应这个消息,那就转发给他吧。于是,通过下面代码,将未知消息成功甩锅出去:
- (id)forwardingTargetForSelector:(SEL)selector

该方法的返回值即是把未知消息甩向的对象。如果成功甩出去了,那么消息转发至此结束,皆大欢喜。如果没有找到背锅侠(备援响应者),则返回nil。那这时候,运行期组件就很绝望:你自己不想动态添加方法来响应这个未知消息,也不想甩给其他人来响应,有些绝望,于是,运行期组件只好求助消息派发系统,进入了下一步:

(3)运行期组件被消息接收者两次拒绝后,生无可恋,于是向消息派发系统求助。于是把未知消息的所有细节封装包裹在NSInvocation对象中,包括选择子SEL、接收者Target和参数等,触发后消息派发系统将亲自出马,把消息指派给目标对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:@"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation setTarget:realObject];
    [anInvocation invoke];
}

注意,由于这个步骤跟上面的步骤二类似,可以指定新的接收者来处理未知消息。他可以先改变这条未知消息的内容再转发出去,如更换选择子,追加参数等。实现此方法时,若发现某调用操作不应由本类处理,则会调用超类的同名方法,即继承体系中的每个类都有机会处理此请求,直至NSObject。如果最后调用了NSObject类的方法还是没能处理,那么该方法会继而调用老少皆知的doesNotRecognizeSelector以抛出异常,至此,未知消息以最终未能处理来正式告终。

扩展知识

为了保证上面章节的整体行文的流畅性,下面我把一些我认为比较重要但是跟这篇文章的主题不太强相关的知识写在这个小节中,后面如果有时间的话(研究表明:说出"后面如果有时间的话"这句话的人通常不会有时间,摊~)我可能会单独成一篇文章。

关于 OC 语言的一些历史

OC是Brad Cox和Tom Love在1980年初发明出的一种程序设计语言,跟同时代的C++一样,都是在C的基础上加入面向对象特性扩充而成的。

他是一门面向对象、消息型语言,由消息型语言的鼻祖Smalltalk演进而来,即该语言采用“消息结构”而非“函数调用”来实现的。(事实上,SmallTalk 可以说是世界上第一个真正的面向对象的语言,第一个具备垃圾回收的语言,第一个拥有真正的集成开发环境的语言,第一次引入了MVC的概念来开发软件的语言)。

我们知道,C++一直是如日中天,霸榜编程语言排名前列30年之久,而作为同样是基于C语言扩充而来的语言-OC,为什么表现的不尽如人意,直到2010年才被众多世人知晓呢?

造成这种结果的原因,跟苹果公司有着密切的关系。当年(1988年)乔布斯创办的NeXT公司选用OC作为其御用程序开发编程语言,并买下了版权, 并且扩展了著名的开源编译器GCC,使之支持 Objective-C 的编译。种种的一切,给OC带来了生机,但同时也由于版权的限制和苹果公司的的孤傲,导致OC不能像Java、Swift、C++等发展这么迅速。

当然,OC对苹果公司的软硬一体化的战略部署,起到了举足轻重的作用,想想如今的谷歌深陷跟甲骨文关于Java版权之争难以自拔,不禁对当年老乔的决定心生敬畏。

参考

  1. Objective-C Runtime 编程指南
  2. Objective-C 编程指南
  3. Objective-C Runtime
  4. OC Runtime 源码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,257评论 25 707
  • 写在前面 本文原标题《以iPhone 6 为例介绍手机内置传感器 》,是我的《传感器》课程的课后大作业。说来之所以...
    继续海阔天空阅读 29,525评论 2 17
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,975评论 4 60
  • 使用滤镜调节照片的饱和度、亮度和对比度值,再调整曝光度实现效果: CIFilter的CIColorControls...
    LJ的ios开发阅读 2,595评论 0 0
  • 都说偏见和欲望害死人,许强可以作证。这些年,他霉到了极点,干什么都倒霉,人称“霉神”。 前年九月,一天上午,上班途...
    ZHANG顽石点头阅读 1,007评论 3 13