很早就知道了Objective-C Runtime这个概念,「Objective-C奇技淫巧」「iOS黑魔法」各种看起来很屌的主题中总会有它的身影;但一直没有深入去学习,一来觉得目前在实际项目中还没有必要了解,二来懒。但,若想成为一个合格的iOS开发者,这个东西是躲不过的,好吧,抡起胳膊开始吧,争取一点点把它整明白吧!
和了解其他技术一样,在了解一个东西之前,我总是问自己,这个有啥实际意义?为什么要了解她?多自问一些问题,找到实际意义,在理解上总会容易一些,学习也自然更有目的性一些。
好吧,看了十几篇博客和iOS文档,还是很容易针对Objective-C Runtime提出一些问题的,有了这些问题,理解Objective-C Runtime就自然不会过于枯燥了,譬如:
- 在iOS中调用方法为什么叫「发送消息」,为什么这个概念这么重要?
- KVO的实现原理是什么,是否涉及Objective-C Runtime?
- Category的原理是什么?为什么其他语言中没有这个东东?
- Category中以「关联对象」的方式添加动态属性的原理什么?
看了一些大牛的博客之后,对Objective-C Runtime的重要性在心里基本有谱了,但路要一步一步走,饭要一口一口吃,先将一些基础性的概念进行整理,「消息转发」「Method Swizzling」之类的高端东西放在之后的博客分析吧!
关于Objective-C Runtime
Runtime的学习资源
Runtime的学习资源非常丰富,下面是个人的一些整理:
- 《Objective-C Runtime Programming Guide》(官方文档);
-
/usr/include/objc/
下的头文件,譬如objc.h
,runtime.h
,NSObject.h
,message.h
等;/usr/include/objc/
下的头文件不多,都可以看一下; - 《Effective Objective-C 2.0》;
- 《理解 Objective-C Runtime》;
- 《Objective-C对象模型及应用》;
- 《深入理解Tagged Point》;
- 《Objective-C Runtime》;
- Objective-C Runtime系列博客:
- 刨根问底Objective-C Runtime系列博客:
- 《继承自NSObject的不常用又很有用的函数(2)》;
P.S: 值得一提的是,Objective-C Runtime是开源的,任何时候都能从opensource中获取源代码。
动态语言v.s静态语言
在网上阅读Objective-C Runtime相关的信息时都提到了「Objective-C是一门动态语言…」「作为一门动态语言…」之类的字眼,这让我有些诧异。因为根据我之前的理解:虽然动态和静态语言都是相对的,但Objective-C怎么也不算动态语言吧!动态语言是Python、Javascript这种弱类型语言吧?!
关于Objective-C是否是动态语言,笔者才疏学浅,不敢妄论,知乎中关于这个有讨论:Objective-C是动态语言吗?为什么?
但无论如何,它比C++、C这类传统静态语言具备更多的动态特性,说它是动态语言或许不为过吧。暂时不纠结这个问题了,希望以后有更多的自信回答这个问题。跟随主流意见,暂时认为Objective-C是一门动态语言吧。
P.S: 后来又仔细阅读了一下《Effective Objective-C 2.0》,其中对「Objective-C是动态语言」有更好的佐证,后面阐述「Objective-C的消息传递机制」时再补充说明吧!
P.S: 关于OC是动态语言这个话题,总会少不了「动态特性」「动态类型」「动态绑定」这几个关键词的讨论分析,之前在《Objective-C基础知识》中进行了简单的概述。
Runtime是什么
Runtime是什么?为什么有Runtime这个东东?
《Objective-C Runtime Programming Guide》的introduction的第一段就回答了这个问题:
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.
不过貌似不够清晰,理解Objective-C Runtime中有更浅显的解释:
Objective-C是面相运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚至可以交换方法的实现,等等;这就需要使用runtime,runtime可以做对象自省查看他们正在做的和不能做的(don’t respond to)并且适当地分发消息。
Objective-C的Runtime是一个运行时库(Runtime Library),它是一个主要使用C和汇编写的库,为C添加了面相对象的能力并创造了Objective-C。这就是说它在类信息(Class Information)中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime创建了所有需要的结构体,让Objective-C的面相对象编程变为可能。
关于Runtime的一些术语
各种Runtime资料中有很多术语,有些是以前认识但理解不深刻的,譬如id
、Class
,有些是很少接触的,譬如isa
。
为了更好地理解Runtime,先了解Runtime的一些术语是非常必要的。
首先简单提一下objc_class
,它是一个结构体,其中包含一些类信息;然后是objc_object
,它也是一个结构体,但它只包括一个条目 – isa
,后者是一个objc_class
指针;
P.S: 为了查看方便,把objc_class
、objc_object
等的定义源码都拷贝出来了,详见foo.c。
其实,说到底,NSObject
就是建立在objc_class
这个结构体之上的,所以了解objc_class
对于了解NSObject
以及Cocoa的对象模型是非常重要的。
P.S: objc_class
和isa
相对而言信息量比较大,后文再详细阐述。
OK,接着来看建立在objc_class
和objc_objec
t基础之上的一些关键字。
Class
Class
(不是class哦),它在<objc/objc.h>
中定义,如下:
|
可以看到Class
其实是一种指针类型,即用于指向objc_class
结构体。NSObject
中定义的方法-(Class)class
用于返回其对应的objc_class
结构体指针;
除了-(Class)class
方法之外,NSObject
类中还有一些常见方法包含Class
类型参数或者返回Class
类型返回值,如下:
|
id
对于有过iOS开发经验的人而言,id
使用太广泛了,不过似乎对它还没有过比较深入的理解;事实上,一直以为id
和NSObject *
等价呢!现在看来,这种理解太naive了,它也在<objc/objc.h>
中定义:
|
SEL
|
没找到objc_selector
的定义,但根据网友的描述:其实它就是个映射到方法的C字符串,可以用Objc编译器命令@selector()
或者Runtime系统的sel_registerName
函数来获得一个SEL类型的方法选择器(通常简称:选择子)。
考虑到Xcode对@selector
的支持比对sel_registerName
的支持更好,所以@selector
貌似用得更多一些,但有时候sel_registerName
或许更简洁一些。
譬如有一段代码:
|
但当前上下文中没有isReadyForEditing
这个方法,所以编译器会有警告;当然,可以有各种方式来关闭警告,如下是其中一种:
|
但此时,若改用sel_registerName
,则这个警告就没了!
P.S: 不晓得Apple更青睐于哪一个,个人感觉是@selector
,因为sel_registerName
的使用几乎没碰到过。
IMP
IMP的定义如下:
|
可以了解到,IMP是一个函数指针。IMP是Implementation的缩写,一个函数是由一个selector(SEL),和一个implement(IML)组成的;Selector相当于门牌号,而Implement才是真正的住户(函数实现)。理解Selector和Implementation的关系蛮重要的!
Method
|
Ivar
|
Category
|
objc_property_t
|
可以通过class_copyPropertyLis
t和protocol_copyPropertyList
方法来获取类(Class)和协议(Protocol)中的属性,获取属性之后,还可以使用property_getName
获取属性的名字(C字串):
|
举个例子:
|
这个示例的执行结果也从侧面反映出了,NSObject
中没有定义属性(只有一个叫isa
成员变量)。
P.S: 属性v.s成员变量?这里留个尾巴,以后把这个问题也分析一下吧!
objc_class和isa
把objc_class
和isa
单独拧出来的原因是它们的信息量比较大,稍微复杂一点!
Objective-C是一种面向对象的语言。按照面向对象语言的设计原则,所有事物都应该是对象(严格来说Objective-C并没有完全做到这一点,因为它有象int
, double
这样的简单变量类型)。所以一定要有这个认识:Objective-C中,类也是对象。
在Cocoa中,所有类都继承自NSObject
,参考NSObject
在<objc/NSObject.h>
中的定义如下:
|
objc_class
的定义如下:
|
可以知道在运行时,所有类对象都有一个名为isa
的指针,这个指针指向一个objc_class
结构体,objc_class
结构体中包含一些与类相关的信息。问题是这个isa
指向的objc_class
结构体的信息所对应的是自己的信息还是对应的类的信息呢?当然是其所对应的类的信息,存储了变量列表、方法列表、遵守的协议列表等等。
简单来说,isa
指针被称为「is a」指针,顾名思义,它告诉了对象所属的类信息,描述「aX is a X」。
某个对象的isa
指针指向的objc_class
结构体存放的是其所对应的类的元数据(metadata),通过它我们可以知道该对象:所对应类的名字(name
)、可以做哪些事情(objc_method_list
是个二维方法列表)、包括哪些成员变量(objc_ivar_list
)、遵守哪些协议(protocols
)。
回到objc_class
,可以看到objc_class
结构体首变量也是一个isa
指针,这也印证了「类也是对象」这个说法。对象的isa
指针指向的是该对象的本类,而类的isa
指针指向的另外一个类被称为元类(metaclass),用来表述类本身所表具备的元数据,类方法就定义于此;也正因为类本身也是一个对象,所以类本身可以接收消息。譬如:考虑到NSObject
类本身也是一个对象(是metaclass的一个对象,常称之为类类型对象),所以[NSObject alloc]
其实可以看成是对NSObject
这个类类型对象发送一个消息(调用器alloc
实例方法)。
除了isa
指针之外,objc_class
结构体中还有一个变量super_class
,它指向了是这个类的超类(super class),可以看到这个super_class
不是一个一位数组,而是一个单独的指针,即一个类有且仅有一个super class,即所谓的「单继承」。
关于isa
和super_class
的更直观描述,还请看图:
到了这里,Objective-C的对象模型基本上解释清楚了;可能还有一个问题:最终的元类是啥?在Cocoa中,所有类都继承自NSObject
,而NSObject
的元类(不晓得叫什么名字,假设叫NSObjectMetaClass
)(上图中右上角的方框)也继承自NSObject
。有些绕,具体来说,NSObject
和NSObjectMetaClass
的isa
指针和super_class
指针指向情况是这样的:
-
NSObject
的isa
指针NSObjectMetaClass
(终极meta class),NSObjectMetaClass
指针指向自己; -
NSObject
的super_class
指针指向nil
,NSObjectMetaClass
的super_class
指向NSObject
;
这样就完了?!No!Objective-C的对象模型还有信息可挖,回过头来看objc_class
的ivars
变量和methodLists
变量:
|
在运行时,类(包括class和metaclass)的objc_class
结构体是固定的,不可能往这个结构体中添加数据,只能修改!譬如可以修改isa
指针,让它指向一个中间类;在我的理解里,应该也可以修改ivars
和methodLists
,让它们指向一个新的区域;若可以这样,那么就可以在运行时随意添加/修改/删除成员变量和方法了。
但是,貌似Objective-C Runtime没有提供修改ivars
和methodLists
指针值的接口。
也因此,ivars
在运行时指向的是一个固定区域,当然可以修改这个区域的值了,但这其实只是修改成员变量值而已;「在这个内存区域后面续上一段空余区域用于存放新的成员变量」?呵呵,想多了吧!因此,我们没办法在运行时为对象添加成员变量,这解释了为什么category中不能定义property(dynamic property不算);
P.S: 那为什么protocol中可以添加变量,在我的理解里,protocol是在编译器处理的。所以objc_class
中有一个变量叫protocols
;
和ivars
不同,methodLists
是一个二维数组。虽然我们没办法扩展methodLists
指向的内存区域,但是我们可以改变这个内存区域(这个内存区域存储的都是指针)的值。因此,我们可以在运行时动态添加(以及做其他的处理,譬如交换等)方法!
P.S: objc_class
结构体中还有一个变量cache
,顾名思义,它是用于缓存的,缓存啥呢?缓存方法,下一篇博客阐述「消息传递机制」时会谈到这个。
到了这里,谁还敢说Objective-C不是动态语言?