在开发 iOS 和 Mac OS X 应用时,你并非是全部由自己从头做起。你的工作是基于苹果公司创建和收集的 Objective-C 框架基础之上的。框架就是一个类库,在运行时可以被多个进程共用;框架里还包括支持软件开发用的资源文件。Cocoa Touch 和 Cocoa 框架为你带来了一套相互支撑的类,它们共同构成你应用的一部分,通常是最重要的那一部分。
利用 C 语言的函数库,你可以根据所编写的应用种类随意选择所需函数并调用它们。而框架则不同,它是将你的程序限定在一个设计范围之内,至少是限定在根据你的应用的作用而定的一个范围内。当使用面向对象框架时,程序的大部分工作都可以通过调用框架里面类的方法来完成,这和面向过程编程类似。但是你仍然需要对一般框架行为进行自定义,通过实现若干方法让它们能够在适当的时间被调用。这些方法就是将你的代码与框架事先设定的结构相连接的钩子,为其添加更加适合你的应用的独特行为方式。
下边的内容将带你探寻框架代码和应用代码之间的关系。
应用的驱动力是「事件」
要探寻在你的代码和框架代码之间的关系,首先可以思考一下当应用启动时究竟发生了什么。最基本的事情是,应用建立好一组核心对象,然后把控制权交给那些对象。随着程序的运行会产生越来越多的对象,但是在最初阶段最需要的东西是完整的结构,要有足够数量的核心代码网络来处理初始任务。最基本的任务一共由两种:
- 绘制应用的初始用户界面。
- 当用户和用户界面产生交互时,处理接收到的事件。
在初始用户界面显示在屏幕上之后,应用就由内部的事件所驱动。其中最重要的事件是来自用户的操作,比如点按按钮。系统会向应用报告这样的事件,同时伴有它们各自的信息。同时含有你编写的代码以及框架代码的应用就会处理这些事件,并根据需要更新用户界面。
应用获取事件并做出响应,一般的响应是绘制用户界面,然后等待下一个事件。只要用户或者其他什么事件来源(比如计时器)在发出各种事件,应有就会不停地、一个接一个地收到这些事件。在应用运行到退出期间,几乎所有行动都来自于用户发出的事件。
获取事件并做出响应的这种机制叫做主事件循环。核心组里的一个对象,即全局应用对象负责管理主事件循环。它会获取事件,将事件派送到相应对象或者最适合处理这种事件的对象,然后去获取下一个事件。下图反映了 iOS 系统中 Cocoa Touch 应用的主事件循环过程。
使用 Objective-C 框架
Cocoa Touch 和 Cocoa 框架只不过是提供各类服务的各个类的收纳袋。它们构成了面向对象的框架,类的集合构建了一个问题空间,并为其提供了一个整合的解决方案。框架并非提供各种分散的服务,在你需要时去利用(比如函数库就是这样),框架勾勒出并实现了整个应用架构,你自己的代码必须适应这个架构。由于应用架构是个大的一般类,因此你可以按照需要来定制自己的每一个应用。在设计应用时,你并非向应用插入一个函数库,而是向框架提供设计的一般类插入你的代码。
要使用框架,则必须接受其定义的应用架构,并按照自己的需要使用和自定义框架里的类,并将自己的模子应用到这个架构上去。框架中的每个类都互相依赖,成组出现,而不是形单影只的分散体。起初,要让自己的代码适应框架的架构看上去很受限制,但事实却正好相反。框架能够让你用无数种方法变换和扩展其设定的一般类行为。你唯一要做的就是接受的条件就是所有应用行为都要在同一个基础之上,因为它们都基于同一个架构。举一个较为宽泛的例子,Objective-C 框架就好比一座房屋的骨架,你的应用代码就是门、窗、墙壁以及一切使这栋房子变得独特的元素。
从使用和整合框架到自己代码的角度来看,总共有两种类:
- 复活对象。有些类定义了复活(Off-the-shelf)对象,也就是可以直接拿来使用的对象。你只需要创建该类的实例,并按照自己的需要使用该实例即可。
- 一般对象。使用一般框架中的类时,你可以创建它们的子类并重写部分所需方法的实现(有时甚至必须创建子类并重写)。通过创建它们的子类,你就可以将自己的代码引入应用的架构中了。框架会在合适的时机调用你创建的子类中的方法。
为一般类框架创建子类是将自己编写的程序代码整合到框架提供的架构中去的一项主要技术,但并不是唯一的方式。在后边的文章中你会学到,Cocoa Touch 和 Cocoa 框架还含有一些结构和机制,可以让你自定的对象和框架对象更好地协同工作。
当你建立子类时,一般需要事先决定好两件事:从哪个类继承(即父类),以及要重写该类的哪些方法。下文将着重探索如何做这两个决定。
从 Cocoa 或 Cocoa Touch 框架继承
像 UIKit 这样的框架定义了一种可供多种应用利用的架构,因为它是个一般类。正因为此,你在看到某些框架的类十分抽象并且有意保持不完整时也不必惊讶。这样的类通常实现了若干段通用代码,但是会留下明显未完成或没有写成安全默认样式的部分代码。
要为应用添加特定的行为,最主要的做法就是给框架类创建自定义子类。子类为父类提供其缺少的内容,填补了父类在应用中存在的一些沟壑。你创建的自定义子类的实例会出现在框架定义的对象网络中,并且从框架中继承与其他对象协同工作的能力。要让一个应用变得有用处,它至少要包含一个子类,或许更多。
接下来的部分将讨论和探索关于创建、使用子类的一些决策和谋略,和它们的基本需求。不过我们不会谈到创建子类的具体细节。《The Objective-C Programming Language》中的「定义一个类」章节讲述了这些技术。
何时需要创建子类
创建和使用子类其实是对现有类的重复利用,并按照自己的需求定制它的过程。有时子类的全部职责就是重写从父类中继承而来的某个方法,将它进行轻微的改动。其他子类则可能向父类添加一两个属性(比如实例变量),然后让定义的方法对这些属性进行操作,将它们整合到父类的行为当中去。
创建和使用子类时,首先要做的是确定父类框架。你在考虑时可以参考下边的向导:
- 了解父类框架。你必须熟知框架中的每个类都带有什么目的,以及能做哪些事情。阅读开发者资源库里的框架介绍和框架本身各个类的代码就是个很好的开始。也许某个类早已实现了你想要做到的事情。如果你发现某个类基本能够做到你需要的事情,也就意味着你很幸运,这个类就可以拿来当作自定义子类的父类。比如,当学习《你的第一个 iOS 应用》时就遇到过 UIViewController 等 UIKit 框架的类。要更深入地了解这些类,你可以这样做:
- 在 Xcode 中,选择 Window > Organizer。
- 点按工具栏里的 Documentation 按钮。
- 点按导航区顶部的 Browse,开始浏览已经安装的开发者资源库。
- 点按最近的 iOS 资源库(你可能需要首先登录到 Apple Developer 才可以)。iOS Developer Library 将在内容区域里显示出来。
- 在文档列表上方的过滤选项里,输入“UIKit Framework Reference”。筛选后的列表就只会显示符合输入内容的文档了。
- 点按一个文档的名称,其页面就会显示出来,包含 UIKit 的所有类、协议列表。
- 仔细阅读介绍,并点按列出的类和协议,这样就能了解更多关于它们的知识。
- 对自己的应用将要做什么非常明确。这条建议既可以说是针对整个应用的,也可以说是针对应用中每个独立功能的。某些框架的架构中强制规定了自己的子类需要符合哪些要求。比如,如果你的应用是基于文档的,你就必须为抽象文档类创建子类。
- 定义你的子类产生的实例将要扮演的角色。在 iOS 和 Mac OS X 应用开发中,模型-视图-控制器(MVC)设计模式就是对象的三大角色。视图对象会显示在用户界面上;模型对象则掌握用户数据(并实现一定的数据处理算法);控制器对象则是连接视图和模型对象的中间人。搞清楚各个对象的角色之后,决定使用哪个子类就会非常容易了。比如,当你需要在 iOS 应用中进行一些自定义绘制时,你就需要一个 UIView 的子类,UIView 是 UIKit 框架中的基本视图类。
虽然子类操作在 iOS 和 Mac OS X 编程中非常重要,但它并不永远都是解决问题的最好方法。如果你需要向某个类添加少量的便捷方法,你可以创建范畴类(Category), 而不是创建它的子类。或者,当你需要拦截应用的某个特殊行为时,你可以在设计模式的基础上,选用框架中其他大量资源里的某一个,比如委托(这些设计模式在《用设计模式让应用开发流水线化》一文中有详细描述)。时刻牢记,有些框架的类是不可以用来创建子类的。参考文档中会告诉你各个类是否能够创建子类。
重写一个方法
你可以创建一个子类但不实现任何父类的方法;比如说某个子类声明了一些额外属性并定义了访问这些属性的新方法,并在父类中调用这些方法。然而,有些子类的首要任务是实现一系列已由父类声明的方法(或父类采用的某个协议里)。重新实现一个继承而来的方法就叫做重写该方法。
框架的类里定义的大部分方法都已被全面实现;你可以直接调用它们,以获得该类能够提供的所有便利。对于这些方法,你不需要也不应该尝试重写它们。其他的框架方法就可以被重写了,但也并非时刻都有必要重写。
但是,有些框架方法在使用之前要求先进行重写;它们存在的目的就是方便你向框架添加特定的行为。这些由框架实现的方法常常只做很少的事情或者什么有用的功能也没有。为了给这些方法增添内容,每个应用都需要对它们进行特性的实现。框架会在应用运行时生命期内的合适时机调用这些方法。
调用还是重写?
你在子类里重写的框架方法通常不是由你亲自调用的,至少不是由你直接调用。你只是重新实现该方法,并让框架处理剩下的所有事情。实际上,基本上你重写的特定版本的方法很少由你自己在代码中进行调用。通常来说,对于框架类声明的公共方法,开发者们可以做下边这两件事:
- 调用它们,让类提供的功能发挥作用
- 重写它们,将你自己的代码引入框架定义的程序模型中
有时某个方法会同时适用于这两种情况;如果主动调用,它会是一个非常有用的功能,同时也可以策略性地重写它。但是在大部分情况下,如果你能够调用某个方法,那么它就是由框架完全定义好的,无需在你的代码中重复定义了。如果你必须在子类中重新实现某个方法,那么这个方法在框架中就是有着特定职责的,会在合适的时机由框架来调用。下图描绘了这两种框架方法。
在这张图片里,假定你的自定义类里有个方法 myMethod,它调用了 setNeedsDisplay 方法,后者是由框架实现的。框架首先准备好了绘制环境,然后调用框架声明的 drawRect: 方法,这是已经由自定义类重写后的方法,能够实际绘制图形。
重写方法并不是难事。只要在重新实现方法时格外小心,仅用一两行代码往往就能明显改变父类定义的方法行为。
调用父类的实现
当你重写某个框架方法时,你需要决定是否替换掉继承到的方法行为,还是选择扩展或补充该行为。如果想要替换已有的行为,只需在方法的实现中输入你自己的代码;如果是想扩展该行为,你需要调用父类的实现然后再提供自己的代码。
如何调用父类的实现呢?只需向 super 发送一条同样的消息调用该方法即可。向 super 发送消息时,你就在调用的那个点上将父类的代码“插入”了你重新实现的代码。打个比方,假设有一个叫做 Celebrate 的类定义了一个方法performFireworks。当框架在视图中绘制并动画化一个烟火时,你想在视图中显示一个横幅。下边的图片显示了此例中调用 super 的作用。
因此,决定调用 super 是基于你如何重新实现方法的:
- 如果你想要补充父类实现中的行为,则要调用 super。
- 如果你想要替换父类实现里的行为,那么就不要调用 super。
如果你是补充和扩展父类的行为,还需要注意另外一件重要的事,那就是何时调用父类方法的实现。因为,你可能需要在自己的代码执行之前让父类的代码产生作用,反之亦然。