当前位置: 首页 > 科技观察

Objective-C和运行时:为什么会这样?

时间:2023-03-12 07:01:07 科技观察

作者很高兴能写出自己对Objective-C的理解和总结,不仅因为作者是Objective-C多年的重度开发者,更因为这是一个独特的想法,创造性的,一个编程具有历史地位和优美句法的语言。如果对这篇文章有什么期待的话,作者希望澄清一些诸如“为什么会这样”的问题。Objective-C是在80年代发明的。Objective-C的作者BradCox和TomLove在接触到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年,苹果收购了NeXT,NeXTSTEP/OPENSTEP系统成为苹果新一代操作系统OSX开发的基础。2005年,苹果引进了ChrisLattner和他的LLVM技术团队。Objective-C的新特性和编译优化首次得到高级编译器最高优先级的支持,从后端代码优化和生成开始。逐渐扩展到前端语法分析(Clang)。今天(2015年),Objective-C已经有了比GCC-LLVM编译器更合适、更好的编译包选项,LLVM包括完整的前后端模块,最新版本6.1(2015)。Objective-C是面向对象的,这是Objective-C最基本的概念。对于面向对象,某些算法(函数)和数据(变量)由于某种内部联系而绑定在一起,构成最基本的程序结构单元。Word,我们称之为抽象对象,术语称为类;通过对变量的赋值(作者认为不仅是变量,闭包等逻辑操作也可以用于赋值)会形成一个实体对象,术语简称为对象(Objective-C中一般也称为个实例)。物与物并不是完全独立的。通过巧妙的方式,他们可以建立紧密的联系,如事物的继承、派生、抽象和代码的重用,这些联系具有微妙而重要的价值。BradCox和TomLov的第一本官方Objective-C书名为《Object-Oriented Programming, An Evolutionary Approach》。那么,为什么是对象,为什么是面向对象?这是一个很好的问题。观察人类的普遍思维,我们知道这个世界上使用最多的概念是对象。我们擅长将我们感知到的一切抽象为单个对象。通过了解物体的组成和物体之间的关系,实现认知这个世界的目的和功能。这一直很完美!面向对象就是把人类思维的天赋和积累的思想财富应用到程序设计中,使程序提升生产能力/改善生活质量的效率和能力大大提高。/*上图是FoundationKit支持的集合对象——(不可变)数组,继承自根类NSObject,支持包括NSCopying在内的一系列协议(接口),count表示一个只读变量,-(id)objectAtIndex:(NSUInteger)index等表示数组支持的可用方法(函数)*/Messagedispatch是调用Objective-C函数的方式(Objective-C其实就是调用方法),上面说了,这个概念是继承自短暂聊天。Objective-C的对象之间相互调用函数,相当于向目标对象发送消息。消息的发送者称为发送者,消息的接收者称为接收者,消息中间传递的字符串称为选择器(selector)。/*上图中的代码表明至少有两个明显的接收者,self.view是其中一个消息接收者,传递的消息(string/selector)是“setBackgroundColor:”,UIColor代表一个类,而class也可以作为消息的接收者,字符串/选择器为"yellowColor"*/对消息的处理是先判断实际的执行方式,然后跳转到它执行。我们理解这是对消息的回应。在编译过程中,仅仅一句“dispatchmessage”的语法是无法确定实际执行的结果的。只有在程序执行过程中才能确定实际的执行结果。这种在运行时确定实际执行的方法在Objective-C中称为动态绑定。消息分发的工作机制明显不同于另一种著名的面向对象编程语言——C++。C++调用对象的函数,编译时必须严格确定函数与对象的关系。如果car中没有定义名为fly的函数,编译器不会通过,而是会报错。如果Objective-C发送一个字符串为“fly”的选择器给car,即使car没有实现fly方法,编译器仍然可以通过,但是运行时会抛出异常,因为实际执行的方法无法获得。也就是说,消息派发的设计使得Objective-C在编译时对对象所属的类具有很强的包容性。上面说了,同一个对象有相同的定义,称为类,而类本身也可以看作是一个对象——一个“类”对象,可以为一个“类”对象定义一个“类”,比如比较操作、哈希、描述、类名等等,总之一切都是对象。在C++中,我们可以根据我们所说的模板来实现“类”的定制。Objective-C通过一个统一的基类如NSObject(不仅是NSObject,还有各种根协议)为所有的类添加定义。.您可以向任何对象发送任何消息,包括空指针nil。消息分发的机制使得在运行时无需重新编译就可以更容易地实现和更实用地干预或挂钩原始目标(方法、变量等)。这是一个依赖于消息派发和动态绑定的实现机制——Runtime,但是Runtime不仅仅是消息派发和动态绑定的工作,它还是Objective-C面向对象、内存模型等特性的实现者。在正式介绍Runtime之前,我们继续介绍Objective-C的另一个重要概念。作者想说的是Objective-C的内存管理模型。程序运行时,创建一个对象总是会占用内存,而内存的总大小总是有限的,所以当一个对象不再需要时,应该及时回收它占用的内存资源,用于新的对象。Objective-C的内存管理原理简单来说就是“引用计数”机制。如果一个模块需要引用一个对象,则用于对象统计的引用计数值将加1,并记录在该对象的结构信息中。当模块不再需要该对象时,它会减1。当计数值为0时,可以认为该对象不再需要,及时销毁以释放内存(回收资源).Objective-C对象的内存空间只分配在“堆空间”(heapspace)上,肯定不会分配在“栈”(stack)上。我们知道,“栈”的占用和回收有着严格的数据操作规则,简称“先进后出”。当函数执行时,传入的变量(当然也包括对象变量)会按照确定的顺序规则自动压入“栈”(占用内存资源)。当函数执行结束时,这些变量会自动弹出(释放)内存资源)。因此,我们可以看出,“栈”实际上是无法实现“引用计数”机制的,Objective-C否定了使用“栈”来存储对象的想法。在语法上,Objective-C不能像C++那样直接声明和创建对象变量,更不能直接操作对象。Objective-C需要创建一个类似于C语言语法(alloc)的对象来申请一块堆内存块。变量,对象指针必须作为访问句柄,这和C语言申请堆内存块很相似。Objective-C的“任性”设计还使得对象嵌套(一个对象作为另一个对象的成员变量),对象是基于引用计数机制的,它的成员变量也必须递归遵循引用计数机制。因为成员变量实际上是对象指针,它们很可能与其他对象共享同一个对象(指针都指向同一块内存),引用计数机制适合支持对这种“共享”内存的管理。特别的,如果你能像C++一样创建一个对象变量作为成员变量,那么这个成员变量会被存储在对象所在的一个连续的内存块中,当成员变量占用的内存块全部被释放时对象被销毁Withdrawal,这个不太符合引用计数的机制,所以进一步理解Objective-C是不支持对象变量的。/*上图中的接口(方法)是Objective-C中内存管理相关的接口(方法)*/Runtime(component)翻译一般称为runtimecomponent,一个用纯C语言编写的基础库(lib),目标-用C编写的程序必须由运行时运行才能正常工作。在Java、PHP、Flash等编程语言中,大家对Runtime都不会太陌生,而Objective-C的Runtime其实就是一回事。运行时实现了Objective-C的许多特性。Objective-C的面向对象、消息派发、动态绑定和内存管理都与Runtime息息相关。那么,在Objective-C中,对象、类和函数(方法)是如何构造和运行的呢?上面说了,面向对象中的类被看作是抽象对象,Runtime也秉承了这个概念。Runtime用纯C语言编写,使用struct结构来描述对象(实体对象)和类(抽象对象)。对象的结构相对简单。*id作为结构体objc_object的指针别名,***struct的成员isa是Class类型的指针变量。正是这个变量决定了对象所属的类。Class类型也是struct,是objc_class结构体的指针别名,用于描述类组成的struct,***成员isa也是Class类型的指针变量(***这两个结构体的成员都是Class类型的指针变量设计让我们进一步理解在Runtime中,类确实和对象一样被对待),一个类的isa会指向一个叫做元类(metaclass)的结构体,metaclass抽象了一个类的特性,元类的第一个成员自然也是isa的Class类型的指针变量。不同的是元类的isa最终指向的是自身。由此可见,类struct是一种递归嵌套设计,体现了面向对象***抽象概念,最终指向自身的实现是实际工程处理的需要。一般我们也会认为objc_classstruct中存放的是类的元数据(metadata),比如类的实例方法、类的实例变量、类的超类指针等。Runtime还允许我们通过标准接口(C函数)查询和动态扩展所有Objective-C变量、方法、属性、协议等,从而达到我们丰富项目中语言和类库特性的目的。/*通过上图中标准的RuntimeAPI(C函数)打印UIKit中UIView的所有变量、属性和方法*/Runtime的另一个重要特性是消息派发,而objc_msgSend是消息派发的核心和基础入口函数,另外还有objc_msgsend_stret、objc_msgSend_fpret、objc_msgSendSuper等函数,但它们的重要性和作用远不及objc_msgSend。objc_msgSend函数会根据接收者和选择器调用合适的方法。为了完成这个操作,函数需要在recevier所属的类中搜索它的“方法列表”,如果能找到匹配选择器字符串名称的方法,就会跳转到这个方法。如果找不到,那就沿着继承体系继续往上找,找到合适的方法后再跳转。如果最后还是找不到匹配的方式,那就进行“消息转发”操作。由此可见,调用一个方法似乎需要不少步骤。每一步都是开销,会不会导致Objective-C的性能问题?幸运的是,obj_msgSend会将匹配结果缓存在“快图”中。每个类都有这样一个缓存。如果以后需要向这个类发送相同的选择器消息,执行会快很多。当然,这种“快速路径”不如“静态绑定函数调用”快,但通过汇编等优化技术,映射表的查询开销已经很小了。可以说,即使比起C++的静态绑定,Objective-C的消息派发机制也不再是性能瓶颈。如果说上面的消息分发机制就是Objective-C动态绑定的全部内容,那还不完整。当对象无法查询到相关方法,无法正确响应消息时,“消息转发”机制也会被激活。是的,除了支持“动态增减”的方法列表外,我们还可以提供其他正常的响应方法。消息转发也分为几个阶段。首先,询问接收者或者它所属的类,看看它是否可以动态添加方法来处理当前的“未知选择器”。这称为“动态”方法解析(dynamicmethodresolution),Runtime会通过回调一个类的方法来寻求对动态添加方法的支持。如果Runtime完成动态添加方法的查询后,接收者仍然不能正常响应,Runtime会继续询问接收者是否有其他对象可以处理该消息,如果返回一个可以处理的对象,Runtime会将消息转发给返回的对象,消息转发过程结束。如果没有对象返回后,Runtime会将与消息相关的所有细节封装到NSInvocation对象中,给接收者再一次尝试解析尚未处理的消息的机会,消息转发的过程可以总结为下图:从图中可以看出,接收方在每一步都有机会处理消息,而且步数越远,累计overh越大负责处理消息。所以***可以在第一步处理。这种情况下,Runtime也可以缓存方法,在一步完成的同时,进一步降低***查询的开销。需要注意的是,在最后一个阶段,需要通过两个接口来完成。首先必须通过-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector接口返回格式化的方法对象,接下来的接口-(void)forwardInvocation:(NSInvocation*)anInvocation传入的参数NSInvocation对象依赖于该方法目的。第一个NSMethodSignature对象返回nil,消息转发过程结束。***利用消息转发机制,我们实现了一个让NSString类支持NSArray实例方法的例子,对于降低程序的崩溃率很有帮助:我们先实现一个接口swizzle方法进行方法替换,这有助于我们使用这种情况,通过swizzle方法(class_addMethod,class_replaceMethod,method_exchangeImplementations)实现父类方法的代码注入,在NSString类的resolveInstanceMethod:中,以动态方法解析的方式注入NSArray的3个实例方法:testcase:testresult:end,作者用了大量的篇幅和代码片段,试图解释Objective-C的一些最基本的概念,包括面向对象,消息派发,内存管理等等,还有讨论了这些概念在Rumtime上的实现,其中还包括不包括Objective-C中同样重要的特性,例如属性、分类、classes,andprotocols,并没有详细说明一些编码细节(关于编码,通过搜索引擎总能得到很多满意的答案)。作者希望能帮助读者在有限的篇幅内快速理解Objective-C,理解为什么是这样而不是那样,对想进一步学习和使用Objective-C的开发者和工程师有所帮助。参考链接(部分):WikiforObjective-CObjective-CRuntimeProgrammingGuide