Objective-C与Runtime

来源:互联网 发布:国泰安数据库好用吗 编辑:程序博客网 时间:2024/06/10 06:07

笔者一直就想着是否能为Objective-C写写自己的理解和总结,不仅仅因为是笔者是Objective-C多年的重度开发者,更是因为这是一门有独特想法的,有创造性的,有优美语法的,有历史地位的编程语言。如果说对本文有什么预期的话,笔者希望能把一些类似“为什么是这样”的问题说清楚。

Objective-C发明于上世纪80年代,Objective-C的作者——Brad Cox和Tom Love,在接触到SmallTalk语言之后,一方面受到SmallTalk的启发,另一方面也是看好C语言有着巨大影响力和广阔前景,因此选择在C语言的基础上引入SmallTalk语言面向对象和消息派发的概念。最初的版本以C语言的扩展的形式实现的,在C编译器中编写支持Objective-C的预处理模块,预处理会先将Objective-C语法代码转化为C代码,再继续C代码的编译过程。1988年,以企业为目标客户的NeXT公司购买Objective-C的知识授权,紧接着扩展了著名开源编译器GCC,使其支持Objective-C,并且开发了AppKit和FoundationKit等基础库,Objective-C成为了NeXTSTEP系统(工作站)上“标准”的应用程序开发语言。

1996年,Apple公司收购了NeXT公司,NeXTSTEP/OPENSTEP系统成为Apple新一代操作系统OS X的研发基础。 2005年,Apple引入了牛人Chris Lattner以及他的LLVM技术团队,Objective-C新特性和编译优化第一次得到高水平编译器最高优先级的支持(GCC项目中Objective-C的优先级一直比较靠后),先从后端的代码优化和生成开始(LLVM),逐步扩展到前端的语法解析(Clang)。如今(2015),Objective-C已经拥有GCC之外更为先进更为优异的编译器套装选择——LLVM编译器,LLVM包括完整的前后端模块,模块化设计,非常友好的调试信息支持,最新版本6.1(2015)。

Objective-C是面向对象的,这是Objective-C最基本的的概念。关于面向对象,把一定的算法(函数)和数据(变量)以某种内在的联系绑定在一起,形成最基本的程序结构单元,这些结构单元即是经常谈及的对象,加上抽象二字,我们会称呼它为抽象对象,术语简称类;通过对一组变量的赋值操作(笔者认为不仅是变量,逻辑运算如闭包也是可以用于赋值)则会构成实体对象,术语简称对象(Objective-C一般也称作实例)。对象和对象之间不是完全独立的,通过巧妙的方式,它们之间能建立紧密的联系,比如继承、派生,对事物的抽象以及对代码的复用有着微妙而重大的价值。

Brad Cox和Tom Lov出版的第一本正式Objective-C著作,书名即为《Object-Oriented Programming, An Evolutionary Approach》。那么,为什么要对象,为什么要面向对象?这是个好问题,观察人类普遍的思维,我们理解这个世界使用最多的概念就是物体,我们擅长把感知到的一切抽象为一个个的物体,通过了解物体的构成,以及物体之间的作用关系,实现对这个世界的认知和作用的目的。这一直是非常奏效的!面向对象就是把人类的思维的天赋和积累的思想财富应用于编程,这样,程序对于增强生产能力/提高生活品质的效率和能力方面会大大提高。

/* 上图为FoundationKit中支持的集合对象——(不可变)数组,继承于根类NSObject,支持实现NSCopying在内的一系列协议(接口),count代表着有一个只读变量,- (id)objectAtIndex:(NSUInteger)index等表示数组支持的可供使用的方法(函数) */

消息派发是Objective-C函数(Objective-C实际称方法)调用的模式,前文亦有提及,概念继承于Smalltalk。Objective-C的对象相互调用函数,被看做是向目标对象传递消息,消息的发送者称作sender,消息的接收者称作receiver,消息中间传递的字符串称作selector(选择子)。

