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

iOS开发:Objective-C运行时详解

时间:2023-03-12 08:40:54 科技观察

这篇文章是我在AltTechTalks:London上关于Objective-C运行时的演讲总结。如果你对Objective-C运行时感兴趣,你应该阅读这篇文章,尤其是文章中的链接,一定会受益匪浅。什么是Objective-C运行时?简单地说,Objective-C运行时是一个实现了Objective-C语言的C库。对象可以用C语言的结构来表示,方法可以用C函数来实现。事实上,他们几乎做到了这一点,并增加了一些额外的功能。这些结构和函数被运行时函数封装后,Objective-C程序员可以在程序运行时创建、检查和修改类、对象及其方法。除了封装之外,Objective-C运行时库还负责计算出方法的最终执行代码。当程序执行[objectdoSomething]时,不会找到方法直接调用。而是向对象(这里,我们通常称其为接收者)发送消息(message)。运行时库让对象有机会根据消息决定做什么。AlanKay反复强调消息传递是Smalltalk(Objective-C是从Smalltalk发展而来)最重要的部分,而不是对象:因为我之前在这个话题上创造了“对象”这个词,所以现在很多人都这么急于这个概念让我非常后悔。其实这里更重要的概念是“消息传递”,它是Smalltalk的核心内容(部分内容尚未完全完成)。在日语中有一个叫做“ma”的短词,用来表示两个物体之间的东西,而它在英语中最接近的对应词可能是“interstitial”。构建大型可扩展系统的关键是设计其各个模块如何相互通信,而不是关注其内部属性和行为。事实上,在一篇介绍Smalltalk虚拟机的文章中,这种编程技术被称为消息传递或消息传递范式。“面向对象”常被用来描述内存管理系统。ObjC运行时这个词在演讲和文章中都有使用。看似只有一个,其实运行时库有很多。虽然它们都支持对象自省检查和消息接收,但是它们有不同的特点和实现方式(例如Apple的runtime在发送消息时一步完成,而GNUruntime会先查询这些消息,然后执行搜索查找函数分两步)。以下所有讨论均基于Apple最新的运行时库(OSX10.5和iOS发布时的Apple版本)。在那次谈话中,我决定研究运行时库的某些功能领域。找了一些我想更透彻理解的东西,然后把它们整理成问答的形式来补上我的演讲。如何动态创建类实现Key-ValueObserving?当我准备这篇演讲时,一篇名为KVO的文章被认为是有害的,开始有很多粉丝。它对KVO提出了很多正确的批评,但与其放弃观察者模式,不如探索一种新的实现方式。KVO实现观察者模式的关键在于它偷偷改变了被观察对象的类。它对原类进行子类化后,可以自定义对象的方法来调用KVO的回调方法。这些都是通过objc_duplicateClass方法来完成的,但是很遗憾,这个方法是不公开的,我们不能私下调用。条条大路通罗马,但幸运的是,除了objc_duplicateClass之外,还有其他方法可以通过使用秘密子类来实现观察者模式,例如创建和注册“类对”。那么什么是类对呢?对于一个Objective-C的类,有一对Class对象来定义它:Class对象定义了这个类的实例方法,元类定义了这个类的类方法。所以每个类实际上是它的元类的一个单例。这段代码展示了观察者模式是如何工作的。当你给一个对象添加观察者时,这个对象会先检查它是否可以被观察,如果可以,它会创建一个新的类,用我们自己的-dealloc替换原来类的方法,它也会使用-classmethodReplaced,类似于KVO被观察对象,当你访问被观察对象的类名时,返回的是它原来的类名,而不是新生成的类。创建完类后,我们需要按照Key-ValueCoding的方式给属性添加一个setter方法:这个setter方法会获取属性修改前的值和修改后的值,然后调用回调函数,形式为块设置两个值告诉观察者。根据我们在代码中的意愿,这个块可以被异步调用。请注意-addObserverForKey:withBlock:将使用sobject_setClass()将观察对象的类替换为新形成的类。这样做的主要目的是改变消息转换为方法的方式,但是这需要非常小心,原类和新类必须有相同的成员变量布局。因为成员变量也是运行时访问的,修改对象的类可能会导致运行时找不到对应的变量。我们在存储观察者集合时遇到了麻烦,因为没有地方可以存储它们。给ObserverPattern类添加成员变量没有任何作用,因为根本没有生成这个类的对象。被观察对象的成员变量是它的原始类,它不考虑这些观察者。Objective-C运行时通过引入关联对象帮助我们摆脱了这种困境。在运行时,理论上所有对象都可以有包含其他对象的字典。通过关联引用,被观察对象可以存储和访问它们的观察者,而不需要额外的成员变量。如果你多次运行它,你会发现ObserverPattern还是有点bug。由于观察者回调是异步调用的,因此观察者收到的更改事件也是乱序的。这意味着观察者实际上无法判断被观察属性的最终状态是什么,并且回调中的新值可能已经被修改。我这样做的目的是为了表明在KVO中同步调用回调实际上是一个有用的特性,而不是一个错误。那些用于创建对象的额外字节是什么?当您创建一个Objective-C对象时,运行时会在实例变量存储区之后分配一点额外空间。这样做的目的是什么?可以得到这个空间的起始指针(使用object_getIndexedIvars),然后可以索引实例变量(ivars)。好吧,下面我就用一个自定义数组来说明索引ivars的用处。让我们创建一个数组!从这个SimpleArray可以看出两点:最明显的一点是它使用了类簇模式。当使用+alloc方法返回一个对象时,一般来说所有的内存都已经为这个对象分配好了,但是在这个例子中,并不知道在+alloc的时候需要多少内存空间。只有在调用-initWithObjects:count:时,数组需要的内存才能根据数组中对象的个数来计算,所以+alloc只是返回一个占位符,真正的数组对象只有在初始化之后才会分配和返回.也许你会问为什么我们要用类簇把事情搞得这么复杂,用calloc()再分配一个合适大小的缓冲区,然后把那些对象指针存进去?答案是利用局部性原理来提高访问性能。从数组的设计我们可以看出,每次访问数组指针的时候,后面都会有很高的几率访问到缓存指针,所以把它们并排放在内存中意味着找到一个就意味着找到另一个.消息分发如何转发消息?Objective-C的强大功能之一是对象不需要实现方法,即使它在编译时声明了选择器。但是它可以在运行时决定方法的实现,或者将这些消息转发给其他对象,或者发出异常,或者做一些其他的事情。但是这个特性的某些方面一直困扰着我:消息转发调用-forwardInvocation:并传入一个NSInvocation对象。但是这个NSInvocation类是在Foundation库中定义的。是不是说runtime需要Foundation的配合?我试着去深挖原因,发现答案不是我想的那样,runtime不需要知道Foundation。runtime会让程序定义转发函数(forwardingfunction),当objc_msgSend()找不到选择器的实现时,就会调用那个转发函数。程序一启动,CoreFoundation就将-forwardInvocation:定义为转发函数。让我们创建一个Ruby!当然,它并不是真正完整的Ruby实现。Ruby有一个名为#method_missing的函数。当对象收到一个它没有实现的消息时,这个函数就会被调用。这类似于Smalltalk方法。使用objc_setForwardHandler,我们还可以在Objective-C类中实现Ruby的methodMissing:方法。总结Objective-C运行时可以有效地帮助我们给程序添加很多动态行为。除了使用methodswizzling来帮助调试程序外,有些开发者在实际程序中不会使用它,但运行时编程确实有很多作用,应该会成为实际应用代码编写的重要工具。原文地址:http://blog.securemacprogramming.com/2013/12/by-your-_cmd/