iOS Runtime 原理

PS 一些关于runtime的小demo在我的下一篇文章[iOS-Runtime-实践篇](http://www.jianshu.com/p/d6a2656fc2cb)中

我们都知道Objective-C是一门动态语言,动态之处体现在它将许多静态语言编译链接时要做的事通通放到运行时去做,这大大增加了我们编程的灵活性。

毫不过分地说,Runtime就是OC的灵魂。接下来我就要拨开OC最外层的外衣,带大家看看OC的真面目(C/C++)。

类和对象

为了更好地说明类在底层的表现形式是怎样, 我们将上面代码利用clang -rewrite-objc Person.m指令将其用C/C++重写, 一窥究竟.

把不必要的删除, 整理后为下面

原来(显然), 我们的类其实就是一个结构体!!! 类跟我们的对象一样, 都有一个isa指针, 所以类其实也是对象的一种.

isa指针

isa指针非常重要, 对象需要通过isa指针找到它的类, 类需要通过isa找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用.

 

实例对象在调用方法时, 首先通过isa指针找到它所属的类, 然后在类的缓存(cache)里找该方法的IMP, 如果没有, 则去类的方法列表中查找, 然后找到则调用该方法, 找不到则报错.

类对象调用方法则如出一辙, 通过isa指针找到元类, 然后就跟上述一致了. 这里涉及的发送消息机制下面会详细讲..

下面展示一些运行时动态获取对象和类的属性的C语言方法

类和类名 :

ivar和属性 :

方法 :

这里说个注意点 : addIvar并不能为一个已经存在的类添加成员变量, 只能为那些运行时动态添加的类, 并且只能在objc_allocateClassPairobjc_registerClassPair这两个方法之间才能添加Ivar.

消息发送和转发机制

在OC中, 如果向某对象发送消息, 那就会使用动态绑定机制来决定需要调用的方法. OC的方法在底层都是普通的C语言函数, 所以对象收到消息后究竟要调用什么函数完全由运行时决定, 甚至可以在运行时改变执行的方法.

[person read:book];
会被编译成
objc_msgSend(person, @selector(read:), book);

objc_msgSend的具体流程如下

1. 通过isa指针找到所属类
2. 查找类的cache列表, 如果没有则下一步
3. 查找类的”方法列表”
4. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
5. 找不到, 就沿着继承体系继续向上查找
6. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
7. 找不到, 执行”消息转发”.

消息转发

上面我们提到, 如果到最后都找不到, 就会来到消息转发

  • 动态方法解析 : 先问接收者所属的类, 你看能不能动态添加个方法来处理这个”未知的选择子”? 如果能, 则消息转发结束.
  • 备胎(后备接收者) : 请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.
  • 消息签名 : 这里会要求你返回一个消息签名, 如果返回nil, 则消息转发结束.
  • 完整的消息转发 : 备胎都搞不定了, 那就只能把该消息相关的所有细节都封装到一个NSInvocation对象, 再问接收者一次, 快想办法把这个搞定了. 到了这个地步如果还无法处理, 消息转发机制也无能为力了.
动态方法解析 :

对象在收到无法解读的消息后, 首先调用其所属类的这个类方法 :

假如尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法resolveClassMethod:

备胎 :

动态方法解析失败, 则调用这个方法

通过备胎这个方法, 可以用”组合”来模拟出”多重继承”.

消息签名 :

备胎搞不定, 这个方法就准备要被包装成一个NSInvocation对象, 在这里要先返回一个方法签名

完整的消息转发 :

给接收者最后一次机会把这个方法处理了, 搞不定就直接程序崩溃!

在这里能做的比较现实的事就是 : 在触发消息前, 先以某种方式改变消息内容, 比如追加另外一个参数, 或是改变选择子等等. 实现此方法时, 如果发现某调用操作不应该由本类处理, 可以调用超类的同名方法. 则继承体系中的每个类都有机会处理该请求, 直到NSObject. 如果NSObject搞不定, 则还会调用doesNotRecognizeSelector:来抛出异常, 此时你就会在控制台看到那熟悉的unrecognized selector sent to instance..

上面这4个方法均是模板方法,开发者可以override,由runtime来调用。最常见的实现消息转发,就是重写方法3和4,忽略这个消息或者代理给其他对象.

Method Swizzling

被称为黑魔法的一个方法, 可以把两个方法的实现互换.
如上文所述, 类的方法列表会把选择子的名称映射到相关的方法实现上, 使得”动态消息派发系统”能够据此找到应该调用的方法. 这些方法均以函数指针的形式来表示, 这种指针叫做IMP,

 

OC运行时系统提供了几个方法能够用来操作这张表, 动态增加, 删除, 改变选择子对应的方法实现, 甚至交换两个选择子所映射到的指针. 如,

经过一些操作后的NSString选择子映射表

如何交换两个已经写好的方法实现?

通过Method Swizzling可以为一些完全不知道其具体实现的黑盒方法增加日志记录功能, 利于我们调试程序. 并且我们可以将某些系统类的具体实现换成我们自己写的方法, 以达到某些目的. (例如, 修改主题, 修改字体等等)

KVO原理

KVO的实现也依赖Runtime. Apple文档曾简单提到过KVO的实现原理 :

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

Apple的文档提得不多, 但是大神Mike Ash在很早很早以前就已经做过研究, 摘下了KVO神秘的面纱了, 有兴趣的可以去查下, 这里不多深究, 只是简单阐述下原理.

原来当你对一个对象进行观察时, 系统会自动新建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. 然后把原对象的isa指针指向这个新类, 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.

不仅如此, Apple还重写了原类的- class方法, 视图欺骗我们, 这个类没有变, 还是原来的那个类. 只要我们懂得Runtime的原理, 这一切都只是掩耳盗铃罢了.


后记

这只是我的Runtime文章的第一篇, 之后还会有Runtime实践篇以及利用Runtime解决实际问题的几个demo, 感兴趣的话还请大家关注关注^_^

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

2 3 收藏 评论

关于作者:Jerry4me

广工大-大三生 iOS Dev 个人主页 · 我的文章 · 9 ·     

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部