/* 上图的代码表示至少有两个明显receiver,self.view为其中一个消息接收者,传递的消息(字符串/选择子)为 “setBackgroundColor:“,UIColor表示一个类,类也是可以作为消息的接收者,字符串/选择子为 “yellowColor” */

消息的处理就是需要先确定实际执行的方法然后跳转过去并执行,我们理解为这是对该消息的回应,编译期间,单从一句”派发消息”的语法是无法确定实际执行的“目标函数”。只有在程序运行期间,实际执行的“目标函数”才能得到确定。这种在运行期间才确定实际执行的方法(“目标函数”),Objective-C称为动态绑定。消息派发这种工作机制明显区别另一著名面向对象编程语言——C++。C++调用对象的函数,函数与对象之间的关系,在编译期间就必须严格确定,如果car里面没有定义函数名为fly的函数,编译器不会通过,而是会报错。Objective-C如果向car发送字符串为”fly”的selector,即使car没有实现fly方法,编译器依然能够通过,但是运行期间则会因为获取不到实际执行的方法而抛出异常。这也就是说,消息派发的设计使得编译期间Objective-C非常包容对象所属的类。如上述,相同对象有相同的定义,称为类,类本身还可以看作对象——“类”对象,可以对“类”对象进行“类”的定义,比如比较运算,哈希,描述,类名等,总之一切皆为对象。C++里面我们可以基于称之为模板的方式实现对“类”的自定义,Objective-C通过统一基类比如NSObject(不仅仅只有NSObject,还可以是各类根协议)对所有类新增定义。你可以向任何包括空指针nil在内的对象发你想发的消息。消息派发的机制使得在不重新编译的情况下,在运行期间,干预或者说hook原来的target(方法、变量等)变得更易于实现,更有实际应用价值。这个是需要依赖于消息派发和动态绑定的实现机制——Runtime,但是Runtime并不仅仅为消息派发和动态绑定而work,它也是Objective-C面向对象、内存模型等特性的实现者。

在正式介绍Runtime之前,我们先继续介绍Objective-C的另外一个重要概念,笔者要说是Objective-C内存管理模型,程序运行时,创建一个对象总是要占用内存的,而内存总大小总归有限,所以当一个对象不再被需要时,应当及时回收它所占用的内存资源用于新的对象,Objective-C的内存管理原理,简单说就是“引用计数”机制。如果有模块需要引用一个对象,引用时会让对象统计用的引用计数值加1,并记录在对象的结构信息当中,当模块不再需要该对象的时候则减1,而当该对象的引用计数值为0时,就可以认为该对象不再被需要,及时销毁释放内存(回收资源)。

Objective-C对象的内存空间仅分配在“堆空间”(heap space)中,肯定是不会分配在“栈”(stack)上。我们知道,“栈”的占用和回收是有严格的数据操作规则,简称“先入后出”。函数执行时,传入的变量(当然包括对象变量)会按照确定的序列规则自动压入“栈”(占有内存资源),函数执行结束时,这些变量又会按照相反的序列规则自动弹出(释放内存资源)。因此,我们可以看出,“栈”其实是无法实施“引用计数”机制的,Objective-C否定使用“栈”存储对象的设想。

在语法上,Objective-C也无法像C++那样直接声明并创建一个对象变量,更无法直接操作该对象,Objective-C都是需要以类似C语言申请堆内存块的语法(alloc)那样创建一个对象变量,并且必须通过对象指针作为访问句柄,这跟C语言申请堆内存块非常类似。Objective-C这一“任性”的设计,也使得对象嵌套(一个对象作为另一个对象的成员变量)时,对象基于引用计数机制,其成员变量也必须递归地遵循引用计数机制。因为成员变量实际都是一枚枚对象指针,很可能是与其它对象共享同一个对象(指针都指向同一块内存),引用计数机制正是适合用于支持这种“共享”内存的管理。需要特别说明,如果可以像C++那样创建一个对象变量做成员变量,那么该成员变量会被存储在该对象所在的一块连续内存块,该对象销毁时能够自动把成员变量的占有的内存块全部释放收回,这与引用计数的机制并不太符合,所以,在Objective-C中对象变量不被支持也进一步得到理解。

