前言:
关于Runtime的资料网上一搜很多,但总是写的只言片语,不太全面。最近花了一个星期的时间重新学习Runtime,并整理了一个系列文章,并发表出来,同时也感谢开源贡献的开发者。这里共有三篇文章:
Runtime系列一:Runtime的前世今生
Runtime系列二:Runtime的原理
Runtime系列三:Runtime在项目中使用场景
一、 Smalltalk 与 C 的融合---Objective-C
三十几年前,Brad Cox 和 Tom Love 在主流且高效的 C 语言基础上,借鉴 Smalltalk 的面向对象与消息机制,想要搞出一个易用且轻量的 C 语言扩展,但 C 和 Smalltalk 的思想和语法格格不入,比如在 Smalltalk 中一切皆对象,一切调用都是发消息:
233 log
再比如用一个工厂方法来实例化一个对象:
p := Person name: 'sunnyxx' age: 26
在当时来看,一个具有面向对象功能的 C 语言真的是非常有吸引力,但必须得解决消息语法的转换,于是乎他们开发了一个 Preprocessor(预编译程序),去解析 Smalltalk 风格的语法,再转换成 C 语言的代码,进而和其他 C 代码一起编译。想法很美好,但 Smalltalk 语法里面又是空格、又是冒号的,万一遇到个什么复杂嵌套调用,语法解析多难写呀,于是乎他们想,诶呀别费劲了,把消息两边加个中括号吧,这样 Parser 写起来简单多了呢对吧:
[Person name:"sunnyxx"age:26];
这就造就了 Objective-C 奇怪的中括号、冒号四不像语法,这怎么看都是个临时的方案,但在当时可能是唯一方法,借用已有的 C 的编译器比重造一个成本低多了,而且完全兼容 C 语言。随着这几年 Apple 开发的火热,Objective-C 越来越成为 Apple 不爽的地方,先是恨透了在 GCC 上给 Objective-C 加支持,自己重建了个 Clang,后是干脆重新发明了个 Swift 来彻底代替,用 30 年的时间终于还完了这个技术债。
好的,虽然有了个 Preprocessor,但只能做到把 Smalltalk 风格的代码分析并转译成 C,还需要解决两个问题:
<1>: C 语言上实现一个 OOP 对象模型
<2>: 将 Smalltalk 风格的 Message 机制转换成 C 函数调用
对象模型的设计倒很省事,直接照搬 Smalltalk 的就好了:如 Class / Meta Class / Instance Method / Class Method 这些概念,还有一些关键字如 self / super / nil 等全都是 Smalltalk 的。这步转换在 Preprocessing 过程中就可以完成,因为重写后的 Class 就是原原本本的 C 语言的 Struct,只需要按 Smalltalk 中“类-元类”的模型设置好即可,无需额外的支持。
消息机制就不一样了,要实现向一个 target ( class / instance ) 发送消息名 ( selector ) 动态寻找到函数实现地址 ( IMP ) 并调用的过程,还要处理消息向父类传递、消息转发( Smalltalk 中叫 “Message-Not-Understood”)等,这些行为无法在 Preprocessing 或 Build Time 实现,需要提供若干运行时的 C 函数进行支持,所有这些函数打个包,便形成了最原始的Runtime。
所以最初的 Objective-C = C + Preprocessor + Runtime
作为单纯的 C 语言扩展,Runtime 中只要实现几个最基础的函数(如 objc_msgSend)即可,但为了构建整套 Objective-C 面向对象的基础库(如 Foundation),Runtime 还需要提供像 NSObject 这样的 Root Class 作为面向对象的起点、提供运行时反射机制以及运行时对 Class 结构修改的 API 等。再后来,即便是 Objective-C 语言本身的不断发展,新语言特性的加入,也不外乎是扩展 Clang 和扩展 Runtime,比如:
ARC:编译器分析对象引用关系,在合适的位置插入内存管理的函数,并需要把这些函数打包加到 Runtime 中,如objc_storeStrong,objc_storeWeak等,同时还要处理 dealloc 函数,自动加入对 super 的调用等。
Lightweight Generics:叫做 “轻量泛型” 是因为只增加了编译器检查的支持,而泛型信息并未影响到运行时,所以 Runtime 库无需改动。
Syntax Sugars:比如 Boxed Expr(@123)、Array Literal(@[...])、Dictionary Literal(@{...})和轻量泛型一样,只是把如@123在编译期 rewrite 成[NSNumber numberWithInt:123]而已,无需改动 Runtime。
Non Fragile Ivars: 类实例变量的动态调整技术,用于实现 Objective-C Binary 的兼容性,随着 Objective-C 2.0 出现,需要编译器和 Runtime 的共同配合。
因此,Runtime 的精髓并非在于平日里很少接触的那些所谓“黑魔法” Runtime API、也并非各种 Swizzle 大法,而是在 Objective-C 语言层面如何处理 Type、处理 Value、如何设计 OOP 数据结构和消息机制、如何设计 ABI 等,去了解这么一个小而美的 C 语言运行时扩展是怎么设计出来的。
参考文章:http://blog.sunnyxx.com/2016/08/13/reunderstanding-runtime-0/
二、Runtime简介
作为一门动态编程语言,Objective-C 会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。运行时系统(runtime)扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。所以它是值的你花功夫去理解的。
2.1、与静态语言编译后的区别
1、静态语言
一个静态语言程序,如下所示的C程序:
#include < stdio.h >
int main(int argc,const char**argv[])
{
printf("Hello World!");return0;
}
会经过编译器的语法分析,优化然后将你最佳化的代码翻译成汇编语言,然后完全按照你设计的逻辑和你的代码自上而下的执行。
2、Objective-C 动态语言
很常见的一个消息发送语句:
[receiver message]
会被编译器转化成
objc_msgSend(receiver, selector)
如果有参数则为
objc_msgSend(receiver, selector, arg1, arg2, …)
消息只有到运行时才会和函数实现绑定起来,而不是按照编译好的逻辑一成不变的执行。按照我的理解,编译阶段只是确定了要去向receiver对象发送message消息,但是却没有发送,真正发送是等到运行的时候进行。因此,编译阶段完全不知道message方法的具体实现,甚至,该方法到底有没有被实现也不知道。这就有可能导致运行时崩溃问题。
2.2、Objective-C Runtime的几点说明
1、runtime是开源的
是的,你没看错,runtime确实是开源的。目前苹果公司和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。其中苹果的版本可以在工程中引用
#import <objc/runtime.h> 点击右键jump to definition,进去查看
2、runtime是由C语言实现的
runtime做为Objective-C最核心的部分,几乎全部由C语言实现。这里的“几乎”所指的例外就包含有的方法(比如下面要说道的objc_msgSend方法)甚至是用汇编实现的!!
3、runtime的两个版本
Objective-C运行时系统有两个已知版本:早期版本(Legacy)和现行版本(Modern)。
在现行版本中,最显著的新特性就是实例变量是"健壮“(non-fragile)的:
在早期版本中,如果您改变类中实例变量的布局,您必须重新编译该类的所有子类。
在现行版本中,如果您改变类中实例变量的布局,您无需重新编译该类的任何子类。
此外,现行版本支持声明property的synthesis属性器。
目前iPhone 程序和 Mac OS X v10.5 及以后的系统中的 64 位程序使用的都是 Objective-C 运行时系统的现行版 本。其它情况(Mac OS X 系统中的 32 位程序)使用的是早期版本。
2.3、和Runtime system交互的三种方式
1、通过Objective-C源代码
大部分情况下,运行时系统在后台自动运行,我们只需编写和编译 Objective-C 源代码。当编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据 结构包含类定义和协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论 的类的对象和协议类的对象,方法选标,实例变量模板,以及其它来自于源代码的信息。运行时系统的主要功能就是根据源代码中的表达式发送消息。
2、通过类NSObject的方法
Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承 了NSObject的行为(NSProxy类是个例外)。然而,某些情况下, NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。
例如,NSObject 类定义了description方法,返回该类内容的字符串表示。这主要是用来调试程序 ——GDB 中的 print-object 方法就是直接打印出该方法返回的字符串。NSObject 类中该方法的 实现并不知道子类中的内容,所以它只是返回类的名字和对象的地址。NSObject 的子类可以重新实现该方法以提供更多的信息。例如,NSArray 类改写了该方法来返回 NSArray 类包含的每个对象的内容。
某些 NSObject 的方法只是简单地从运行时系统中获得信息,从而允许对象进行一定程度的自我检查。
例如,class 返回对象的类;isKindOfClass:和 isMemberOfClass:则检查对象是否在指定的 类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol: 检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
3、通过运行时系统的函数
运行时系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明 头文件在/usr/include/objc中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。还有一些函数构成了 NSObject 类方法的基础。这些函数使得访问运行时系统接口和提供开发工具成为可 能。尽管大部分情况下它们在 Objective-C 程序不是必须的,但是有时候对于 Objecitve-C 程序来说某些函 数是非常有用的。 这些函数的文档参见 Objective-C 2.0 运行时系统参考库。
网友评论