/* 上图的接口(方法)是Objective-C中内存管理相关的接口 (方法)*/

Runtime(component)译名一般称为运行期组件,一个纯C语言写成的基础库(lib),Objective-C编写出来的程序必须得到Runtime的运行才能正常work,在Java、PHP或者Flash之类的编程语言当中,大家对于Runtime并不会太陌生,Objective-C的Runtime其实也是一回事。正是Runtime实现了Objective-C重要的特性,Objective-C面向对象、消息派发、动态绑定和内存管理都与Runtime的息息相关。那么,在Objective-C当中,对象、类、函数(方法)都是如何被构造并发挥作用的?前文提及,面向对象中的类,被看作抽象了的对象,Runtime也是秉持这一理念。Runtime是纯C写成,用struct(结构体)来描述对象(实体对象)和类(抽象对象)。

对象的struct比较简单,用*id作为结构体objc_object的指针别名,首个struct成员isa是Class类型的指针变量,正是该变量确定了对象所属的类。Class类型也是struct,是结构体objc_class的指针别名,用于描述类构成的struct,首个成员isa也是Class类型的指针变量(由两个结构体的首个成员均为Class类型的指针变量的设计使得我们能进一步体会到Runtime中,类的确有着和对象相同的看待),类的isa会指向称之为metaclass(元类)的struct,metaclass进一步抽象了类的特性,metaclass是Class类型,它的首个成员自然也是isa的Class类型的指针变量,不同的是metaclass的isa最终指向的是它自身,由此我们可以观察到,Class类型的struct正好是一种递归嵌套的设计,它正体现了面向对象无限抽象的理念,最终实现上指向自己则是实际工程处理的需要。一般我们还认为objc_class这个struct存放类的metadata(元数据),包括类的实例方法、类的实例变量以及类的超类指针等,从上图中的类的结构体声明中我们都可以直观看到这些数据。

Runtime还允许我们通过标准的接口(C函数)对所有Objective-C类的变量、方法、属性以及协议等等作查询和动态扩展,从而达到我们丰富项目中语言和类库特性的目标。

/* 上图的通过标准的Runtime API(C函数)打印UIKit中UIView的所有变量、属性以及方法*/

Runtime的另外一个重要的特性实现即为消息派发,objc_msgSend是消息派发最核心最基础的入口函数,除此之外还有objc_msgsend_stret,objc_msgSend_fpret,objc_msgSendSuper等函数,然而它们的重要性和作用远不及objc_msgSend。objc_msgSend函数会依据receiver和selector的来调用适当的方法。为了完成此操作,该函数需要在recevier所属的类中搜寻其“方法列表”,如果能找到与selector字符串名称相符的方法,就跳转至该方法。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。由此,我们可以看到,调用一个方法似乎需要相当的步骤。每一个步骤都是开销,是否会导致Objective-C有性能问题?所幸obj_msgSend会将匹配到得结果缓存在“快速映射表”(fast map),每个类都有这样一块缓存,若是后面还需要向该类发送和相同的selector消息,执行起来将会快许多。

当然,这种“快速执行路径”(fast path)还是不如“静态绑定函数调用”(statically bound function call)那样快,不过通过汇编等优化技术,映射表的查询开销已非常小,可以说,即使相比较C++的静态绑定,Objective-C的消息派发机制已经不是性能瓶颈所在。如果说以上的消息派发机制就是Objective-C动态绑定的全部内容,其实并不完全。当对象查询不到相关的方法,消息无法正确回应时,还会启动“消息转发”机制。是的,在支持“动态增加和替换”的方法列表之外,我们还能够提供其它的正常响应方式。消息转发还分为几个阶段,第一,先询问receiver或者说是它所属的类,看其是否能动态添加方法,以处理当前这个“未知选择子”(unkonwn selector),这叫做“动态方法解析”(dynamic method resolution),Runtime会通过回调一个类方法来寻求动态添加方法的支持。如果Runtime完成动态添加方法的询问之后,receiver仍然无法正常响应,则Runtime会继续向receiver询问是否有其它对象即其它receiver能处理这条消息,若返回能够处理的对象,Runtime会把消息转给返回的对象,消息转发流程也就结束。若无对象返回,Runtime会把消息有关的全部细节都封装到NSInvocation对象中,再给receiver最后一次机会,令其设法解决当前还未处理的这条消息。消息转发的流程可以归纳到以下图表:

由图表可以看出,receiver在每一步中均有机会处理消息,步骤越往后,处理消息累计开销就越大。所以,最好能在第一步就处理完,这样的话,Runtime还可以把方法进行缓存,在一步到位的同时进一步降低首次查询这样的开销。需要注意的是在最后一个阶段,需要由两个接口一起完成,先要通过

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

接口返回格式化的方法对象,下一个接口

- (void)forwardInvocation:(NSInvocation *)anInvocation

中传入参数NSInvocation对象对前一个接口返回的方法对象是有依赖,前一个接口的NSMethodSignature对象返回nil,则消息转发流程即告结束。

最后利用消息转发机制,我们实现一个让NSString类支持NSArray实例方法的例子,这对于降低程序的Crash率很有帮助:

我们先实现一个方法替换的接口swizzle method,帮助我们在不需要继承的情况下,实现对父类方法的代码注入

通过swizzle方式(class_addMethod、class_replaceMethod、method_exchangeImplementations),在NSString类的resolveInstanceMethod:中,动态方法解析的方式注入3个NSArray的实例方法:

测试用例:

测试结果:

结尾,笔者用了很大的篇幅和代码片段尝试去解释Objective-C最基本的一些概念,包括面向对象、消息派发、内存管理等等,并且也讨论了这些概念在Rumtime上的实现,这当中还不包括属性、分类、类族、协议、ARC、语法糖等Objective-C中同样重要的feature,也没有深入阐述其中的一些编码细节(有关编码,搜索引擎总能获取不少令人满意的答案)。笔者更多地是希望在有限的篇幅中帮助读者快速理解Objective-C,理解它为什么是这样而不是那样,并且对于想进一步学习和使用Objective-C的开发者和工程师能有所帮助。

 循例原文地址:http://springox.w18.net/2015/09/03/objectivecruntime/

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 mp3连接不上电脑怎么办 深户离职后社保怎么办 依云喷雾过敏了怎么办 阿里云邮箱满了怎么办 苹果手机ic坏了怎么办 王者队友不给力怎么办 协同大作战出bug怎么办 耳朵一个大一个小怎么办 58同城兼职被骗怎么办 我欠了好多钱怎么办 p2p平台跑路了怎么办 别人借钱跑路了怎么办 善心汇崩盘了钱怎么办 日本销签忘记了怎么办 手腕的筋拉伤了怎么办 膝盖韧带拉伤怎么办恢复快 手写发票写错了怎么办 发票电话写错了怎么办 京东不可7天退货怎么办 代销有人下单了怎么办 茜子饰品坏了怎么办 唇釉有点干了怎么办 如果微商被骗了怎么办 微信照片过期了怎么办 异地恋没话说了怎么办 请事假公司不批怎么办 请病假领导不批怎么办 农民被当官的整怎么办 领英人脉圈以外怎么办 收到领英的短信怎么办 狗半夜叫个不停怎么办 如果被鬼上身了怎么办 支付宝借钱不还怎么办 鞋子里鞋垫老跑怎么办 鞋垫在鞋里老串怎么办 网贷已经借不到怎么办 骨龄比实际年龄大怎么办 小孩崴脚了肿了怎么办 报到证过期2年多怎么办 报到证过了期限怎么办 报到证超过两年怎么办