iphone应用程序编程指南

http://www.apple.com.cn/developer/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Introduction/Introduction.html

核心应用程序

所有的iPhone应用程序都是基于UIKit框架构建而成的,因此,它们在本质上具有相同的核心架构。UIKit负责提供运行应用程序和协调用户输入及屏幕显示所需要的关键对象。应用程序之间不同的地方在于如何配置缺省对象,以及如何通过定制对象来添加用户界面和行为。

虽然应用程序的界面和基本行为的定制发生在定制代码的内部,但是,还有很多定制需要在应用程序的最高级别上进行。这些高级的定制会影响应用程序和系统、以及和设备上的其它程序之间的交互方式,因此,理解何时需要定制、何时缺省行为就已经足够是很重要的。本章将概要介绍核心应用程序架构和高级别的定制点,帮助您确定什么时候应该定制,什么时候应该使用缺省的行为。

核心应用程序架构

从应用程序启动到退出的过程中,UIKit框架负责管理大部分关键的基础设施。iPhone应用程序不断地从系统接收事件,而且必须响应那些事件。接收事件是UIApplication对象的工作,但是,响应事件则需要您的定制代码来处理。为了理解事件响应需要在哪里进行,我们有必要对iPhone应用程序的整个生命周期和事件周期有一些理解。本文的下面部分将描述这些周期,同时还对iPhone应用程序开发过程中使用的一些关键设计模式进行总结。

应用程序的生命周期

应用程序的生命周期是由发生在程序启动到终止期间的一序列事件构成的。在iPhone OS中,用户可以通过轻点Home屏幕上的图标来启动应用程序。在轻点图标之后的不久,系统就会显示一个过渡图形,然后调用相应的main函数来启动应用程序。从这个点之后,大量的初始化工作就会交给UIKit,由它装载应用程序的用户界面和准备事件循环。在事件循环过程中,UIKit会将事件分发给您的定制对象及响应应用程序发出的命令。当用户进行退出应用程序的操作时,UIKit会通知应用程序,并开始应用程序的终止过程。

图1-1显示了一个简化了的iPhone应用程序生命周期。这个框图展示了发生在应用程序启动到退出过程中的事件序列。在应用程序初始化和终止的时候,UIKit会向应用程序委托对象发送特定的消息,使其知道正在发生的事件。在事件循环中,UIKit将事件派发给应用程序的定制事件处理器。有关初始化和终止事件的如何处理的信息,将在随后的“初始化和终止”部分进行讨论;事件处理的过程则在“事件处理周期”部分介绍,在后面的章节也还有更为详细的讨论。

图1-1  应用程序的生命周期

Application life cycle
主函数

在iPhone的应用程序中,main函数仅在最小程度上被使用,应用程序运行所需的大多数实际工作由UIApplicationMain函数来处理。因此,当您在Xcode中开始一个新的应用程序工程时,每个工程模板都会提供一个main函数的标准实现,该实现和“处理关键的应用程序任务”部分提供的实现是一样的。main例程只做三件事:创建一个自动释放池,调用UIApplicationMain函数,以及使用自动释放池。除了少数的例外,您永远不应该改变这个函数的实现。

程序清单1-1  iPhone应用程序的main函数

#import <UIKit/UIKit.h>
 
int main(int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;
}

请注意:自动释放池用于内存管理,它是Cocoa的一种机制,用于延缓释放具有一定功能的代码块中创建的对象。有关自动释放池的更多信息,请参见Cocoa内存管理编程指南;如果需要了解与自动释放池有关的具体内存管理规则,则请参见“恰当地分配内存”部分。

程序清单的核心代码是UIApplicationMain函数,它接收四个参数,并将它们用于初始化应用程序。传递给该函数的缺省值并不需要修改,但是它们对于应用程序启动的作用还是值得解释一下。除了传给main函数的argcargv之外,该函数还需要两个字符串参数,用于标识应用程序的首要类(即应用程序对象所属的类)和应用程序委托类。如果首要类字符串的值为nil, UIKit就缺省使用UIApplication类;如果应用程序委托类为nil,UIKit就会将应用程序主nib文件(针对通过Xcode模板创建的应用程序)中的某个对象假定为应用程序的委托对象。如果您将这些参数设置为非nil值,则在应用程序启动时,UIApplicationMain函数会创建一个与传入值相对应的类实例,并将它用于既定的目的。因此,如果您的应用程序使用了UIApplication类的定制子类(这种做法是不推荐的,但确实是可能的),就需要在第三个参数指定该定制类的类名。

应用程序的委托

监控应用程序的高级行为是应用程序委托对象的责任,而应用程序委托对象是您提供的定制类实例。委托是一种避免对复杂的UIKit对象(比如缺省的UIApplication对象)进行子类化的机制。在这种机制下,您可以不进行子类化和方法重载,而是将自己的定制代码放到委托对象中,从而避免对复杂对象进行修改。当您感兴趣的事件发生时,复杂对象会将消息发送给您定制的委托对象。您可以通过这种“挂钩”执行自己的定制代码,实现需要的行为。

重要提示:委托模式的目的是使您在创建应用程序的时候省时省力,因此是非常重要的设计模式。如果您需要概要了解iPhone应用程序中使用的重要设计模式,请参见“基本设计模式”部分;如果需要对委托和其它UIKit设计模式的详细描述,则请参见Cocoa基本原理指南部分。

应用程序的委托对象负责处理几个关键的系统消息。每个iPhone应用程序都必须有应用程序委托对象,它可以是您希望的任何类的实例,但需要遵循UIApplicationDelegate协议,该协议的方法定义了应用程序生命周期中的某些挂钩,您可以通过这些方法来实现定制的行为。虽然您不需要实现所有的方法,但是每个应用程序委托都应该实现“处理关键的应用程序任务”部分中描述的方法。

有关UIApplicationDelegate协议方法的更多信息请参见UIApplicationDelegate协议参考

主Nib文件

初始化的另一个任务是装载应用程序的主nib文件。如果应用程序的信息属性列表(Info.plist)文件中含有NSMainNibFile键,则作为初始化过程的一个部分,UIApplication对象会装载该键指定的nib文件。主nib文件是唯一一个自动装载的nib文件,其它的nib文件可以在稍后根据需要进行装载。

Nib文件是基于磁盘的资源文件,用于存储一或多个对象的快照。iPhone应用程序的主nib文件通常包含一个窗口对象和一个应用程序委托对象,还可能包含一个或多个管理窗口的其它重要对象。装载一个nib文件会使该文件中的对象被重新构造,从而将每个对象的磁盘表示转化为应用程序可以操作的内存对象。从nib文件中装载的对象和通过编程方式创建的对象之间没有区别。然而,对于用户界面而言,以图形的方式(使用Interface Builder程序)创建与用户界面相关联的对象并将它们存储在nib文件中通常比以编程的方式进行创建更加方便。

有关nib文件及其在iPhone应用程序中如何使用的更多信息,请参见“Nib文件”部分,有关如何为应用程序指定主nib文件的信息则请参见“信息属性列表”部分。

事件处理周期

在应用程序初始化之后,UIApplicationMain函数就会启动管理应用程序事件和描画周期的基础组件,如图1-2所示。在用户和设备进行交互的时候,iPhone OS会检测触摸事件,并将事件放入应用程序的事件队列。然后,UIApplication对象的事件处理设施会从队列的上部逐个取出事件,将它分发到最适合对其进行处理的对象。举例来说,在一个按键上发生的触摸事件会被分发到对应的按键对象。事件也可以被分发给控制器对象和应用程序中不直接负责处理触摸事件的其它对象。

图1-2  事件和描画周期

The event and drawing cycle

在iPhone OS的多点触摸事件模型中,触摸数据被封装在事件对象(UIEvent)中。为了跟踪触摸动作,事件对象中包含一些触摸对象(UITouch),每个触摸对象都对应于一个正在触摸屏幕的手指。当用户把手指放在屏幕上,然后四处移动,并最终离开屏幕的时候,系统通过对应的触摸对象报告每个手指的变化。

在启动一个应用程序时,系统会为该程序创建一个进程和一个单一的线程。这个初始线程成为应用程序的主线程,UIApplication对象正是在这个线程中建立主运行循环及配置应用程序的事件处理代码。图1-3显示了事件处理代码和主运行循环的关系。系统发送的触摸事件会在队列中等待,直到被应用程序的主运行循环处理

图1-3  在主运行循环中处理事件

Processing events in the main run loop

请注意:运行循环负责监视指定执行线程的输入源。当输入源有数据需要处理的时候,运行循环就唤醒相应的线程,并将控制权交给输入源的处理器代码。处理器在完成任务后将控制权交回运行循环,然后,运行循环就处理下一个事件。如果没有其它事件,运行循环会使线程进入休眠状态。您可以通过Foundation框架的NSRunLoop类来安装自己的输入源,包括端口和定时器。更多有关NSRunLoop和运行循环的一般性讨论,请参见线程编程指南

UIApplication对象用一个处理触摸事件的输入源来配置主运行循环,使触摸事件可以被派发到恰当的响应者对象。响应者对象是继承自UIResponder类的对象,它实现了一或多个事件方法,以处理触摸事件不同阶段发生的事件。应用程序的响应者对象包括UIApplicationUIWindowUIView、及所有UIView子类的实例。应用程序通常将事件派发给代表应用程序主窗口的UIWindow对象,然后由窗口对象将事件传送给它的第一响应者,通常是发生触摸事件的视图对象(UIView)。

除了定义事件处理方法之外,UIResponder类还定义了响应者链的编程结构。响应者链是为实现Cocoa协作事件处理而设计的机制,它由应用程序中一组链接在一起的响应者对象组成,通常以第一响应者作为链的开始。当发生某个事件时,如果第一响应者对象不能处理,就将它传递给响应者链中的下一个对象。消息继续在链中传递—从底层的响应者对象到诸如窗口、应用程序、和应用程序委托这样的高级响应者对象—直到事件被处理。如果事件最终没有被处理,就会被丢弃。

进行事件处理的响应者对象可能发起一系列程序动作,结果导致应用程序重画全部或部分用户界面(也可能导致其它结果,比如播放一个声音)。举例来说,一个控键对象(也就是一个UIControl的子类对象)在处理事件时向另一个对象(通常是控制器对象,负责管理当前活动的视图集合)发送动作消息。在处理这个动作消息时,控制器可能以某种方式改变用户界面或者视图的位置,而这又要求某些视图对自身进行重画。如果这种情况发生,则视图和图形基础组件会接管控制权,尽可能以最有效的方式处理必要的重画事件。

更多有关事件、响应者、和如何在定制对象中处理事件的信息,请参见“事件处理”部分;更多有关窗口及视图如何与事件处理机制相结合的信息,请参见“视图交互模型”部分;有关图形组件及视图如何被更新的更多信息,则请参见“视图描画周期”部分。

基本设计模式

UIKit框架的设计结合了很多在Mac OS X Cocoa应用程序中使用的设计模式。理解这些设计模式对于创建iPhone应用程序是很关键的,我们值得为此花上几分钟时间。下面部分将简要概述这些设计模式。

表1-1  iPhone应用程序使用的设计模式

设计模式

描述

模型-视图-控制器

模型-视图-控制器(MVC)模式将您的代码分割为几个独立的部分。模型部分定义应用程序的数据引擎,负责维护数据的完整性;视图部分定义应用程序的用户界面,对显示在用户界面上的数据出处则没有清楚的认识;控制器部分则充当模型和控制器的桥梁,帮助实现数据和显示的更新。

委托

委托模式可以对复杂对象进行修改而不需要子类化。与子类化不同的是,您可以照常使用复杂对象,而将对其行为进行修改的定制代码放在另一个对象中,这个对象就称为委托对象。复杂对象需要在预先定义好的时点上调用委托对象的方法,使其有机会运行定制代码。

目标-动作

控件通过目标-动作模式将用户的交互通知给您的应用程序。当用户以预先定义好的方式(比如轻点一个按键)进行交互时,控件就会将消息(动作)发送给您指定的对象(目标)。接收到动作消息后,目标对象就会以恰当的方式进行响应(比如在按动按键时更新应用程序的状态)。

委托内存模型

Objective-C使用引用计数模式来确定什么时候应该释放内存中的对象。当一个对象刚刚被创建时,它的引用计数是1。然后,其它对象可以通过该对象的retainrelease、或autorelease方法来增加或减少引用计数。当对象的引用计数变为0时,Objective-C运行环境会调用对象的清理例程,然后解除分配该对象。

有关这些设计模式更为详尽的讨论请参见Cocoa基本原理指南

应用程序运行环境

iPhone OS的运行环境被设计为快速而安全的程序执行环境。下面的部分这个运行环境的关键部分,并就如何在这个环境中进行操作提供一些指导。

启动过程快,使用时间短

iPhone OS设备的优势是它们的便捷性。用户通常从口袋里掏出设备,用上几秒或几分钟,就又放回口袋中了。在这个过程中,用户可能会打电话、查找联系人、改变正在播放的歌曲、或者取得一片信息。

在iPhone OS中,每次只能有一个前台应用程序。这意味着每次用户在Home屏幕上轻点您的应用程序图标时,您的程序必须快速启动和初始化,以尽可能减少延迟。如果您的应用程序花很长时间来启动,用户可能就不喜欢了。

除了快速启动,您的应用程序还必须做好快速退出的准备。每次用户离开您的应用程序时,无论是按下Home键还是通过软件提供的功能打开了另一个应用程序,iPhone OS会通知您的应用程序退出。在那个时候,您需要尽快将未保存的修改保存到磁盘上。如果您的应用程序退出的时间超过5秒,系统可能会立刻终止它的运行。

当用户切换到另一个应用程序时,虽然您的程序不是运行在后台,但是我们鼓励您使它看起来好像是在后台运行。当您的程序退出时,除了对未保存的数据进行保存之外,还应该保存当前的状态信息;而在启动时,则应该寻找这些状态信息,并将程序恢复到最后一次使用时的状态。这样可以使用户回到最后一次使用时的状态,使用户体验更加一致。以这种方式保存用户的当前位置还可以避免每次启动都需要经过多个屏幕才能找到需要的信息,从而节省使用的时间。

应用程序沙箱

由于安全的原因,iPhone OS将每个应用程序(包括其偏好设置信息和数据)限制在文件系统的特定位置上。这个限制是安全特性的一部分,称为应用程序的“沙箱”。沙箱是一组细粒度的控制,用于限制应用程序对文件、偏好设置、网络资源、和硬件等的访问。在iPhone OS中,应用程序和它的数据驻留在一个安全的地方,其它应用程序都不能进行访问。在应用程序安装之后,系统就通过计算得到一个不透明的标识,然后基于应用程序的根目录和这个标识构建一个指向应用程序家目录的路径。因此,应用程序的家目录具有如下结构:

  • /ApplicationRoot/ApplicationID/

在安装过程中,系统会创建应用程序的家目录和几个关键的子目录,配置应用程序沙箱,以及将应用程序的程序包拷贝到家目录上。将应用程序及其数据放在一个特定的地方可以简化备份-并-恢复操作,还可以简化应用程序的更新及卸载操作。有关系统为每个应用程序创建的专用目录、应用程序更新、及备份-并-恢复操作的更多信息,请参见“文件和数据管理”部分。

重要提示:沙箱可以限制攻击者对其它程序和系统造成的破坏,但是不能防止攻击的发生。换句话说,沙箱不能使您的程序避免恶意的直接攻击。举例来说,如果在您的输入处理代码中有一个可利用的缓冲区溢出,而您又没有对用户输入进行正当性检查,则攻击者可能仍然可以使您的应用程序崩溃,或者通过这种漏洞来执行攻击者的代码。

虚拟内存系统

在本质上,iPhone OS使用与Mac OS X同样的虚存系统。在iPhone OS中,每个程序都仍然有自己的虚拟地址空间,但其可用的虚拟内存受限于现有的物理内存的数量(这和Mac OS X不同)。这是因为当内存用满的时候,iPhone OS并不将非永久内存页面(volatile pages)写入到磁盘。相反,虚拟内存系统会根据需要释放永久内存(nonvolatile memory),确保为正在运行的应用程序提供所需的空间。内存的释放是通过删除当前没有正在使用或包含只读内容(比如代码页面)的内存页面来实现的,这样的页面可以在稍后需要使用的时候重新装载到内存中。

如果内存还是不够,系统也可能向正在运行的应用程序发出通告,要求它们释放额外的内存。所有的应用程序都应该响应这种通告,并尽自己所能减轻系统的内存压力。有关如何在应用程序中处理这种通告的更多信息,请参见“观察低内存警告”部分。

自动休眠定时器

iPhone OS试图省电的一个方法是使用自动休眠定时器。如果在一定的时间内没有检测到触摸事件,系统最初会使屏幕变暗,并最终完全关闭屏幕。大多数开发者都应该让这个定时器打开,但是,游戏和不使用触摸输入的应用程序开发者可以禁用这个定时器,使屏幕在应用程序运行时不会变暗。将共享的UIApplication对象的idleTimerDisabled属性设置为YES,就可以禁用自动休眠定时器。

由于禁用休眠定时器会导致更大的电能消耗,所以开发者应该尽一切可能避免这样做。只有地图程序、游戏、以及不依赖于触摸输入而又需要在设备屏幕上显示内容的应用程序才应该考虑禁用休眠定时器。音频应用程序不需要禁用这个定时器,因为在屏幕变暗之后,音频内容可以继续播放。如果您禁用了定时器,请务必尽快重新激活它,使系统可以更省电。有关应用程序如何省电的其它贴士,请参见“减少电力消耗”部分。

应用程序的程序包

当您连编iPhone程序时,Xcode会将它组织为程序包程序包是文件系统中的一个目录,用于将执行代码和相关资源集合在一个地方。iPhone应用程序包中包含应用程序的执行文件和应用程序需要用到的所有资源(比如应用程序图标、其它图像、和本地化内容)。表1-2列出了一个典型的iPhone应用程序包中的内容(为了便于说明,我们称之为MyApp)。这个例子只是为了演示,表中列出的一些文件可能并不出现在您自己的应用程序包中。

表1-2  一个典型的应用程序包

文件

描述

MyApp

包含应用程序代码的执行文件,文件名是略去.app后缀的应用程序名。这个文件是必需的。

Settings.bundle

设置程序包是一个文件包,用于将应用程序的偏好设置加入到Settings程序中。这种程序包中包含一些属性列表和其它资源文件,用于配置和显示您的偏好设置。更多信息请参见“显示应用程序的偏好设置”部分。

Icon.png

这是个57 x 57像素的图标,显示在设备的Home屏幕上,代表您的应用程序。这个图标不应该包含任何光亮效果。系统会自动为您加入这些效果。这个文件是必须的。更多有关这个图像文件的信息,请参见“应用程序图标和启动图像”部分。

Icon-Settings.png

这是一个29 x 29像素的图标,用于在Settings程序中表示您的应用程序。如果您的应用程序包含设置程序包,则在Settings程序中,这个图标会显示在您的应用程序名的边上。如果您没有指定这个图标文件,系统会将Icon.png文件按比例缩小,然后用做代替文件。有关这个图像文件的更多信息,青参见“显示应用程序的偏好设置”部分。

MainWindow.nib

这是应用程序的主nib文件,包含应用程序启动时装载的缺省用户界面对象。典型情况下,这个nib文件包含应用程序的主窗口对象和一个应用程序委托对象实例。其它界面对象则或者从其它nib文件装载,或者在应用程序中以编程的方式创建(主nib文件的名称可以通过Info.plist文件中的NSMainNibFile键来指定,进一步的信息请参见“信息属性列表”部分)。

Default.png

这是个480 x 320像素的图像,在应用程序启动的时候显示。系统使用这个文件作为临时的背景,直到应用程序完成窗口和用户界面的装载。有关这个图像文件的信息请参见“应用程序图标和启动图像”部分。

iTunesArtwork

这是个512 x 512的图标,用于通过ad-hoc方式发布的应用程序。这个图标通常由App Store来提供,但是通过ad-hoc方式分发的应用程序并不经由App Store,所以在程序包必须包含这个文件。iTunes用这个图标来代表您的程序(如果您的应用程序在App Store上发布,则在这个属性上指定的文件应该和提交到App Store的文件保持一致(通常是个JPEG或PNG 文件),文件名必须和左边显示的一样,而且不带文件扩展名)。

Info.plist

这个文件也叫信息属性列表,它是一个定义应用程序键值的属性列表,比如程序包ID、版本号、和显示名称。进一步的信息请参见“信息属性列表”部分。这个文件是必需的。

sun.png (或其它资源文件)

非本地化资源放在程序包目录的最上层(在这个例子中,sun.png表示一个非本地化的图像)。应用程序在使用非本地化资源时,不需要考虑用户选择的语言设置。

en.lproj

fr.lproj

es.lproj

其它具体语言的工程目录

本地化资源放在一些子目录下,子目录的名称是ISO 639-1定义的语言缩写加上.lproj后缀组成的(比如en.lprojfr.lproj、和es.lproj目录分别包含英语、法语、和西班牙语的本地化资源)。更多信息请参见“国际化您的应用程序”部分。

iPhone应用程序应该是国际化的。程序支持的每一种语言都有一个对应的语言.lproj文件夹。除了为应用程序提供定制资源的本地化版本之外,您还可以本地化您的应用程序图标(Icon.png)、缺省图像(Default.png)、和Settings图标(Icon-Settings.png),只要将同名文件放到具体语言的工程目录就可以了。然而,即使您提供了本地化的版本,也还是应该在应用程序包的最上层包含这些文件的缺省版本。当某些的本地化版本不存在的时候,系统会使用缺省版本。

您可以通过NSBundle类的方法或者与CFBundleRef类型相关联的函数来获取应用程序包中本地化和非本地化图形及声音资源的路径。举例来说,如果您希望得到图像文件sun.png(显示在“响应中断”部分中)的路径并通过它创建一个图像文件,则需要下面两行Objective-C代码:

NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"sun" ofType:@"png"];
UIImage* sunImage = [[UIImage alloc] initWithContentsOfFile:imagePath];

代码中的mainBundle类方法用于返回一个代表应用程序包的对象。有关资源装载的信息请参见资源编程指南

信息属性列表

信息属性列表是一个名为Info.plist的文件,通过Xcode创建的每个iPhone应用程序都包含一个这样的文件。属性列表中的键值对用于指定重要的应用程序运行时配置信息。信息属性列表的元素被组织在一个层次结构中,每个结点都是一个实体,比如数组、字典、字符串、或者其它数值类型。

在Xcode中,您可以通过在Project菜单中选择Edit Active Target TargetName命令、然后在目标的Info窗口中点击Properties控件来访问信息属性列表。Xcode会显示如图1-4所示的信息面板。

图1-4  目标Info窗口的属性面板

The Properties pane of a target’s Info window

属性面板显示的是程序包的一些属性,但并不是所有属性都显示在上面。当您选择“Open Info.plist as File” 按键或在Xcode工程中选择Info.plist文件时,Xcode会显示如图1-5所示的属性列表编辑器窗口,您可以通过这个窗口来编辑属性值和添加键-值对。您还可以查看添加到Info.plist文件中的实际键名,具体操作是按住Control键的同时点击编辑器中的信息属性列表项目,然后选择上下文菜单中的Show Raw Keys/Values命令。

图1-5  信息属性列表编辑器

The information property list editor

Xcode会自动设置某些属性的值,其它属性则需要显式设置。表1-3列出了一些重要的键,供您在自己的Info.plist文件中使用(在缺省情况下,Xcode不会直接显示实际的键名,因此,下表在括号中列出了这些键在Xcode中显示的字符串。您可以查看所有键的实际键名,具体做法是按住Control键的同时点击编辑器中的信息属性列表项目,然后选择上下文菜单中的Show Raw Keys/Values命令)。有关属性列表文件可以包含的完整属性列表及系统如何使用这些属性的信息,请参见运行环境配置指南

表1-3   Info.plist文件中重要的键

CFBundleDisplayName (程序包显示名)

显示在应用程序图标下方的名称。这个值应该本地化为所有支持的语言。

CFBundleIdentifier (程序包标识)

这是由您提供的标识字符串,用于在系统中标识您的应用程序。这个字符串必须是一个统一的类型标识符(UTI),仅包含字母数字(A-Za-z0-9),连字符(-),和句号(.);且应该使用反向DNS格式。举例来说,如果您的公司的域名为Ajax.com,且您创建的应用程序名为Hello,则可以将字符串com.Ajax.Hello作为应用程序包的标识。

程序包的标识用于验证应用程序的签名。

CFBundleURLTypes (URL类型)

这是应用程序能够处理的URL类型数组。每个URL类型都是一个字典,定义一种应用程序能够处理的模式(如httpmailto)。应用程序可以通过这个属性来注册定制的URL模式。

CFBundleVersion (程序包版本号)

这是一个字符串,指定程序包的连编版本号。它的值是单调递增的,由一或多个句号分隔的整数组成。这个值不能被本地化。

LSRequiresIPhoneOS

这是一个Boolean值,用于指示程序包是否只能运行在iPhone OS 系统上。Xcode自动加入这个键,并将它的值设置为true。您不应该改变这个键的值。

NSMainNibFile (主nib文件的名称)

这是一个字符串,指定应用程序主nib文件的名称。如果您希望使用其它的nib文件(而不是Xcode为工程创建的缺省文件)作为主nib文件,可以将该nib文件名关联到这个键上。nib文件名不应该包含.nib扩展名。

UIStatusBarStyle

这是个字符串,标识程序启动时状态条的风格。这个键的值基于UIApplication.h头文件中声明的UIStatusBarStyle常量。缺省风格是UIStatusBarStyleDefault。在启动完成后,应用程序可以改变状态条的初始风格。

UIStatusBarHidden

这个一个Boolean值,指定在应用程序启动的最初阶段是否隐藏状态条。将这个键值设置为true将隐藏状态条。缺省值为false

UIInterfaceOrientation

这是个字符串,标识应用程序用户界面的初始方向。这个键的值基于UIApplication.h头文件中声明的UIInterfaceOrientation 常量。缺省风格是UIInterfaceOrientationPortrait

有关将应用程序启动为景观模式的更多信息,请参见“以景观模式启动”部分。

UIPrerenderedIcon

这个一个Boolean值,指示应用程序图标是否已经包含发光和斜面效果。这个属性缺省值为false。如果您不希望系统在您的原图上加入这些效果,则将它设置为true。

UIRequiredDeviceCapabilities

这是个信息键,作用是使iTunes和App Store知道应用程序运行需要依赖于哪些与设备相关的特性。iTunes和移动App Store程序使用这个列表来避免将应用程序安装到不支持所需特性的设备上。

这个键的值可以是一个数组或者字典如果您使用的是数组,则数组中存在某个键就表示该键对应的特性是必需的;如果您使用的是字典,则必须为每个键指定一个Boolean值,表示该键是否需要。无论哪种情况,不包含某个键表示该键对应的特性不是必需的。

如果您需要可包含在这个字典中的键列表,请参见表1-4。这个键在iPhone OS 3.0及更高版本上才被支持。

UIRequiresPersistentWiFi

这是个Boolean值,用于通知系统应用程序是否使用Wi-Fi网络进行通讯。如果您的应用程序需要在一段时间内使用Wi-Fi,则应该将这个键值设置为true;否则,为了省电,设备会在30分钟内关闭Wi-Fi连接。设置这个标志还可以让系统在Wi-Fi网络可用但未被使用的时候显示网络选择对话框。这个键的缺省值是false

请注意,当设备处于闲置状态(也就是屏幕被锁定的状态)时,这个属性的值为true是没有作用的。这种情况下,应用程序会被认为是不活动的,虽然它可能在某些级别上还可以工作,但是没有Wi-Fi连接。

UISupportedExternalAccessoryProtocols

这是个字符串数组,标识应用程序支持的配件协议。配件协议是应用程序和连接在iPhone或iPod touch上的第三方硬件进行通讯的协议。系统使用这个键列出的协议来识别当配件连接到设备上时可以打开的应用程序。

有关配件和协议的更多信息,请参见“和配件通讯”部分。这个键只在iPhone OS 3.0和更高版本上支持。

UIViewGroupOpacity

这是个Boolean值,用于指示Core Animation子层是否继承其超层的不透明特性。这个特性使开发者可以在仿真器上进行更为复杂的渲染,但是对性能会有显著的影响。如果属性列表上没有这个键,则其缺省值为NO

这个键只在iPhone OS 3.0和更高版本上支持。

UIViewEdgeAntialiasing

这是个Boolean值,用于指示在描画不和像素边界对齐的层时,Core Animation层是否进行抗锯齿处理。这个特性使开发者可以在仿真器上进行更为复杂的渲染,但是对性能会有显著的影响。如果属性列表上没有这个键,则其缺省值为NO

这个键只在iPhone OS 3.0和更高版本上支持。

如果信息属性文件中的属性值是显示在用户界面上的字符串,则应该进行本地化,特别是当Info.plist中的字符串值是与本地化语言子目录下InfoPlist.strings文件中的字符串相关联的键时。更多信息请参见“国际化您的应用程序”部分。

表1-4列出了和UIRequiredDeviceCapabilities键相关联的数组或字典中可以包含的键。您应该仅包含应用程序确实需要的键。如果应用程序可以通过不执行某些代码路径来适应设备特性不存在的情况,则不需要使用对应的键。

表1-4   UIRequiredDeviceCapabilities键的字典键

描述

telephony

如果您的应用程序需要Phone程序,则包含这个键。如果您的应用程序需要打开tel模式的URL,则可能需要这个特性。

sms

如果您的应用程序需要Messages程序,则包含这个键。如果您的应用程序需要打开sms模式的URL,则可能需要这个特性。

still-camera

如果您的应用程序使用UIImagePickerController接口来捕捉设备照相机的图像时,需要包含这个键。

auto-focus-camera

如果您的应用程序需要设备照相机的自动对焦能力,则需要包含这个键。虽然大多数开发者应该不需要,但是如果您的应用程序支持微距摄影,或者需要更高锐度的图像以进行某种处理,则可能需要包含这个键。

video-camera

如果您的应用程序使用UIImagePickerController接口来捕捉设备摄像机的视频时,需要包含这个键。

wifi

当您的应用程序需要设备的网络特性时,包含这个键。

accelerometer

如果您的应用程序使用UIAccelerometer接口来接收加速计事件,则需要包含这个键。如果您的程序仅需要检测设备的方向变化,则不需要。

location-services

如果您的应用程序使用Core Location框架来访问设备的当前位置,则需要包含这个键(这个键指的是一般的位置服务特性。如果您需要GPS级别的精度,则还应该包含gps键)。

gps

如果您的应用程序需要GPS(或者AGPS)硬件,以获得更高精度的位置信息,则包含这个键。如果您包含了这个键,就应该同时包含location-services键。如果您的程序需要更高精度的位置数据,而不是由蜂窝网络或Wi-fi信号提供的数据,则应该要求只接收GPS数据。

magnetometer

如果您的应用程序使用Core Location框架接收与方向有关的事件时,则需要包含这个键。

microphone

如果您的应用程序需要使用内置的麦克风或支持提供麦克风的外设,则包含这个键。

opengles-1

如果您的应用程序需要使用OpenGL ES 1.1 接口,则包含这个键。

opengles-2

如果您的应用程序需要使用OpenGL ES 2.0 接口,则包含这个键。

应用程序图标和启动图像

显示在用户Home屏幕上的图标文件的缺省文件名为Icon.png(虽然通过Info.plist文件中的CFBundleIconFile属性可以进行重命名)。它应该是一个位于程序包最上层目录的PNG文件。应用程序图标应该是一个57 x 57像素的图像,不带任何刨光和圆角斜面效果。典型情况下,系统在显示之前会将这些效果应用到图标上。然而,在应用程序的Info.plist文件中加入UIPrerenderedIcon键可以重载这个行为,更多信息请参见表1-3

请注意:如果您以ad-hoc的方式(而不是通过App Store)将应用程序发布给本地用户,则程序包中还应该包含一个512 x 512像素版本的应用程序图标,命名为iTunesArtwork。在分发您的应用程序时,iTunes需要显示这个文件提供的图标。

应用程序的启动图像文件的文件名为Default.png。这个图像应该和应用程序的初始界面比较相似;系统在应用程序准备好显示用户界面之前显示启动文件,使用户觉得启动速度很快。启动图像也应该是PNG图像文件,位于应用程序包的顶层目录。如果应用程序是通过URL启动的,则系统会寻找名为Default-scheme.png的启动文件,其中scheme是URL的模式。如果该文件不存在,才选择Default.png文件。

将一个图像文件加入到Xcode工程的具体做法是从Project菜单中选择Add to Project命令,在浏览器中定位目标文件,然后点击Add按键。

请注意:除了程序包顶层目录中的图标和启动图像,您还可以在应用程序中具体语言的工程子目录下包含这些图像文件的本地化版本。更多有关应用程序本地化资源的信息请参见“国际化您的应用程序”部分。

Nib文件

nib文件是一种数据文件,用于存储可在应用程序需要时使用的一些“冻结”的对象。大多数情况下,应用程序使用nib文件来存储构成用户界面的窗口和视图。当您将nib文件载入应用程序时,nib装载代码会将文件中的内容转化为应用程序可以操作的真正对象。通过这个机制,nib文件省去了用代码创建那些对象的工作。

Interface Builder是一个可视化的设计环境,您可以用它来创建nib文件。您可以将标准对象(比如UIKit框架中提供的窗口和视图)和Xcode工程中的定制对象放到nib文件中。在Interface Builder中创建视图层次相当简单,只需要对视图对象进行简单拖拽就可以了。您也可以通过查看器窗口来配置每个对象的属性,以及通过创建对象间的连接来定义它们在运行时的关系。您所做的改变最终都会作为nib文件的一部分存储到磁盘上。

在运行时,当您需要nib文件中包含的对象时,就将nib文件装载到程序中。典型情况下,装载nib文件的时机是当用户界面发生变化和需要在屏幕上显示某些新视图的时候。如果您的应用程序使用视图控制器,则视图控制器会自动处理nib文件的装载过程,当然,您也可以通过NSBundle类的方法自行装载。

有关如何设计应用程序用户界面的更多信息,请参见iPhone用户界面指南。有关如何创建nib文件的信息则参见Interface Builder用户指南

处理关键的应用程序任务

本部分将描述几个所有iPhone应用程序都应该处理的任务。这些任务是整个应用程序生命周期的一部分,因此也是将应用程序集成到iPhone OS系统的重要方面。在最坏的情况下,没有很好地处理其中的某些任务甚至可能会导致应用程序被操作系统终止。

初始化和终止

在初始化和终止过程中,UIApplication类会向应用程序的委托发送恰当的消息,使其执行必要的任务。虽然系统并不要求您的应用程序响应这些消息,但是,几乎所有的iPhone应用程序都应该处理这些消息。初始化是您为应用程序准备用户界面及使其进入初始运行状态的阶段。类似地,在终止阶段,您应该把未保存的数据和关键的应用程序状态写入磁盘。

由于一个iPhone应用程序必须在其它应用程序启动之前退出,所以花在初始化和终止阶段的执行时间要尽可能少。初始化阶段并不适合装载大的、却又不需要马上使用的数据结构。在开始阶段,您的目标应该是尽可能快地显示应用程序的用户界面,最好是使它进入最后一次退出的状态。如果您的应用程序在启动过程中需要更多的时间来装载网络数据,或者执行一些可能很慢的任务,则应该首先显示出用户界面并运行起来,然后在后台线程中执行速度慢的任务。这样,您就有机会向用户显示进度条和其它反馈信息,指示应用程序正在装载必要的数据,或者正在执行重要的任务。

表1-5列举出UIApplicationDelegate协议定义的方法,您在应用程序委托中需要实现这些协议方法,以处理初始化和终止的事务。表中还列出了您在每个方法中应该执行的关键事务。

表1-5  应用程序委托的责任

委托方法

描述

applicationDidFinishLaunching:

使用这个方法来将应用程序恢复到上一个会话的状态。您也可以在这个方法中执行应用程序数据结构和用户界面的定制初始化。

applicationWillTerminate:

使用这个方法来将未存数据或关键的应用程序状态存入磁盘。您也可以在这个方法中执行额外的清理工作,比如删除临时文件。

响应中断

除了Home按键可以终止您的应用程序之外,系统也可以暂时中断您的应用程序,使用户得以响应一些重要的事件。举例来说,应用程序可能被呼入的电话、SMS信息、日历警告、或者设备上的Sleep按键所打断。按下Home按键会终止您的应用程序,而上述这些中断则只是暂时的。如果用户忽略这些中断,您的应用程序可以象之前那样继续运行;然而,如果用户决定接电话或回应SMS信息,系统就会开始终止您的程序。

图1-6显示了在电话、SMS信息、或者日历警告到来时发生的事件序列。紧接在图后面的步骤说明更为详细地描述了事件序列的关键点,包括您在响应每个事件时应该做的事项。这个序列并不反映当用户按下Sleep/Wake按键时发生的情景;该场景的事件序列在步骤说明之后的部分进行描述。

图1-6  中断过程的事件流程

The flow of events during an interruption
  1. 系统检测到有电话、SMS信息、或者日历警告发生。

  2. 系统调用应用程序委托applicationWillResignActive:方法,同时禁止将触摸事件发送给您的应用程序。

    中断会导致应用程序暂时失去控制权。如果控制权的丢失会影响程序的行为或导致不好的用户体验,您就应该在委托方法中采取恰当的步骤进行规避。举例来说,如果您的程序是个游戏,就应该暂停。您还应该禁用定时器、降低OpenGL的帧率(如果正在使用OpenGL的话),通常还应该使应用程序进行休眠状态。在这休眠状态下,您的应用程序继续运行,但是不应该做任何重要的工作。

  3. 系统显示一个带有事件信息的警告窗口。用户可以选择忽略或响应该事件。

  4. 如果用户忽略该事件,系统就调用应用程序委托的applicationDidBecomeActive:方法,并重新开始向应用程序传递触摸事件。 

    您可以在这个方法中重新激活定时器、提高OpenGL的帧率、以及将应用程序从休眠状态唤醒。对于处于暂停状态的游戏,您应该考虑使它停在当时的状态上,等待用户做好重新玩的准备。举例来说,您可以显示一个警告窗口,而窗口中带有重新开始的控件。

  5. 如果用户选择响应该事件(而不是忽略),则系统会调用应用程序委托的applicationWillTerminate:方法。您的应用程序应该正常终止,保存所有必要的上下文信息,使应用程序在下一次启动的时候可以回到同样的位置。

    在您的应用程序终止之后,系统就开始启动负责中断的应用程序。

根据用户对中断的不同响应,系统可能在中断结束之后再次启动您的应用程序。举例来说,如果用户接听一个电话并在完成后挂断,则系统会重新启动您的应用程序;如果用户在接听电话过程中回到Home屏幕或启动另一个程序,则系统就不再启动您的应用程序了。

重要提示:当用户接听电话并在通话过程中重新启动您的应用程序时,状态条的高度会变大,以反映当前用户正在通话中。类似地,当用户结束通话的时候,状态条的高度会缩回正常尺寸。您的应用程序应该为状态条高度的变化做好准备,并据此调整内容区域的尺寸。视图控制器会自动处理这个行为,然而,如果您通过代码进行用户界面的布局,就需要在视图布局以及通过layoutSubviews方法处理动态布局变化时考虑状态条的高度。

在运行您的应用程序时,如果用户按下设备的休眠/唤醒按键,系统会调用应用程序委托的applicationWillResignActive:方法,停止触摸事件的派发,然后使设备进入休眠状态。之后,当用户唤醒设备时,系统会调用应用程序委托的applicationDidBecomeActive:方法,并再次开始向应用程序派发事件。如同处理其它中断一样,您应该使用这些方法来使应用程序进入休眠状态(或者暂停游戏)及再次唤醒它们。在休眠时,您的应用程序应该尽可能少用电力。

观察低内存警告

当系统向您的应用程序发送低内存警告时,您需要加以注意。当可用内存的数量降低到安全阈值以下时,iPhone OS会通知最前面的应用程序。如果您的应用程序收到这种警告,就必须尽可能多地释放内存,即释放不再需要的对象或清理易于在稍后进行重建的缓存。

UIKit提供如下几种接收低内存警告的方法:

一旦收到上述的任何警告,您的处理代码就应该立即响应,释放所有不需要的内存。视图控制器应该清除当前离屏的视图对象,您的应用程序委托则应该释放尽可能多的数据结构,或者通知其它应用程序对象释放其拥有的内存。

如果您的定制对象知道一些可清理的资源,则可以让该对象注册UIApplicationDidReceiveMemoryWarningNotification通告,并在通告处理器代码中直接释放那些资源。如果您通过少数对象来管理大多数可清理的资源,且适合清理所有的这些资源,则同样可以让这些对象进行注册。但是,如果您有很多可清理的对象,或者仅希望释放这些对象的一个子集,则在您的应用程序委托中进行释放可能更好一些。

重要提示:和系统的应用程序一样,您的应用程序总是需要处理低内存警告,即使在测试过程中没有收到那些警告,也一样要进行处理。系统在处理请求时会消耗少量的内存。在检测到低内存的情况时,系统会将低内存警告发送给所有正在运行的进程(包括您的应用程序),而且可能终止某些后台程序(如果必要的话),以减轻内存的压力。如果释放后内存仍然不够—可能因为您的应用程序发生泄露或消耗太多内存—系统仍然可能会终止您的应用程序。

定制应用程序的行为

有几种方法可以对基本的应用程序行为进行定制,以提供您希望的用户体验。本文的下面部分将描述一些必须在应用程序级别进行的定制。

以景观模式启动

为了配合Home屏幕的方向,iPhone OS的应用程序通常以肖像模式启动。如果您的应用程序既可以以景观模式运行,也可以以肖像模式运行,那么,一开始应该总是以纵向模式启动,然后由视图控制器根据设备的方向旋转用户界面。但是,如果您的应用程序只能以景观模式启动,则必须执行下面的步骤,使它一开始就以景观模式启动。

重要提示:上面描述的步骤假定您的应用程序使用视图控制器来管理视图层次。视图控制器为处理方向改变和复杂的视图相关事件提供了大量的基础设施。如果您的应用程序不使用视图控制器—游戏和其它基于OpenGL ES的应用程序可能是这样的—就必须根据需要旋转绘图表面(或者调整绘图命令),以便将您的内容以景观模式展示出来。

UIInterfaceOrientation属性提示iPhone OS在启动时应该配置应用程序状态条(如果有的话)的方向,就象配置视图控制器管理下的视图方向一样。在iPhone OS 2.1及更高版本的系统中,视图控制器会尊重这个属性,将视图的初始方向设置为指定的方向。使用这个属性相当于在applicationDidFinishLaunching:方法的一开始执行UIApplicationsetStatusBarOrientation:animated:方法。

请注意:在v2.1之前的iPhone OS系统中,如果要以景观模式启动基于视图控制器的应用程序,需要在上文描述的所有步骤的基础上对应用程序根视图的转换矩阵进行一个90度的旋转。在iPhone OS 2.1之前,视图控制器并不会根据UIInterfaceOrientation键的值自动进行旋转,当然在iPhone OS 2.1及更高版本的系统中不需要这个步骤。

和其它应用程序进行通讯

如果一个应用程序支持一些已知类型的URL,您就可以通过对应的URL模式和该程序进行通讯。然而,在大多数情况下,URL只是用于简单地启动一个应用程序并显示一些和调用方有关的信息。举例来说,对于一个用于管理地址信息的应用程序,您就可以在发送给它的URL中包含一个Maps程序可以处理的地址,以便显示相应的位置。这个级别的通讯为用户创造一个集成度高得多的环境,减少应用程序重新实现设备上其它程序已经实现的功能的必要性。

苹果内置支持httpmailtotel、和sms这些URL模式,还支持基于http的、指向Maps、YouTube、和iPod程序的URL。应用程序也可以自己注册定制的URL模式。您的应用程序可以和其它应用程序通讯,具体方法是用正确格式的内容创建一个NSURL对象,然后将它传给共享UIApplication对象openURL:方法。openURL:方法会启动注册接收该URL类型的应用程序,并将URL传给它。当用户最终退出该应用程序时,系统通常会重新启动您的应用程序,但并不总是这样。系统会考虑用户在URL处理程序中的动作及在用户看来返回您的应用程序是否合理,然后做出决定。

下面的代码片断展示了一个程序如何请求另一个程序提供的服务(假定这个例子中的“todolist”是由应用程序注册的定制模式):

NSURL *myURL = [NSURL URLWithString:@"todolist://www.acme.com?Quarterly%20Report#200806231300"];
[[UIApplication sharedApplication] openURL:myURL];

重要提示:如果您的URL类型包含的模式和苹果定义的一样,则启动的是苹果提供的程序,而不是您的程序。如果有多个第三方的应用程序注册处理同样的URL模式,则该类型的URL由哪个程序处理是没有定义的。

如果您的应用程序定义了自己的URL模式,则应该实现对该模式进行处理的方法,具体信息在“实现定制的URL模式”部分中进行描述。有关系统支持的URL处理,包括如何处理URL的格式,请参见苹果的URL模式参考

实现定制的URL模式

您可以为自己的应用程序注册包含定制模式的URL类型。定制的URL模式是第三方应用程序和其它程序及系统进行交互的机制。通过定制的URL模式,应用程序可以将自己的服务提供给其它程序。

注册定制的URL模式

在为您的应用程序注册URL类型时,必须指定CFBundleURLTypes属性的子属性,我们已经在“信息属性列表”部分中介绍过这个属性了。CFBundleURLTypes属性是应用程序的Info.plist文件中的一个字典数组,每个字典负责定义一个应用程序支持的URL类型。表1-6描述了CFBundleURLTypes字典的键和值。

表1-6   CFBundleURLTypes属性的键和值

CFBundleURLName

这是个字符串,表示URL类型的抽象名。为了确保其唯一性,建议您使用反向DNS风格的标识,比如com.acme.myscheme

这里提供的URL类型名是一个指向本地化字符串的键,该字符串位于本地化语言包子目录中的InfoPlist.strings文件中。本地化字符串是人类可识别的URL类型名称,用相应的语言来表示。

CFBundleURLSchemes

这是个URL模式的数组,表示归属于这个URL类型的URL。每个模式都是一个字符串。属于指定URL类型的URL都带有它们的模式组件。

图1-7显示了一个正在用内置的Xcode编辑器编辑的Info.plist文件。在这个图中,左列中的URL类型入口相当于您直接加入到Info.plist文件的CFBundleURLTypes键。类似地,“URL identifier”和“URL Schemes”入口相当于CFBundleURLNameCFBundleURLSchemes键。

图1-7  Info.plist文件中定义一个定制的URL模式

Defining a custom URL scheme in the Info.plist file

您在对CFBundleURLTypes属性进行定义,从而注册带有定制模式的URL类型之后,可以通过下面的方式来进行测试:

  1. 连编、安装、和运行您的应用程序。

  2. 回到Home屏幕,启动Safari(在iPhone仿真器上,在菜单上选择Hardware > Home命令就可以回到Home屏幕)。

  3. 在Safari的地址栏中,键入使用定制模式的URL。

  4. 确认您的应用程序是否启动,以及应用程序委托是否收到application:handleOpenURL:消息。

处理URL请求

应用程序委托在application:handleOpenURL:方法中处理传递给应用程序的URL请求。如果您已经为自己的应用程序注册了定制的URL模式,则务必在委托中实现这个方法。

基于定制模式的URL采用的协议是请求服务的应用程序能够理解的。URL中包含一些注册模式的应用程序期望得到的信息,这些信息是该程序在处理或响应URL请求时需要的。传递给application:handleOpenURL:方法的NSURL对象表示的是Cocoa Touch框架中的URL。NSURL遵循RFC 1808规范,该类中包含一些方法,用于返回RFC 1808定义的各个URL要素,包括用户名、密码、请求、片断、和参数字符串。与您注册的定制模式相对应的“协议”可以使用这些URL要素来传递各种信息。

在程序清单1-2显示的application:handleOpenURL:方法实现中,传入的URL对象在其请求和片断部分带有具体应用程序的信息。应用程序委托抽出这些信息—在这个例子中,是指一个to-do任务的名称和到期日—并根据这些信息创建应用程序的模型对象。

程序清单1-2  处理基于定制模式的URL请求

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    if ([[url scheme] isEqualToString:@"todolist"]) {
        ToDoItem *item = [[ToDoItem alloc] init];
        NSString *taskName = [url query];
        if (!taskName || ![self isValidTaskString:taskName]) { // must have a task name
            [item release];
            return NO;
        }
        taskName = [taskName stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
 
        item.toDoTask = taskName;
        NSString *dateString = [url fragment];
        if (!dateString || [dateString isEqualToString:@"today"]) {
            item.dateDue = [NSDate date];
        } else {
            if (![self isValidDateString:dateString]) {
                [item release];
                return NO;
            }
            // format: yyyymmddhhmm (24-hour clock)
            NSString *curStr = [dateString substringWithRange:NSMakeRange(0, 4)];
            NSInteger yeardigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(4, 2)];
            NSInteger monthdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(6, 2)];
            NSInteger daydigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(8, 2)];
            NSInteger hourdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(10, 2)];
            NSInteger minutedigit = [curStr integerValue];
 
            NSDateComponents *dateComps = [[NSDateComponents alloc] init];
            [dateComps setYear:yeardigit];
            [dateComps setMonth:monthdigit];
            [dateComps setDay:daydigit];
            [dateComps setHour:hourdigit];
            [dateComps setMinute:minutedigit];
            NSCalendar *calendar = [NSCalendar currentCalendar];
            NSDate *itemDate = [calendar dateFromComponents:dateComps];
            if (!itemDate) {
                [dateComps release];
                [item release];
                return NO;
            }
            item.dateDue = itemDate;
            [dateComps release];
        }
 
        [(NSMutableArray *)self.list addObject:item];
        [item release];
        return YES;
    }
    return NO;
}

请务必对传入的URL输入进行验证。如果您希望了解如何避免URL处理的相关问题,请参见安全编码指南文档中的验证输入部分。如果要了解苹果定义的URL模式,请参见苹果的URL模式参考

显示应用程序的偏好设置

如果您的应用程序通过偏好设置来控制其行为的不同方面,那么,以何种方式向用户提供偏好设置就取决于它们是否为程序的必需部分。

  • 如果偏好设置是程序使用的必需部分(且直接实现起来足够简单),那么应该直接通过应用程序的定制界面来呈现。

  • 如果偏好设置不是必需的,且要求相对复杂的界面,则应该通过系统的Settings程序来呈现。

在确定一组偏好设置是否为程序的必需部分时,请考虑您为程序设计的使用模式。如果您希望用户相对频繁地修改偏好设置,或者这些偏好设置对程序的行为具有相对重要的影响,则可能就是必需部分。举例来说,游戏中的设置通常都是玩游戏的必需部分,或者是用户希望快速改变的项目。然而,由于Settings程序是一个独立的程序,所以只能用于处理用户不频繁访问的偏好设置。

如果您选择在应用程序内进行偏好设置管理,则可以自行定义用户界面及编写代码来实现。但是,如果您选择使用Settings程序,则必须提供一个设置包(Settings Bundle)来进行管理。

设置包是位于应用程序的程序包目录最顶层的定制资源,它是一个封装了的目录,名字为Settings.bundle。设置包中包含一些具有特别格式的数据文件(及其支持资源),其作用是告诉Settings程序如何显示您的偏好设置。这些文件还告诉Settings程序应该把结果值存储在偏好设置数据库的什么位置上,以便应用程序随后可以通过NSUserDefaultsCFPreferences API来进行访问。

如果您通过设置包来实现偏好设置管理,则还应该提供一个定制的图标。Settings程序会在您的应用程序包的最顶层寻找名为Icon-Settings.png的图像文件,并将该图像显示在应用程序名称的边上。该文件应该是一个29 x 29像素的PNG图像文件。如果您没有在应用程序包的最顶层提供这个文件,则Settings程序会缺省使用缩放后的应用程序图标(Icon.png)。

有关如何为应用程序创建设置包的更多信息,请参见“应用程序的偏好设置”部分。

关闭屏幕锁定

如果一个基于iPhone OS的设备在某个特定时间段中没有接收到触摸事件,就会关闭屏幕,并禁用触摸传感器。以这种方式锁定屏幕是省电的重要方法。因此,除非您确实需要在应用程序中避免无意的行为,否则应该总是打开屏幕锁定功能。举例来说,如果您的应用程序不接收屏幕事件,而是使用其它特性(比如加速计)来进行输入,则可能需要禁用屏幕锁定功能。

将共享的UIApplication对象的idleTimerDisabled属性设置为YES,就可以禁止屏幕锁定。请务必在程序不需要禁止屏幕锁定功能时将该属性重置为NO。举例来说,您可能在用户玩游戏的时候禁止了屏幕锁定,但是,当用户处于配置界面或没有处于游戏活跃状态时,应该重新打开这个功能。

国际化您的应用程序

理想情况下,iPhone应用程序显示给用户的文本、图像、和其它内容都应该本地化为多种语言。比如,警告对话框中显示的文本就应该以用户偏好的语言显示。为工程准备特定语言的本地化内容的过程就称为国际化。工程中需要本地化的候选组件包括:

  • 代码生成的文本,包括与具体区域设置有关的日期、时间、和数字格式。

  • 静态文本—比如装载到web视图、用于显示应用程序帮助的HTML文件。

  • 图标(包括您的应用程序图标)及其它包含文本或具体文化意义的图像。

  • 包含发声语言的声音文件。

  • Nib文件

通过Settings程序,用户可以从Language偏好设置视图(参见图1-8)中选择希望在用户界面上看到的语言。您可以访问General设置,然后在International组中找到该视图。

图1-8  语言偏好设置视图

The Language preference view

用户选择的语言和程序包中的一个子目录相关联,该子目录名由两个部分组成,分别是ISO 639-1定义的语言码和.lproj后缀。您还可以对语言码进行修改,使之包含具体的地区,方法是在后面(在下划线之后)加入ISO 3166-1定义的区域指示符。举例来说,如果要指定美国英语的本地化资源,程序包中的子目录应该命名为en_US.lproj。我们约定,本地化语言子目录称为lproj文件夹。

请注意:您也可以使用ISO 639-2语言码,而不一定使用ISO 639-1的定义。有关语言和区域代码的信息,请参见国际化编程主题文档中的“语言和地域的指定”部分。

一个lproj文件夹中包含所有指定语言(还可能包含指定地区)的本地化内容。您可以用NSBundle类或CFBundleRef封装类型提供的工具来(在应用程序的lproj文件夹)定位当前选定语言的本地化资源。列表1-3给出一个包含英语(en)本地化内容的目录。

列表1-3  本地化语言子目录的内容

en.lproj/
    InfoPlist.strings
    Localizable.strings
    sign.png

这个例子目录有下面几个项目:

  • InfoPlist.strings文件,包含与Info.plist文件中特定键(比如CFBundleDisplayName)相关联的本地化字符串值。比如,一个英文名称为Battleship的应用程序,其CFBundleDisplayName键在fr.lproj子目录的InfoPlist.strings文件中有如下的入口:

    CFBundleDisplayName = "Cuirassé";
  • Localizable.strings文件,包含应用程序代码生成的字符串的本地化版本。

  • 本例子中的sign.png,是一个包含本地化图像的文件。

为了本地化,我们需要国际化代码中的字符串,具体做法是用NSLocalizedString宏来代替字符串。这个宏的定义如下:

NSString *NSLocalizedString(NSString *key, NSString *comment);

第一个参数是一个唯一的键,指向给定lproj文件夹中Localizable.strings文件里的一个本地化字符串;第二个参数是一个注释,说明字符串如何使用,因此可以为翻译人员提供额外的上下文。举例来说,假定您正在设置用户界面中一个标签(UILabel对象)的内容,则下面的代码可以国际化该标签的文本:

label.text = NSLocalizedString(@"City", @"Label for City text field");

然后,您就可以为给定语言创建一个Localizable.strings文件,并将它加入到相应的lproj文件夹中。对于上文例子中的键,该文件中应该有如下入口:

"City" = "Ville";

请注意:另一种方法是在代码中恰当的地方插入NSLocalizedString调用,然后运行genstrings命令行工具。该工具会生成一个Localizable.strings文件的模板,包含每个需要翻译的键和注释。更多有关genstrings的信息,请参见genstrings(1)的man页面。

更多有关国际化的信息,请参见国际化编程主题

性能和响应速度的调优

在应用程序开发过程的每一步,您都应该考虑自己所做的设计对应用程序总体性能的影响。由于iPhone和iPod touch设备的移动本质,iPhone应用程序的操作环境受到更多的限制。本文的下面部分将描述在开发过程中应该考虑哪些因素。

不要阻塞主线程

您应该认真考虑在应用程序主线程上执行的任务。主线程是应用程序处理触摸事件和其它用户输入的地方。为了确保应用程序总是可以响应用户,我们不应该在主线程中执行运行时间很长或可能无限等待的任务,比如访问网络的任务。相反,您应该将这些任务放在后台线程。一个推荐的方法是将每个任务都封装在一个操作对象中,然后加入操作队列。当然,您也可以自己创建显式的线程。

将任务转移到后台可以使您的主线程继续处理用户输入,这对于应用程序的启动和退出尤其重要。在这些时候,系统期望您的应用程序及时响应事件。如果应用程序的主线程在启动过程中被阻塞住了,系统甚至可能在启动完成之前将它杀死;如果主线程在退出时被阻塞了,则应用程序可能来不及保存关键用户数据就被杀死了。

更多有关如何使用操作对象和线程的信息,请参见线程编程指南

有效地使用内存

由于iPhone OS的虚存模型并不包含磁盘交换区空间,所以应用程序在更大程度上受限于可供使用的内存。对内存的大量使用会严重降低系统的性能,可能导致应用程序被终止。因此,在设计阶段,您应该把减少应用程序的内存开销放在较高优先级上。

应用程序的可用内存和相对性能之间有直接的联系。可用内存越少,系统在处理未来的内存请求时就越可能出问题。如果发生这种情况,系统总是先把代码页和其它非易失性资源从内存中移除。但是,这可能只是暂时的修复,特别是当系统在短时间后又再次需要那些资源的时候。相反,您需要尽可能使内存开销最小化,并及时清除自己使用的内存。

本文的下面部分将就如何有效使用内存和在只有少量内存时如何反应方面提供更多的指导。

减少应用程序的内存印迹

表1-7列出一些如何减少应用程序总体内存印迹的技巧。在开始时将内存印迹降低了,随后就可以有更多的空间用于需要操作的数据。

表1-7  减少应用程序内存印迹的技巧

技巧

采取的措施

消除内存泄露

由于内存是iPhone OS的关键资源,所以您的应用程序不应该有任何的内存泄露。存在内存泄露意味着应用程序在之后可能没有足够的内存。您可以用Instruments程序来跟踪代码中的泄露,该程序既可以用于仿真器,也可以用于实际的设备。有关如何使用Instruments的更多信息,请参见Instruments用户指南

使资源文件尽可能小

文件驻留在磁盘中,但在使用时需要载入内存。属性列表文件和图像文件是通过简单的处理就可以节省空间的两种资源类型。您可以通过NSPropertyListSerialization类将属性列表文件存储为二进制格式,从而减少它们的使用空间;对于图像,可以将所有图像文件压缩得尽可能小(PNG图像是iPhone应用程序的推荐图像格式,可以用pngcrush工具来进行压缩)。

使用Core Data或SQLite来处理大的数据集合

如果您的应用程序需要操作大量的结构化数据,请将它存储在Core Data的持久存储或SQLite数据库,而不是使用扁平文件。Core Data和SQLite都提供了管理大量数据的有效方法,不需要将整个数据一次性地载入内存。

Core Data的支持是在iPhone OS 3.0系统上引入的。

延缓装载资源

在真正需要资源文件之前,永远不应该进行装载。预先载入资源文件表面看好象可以节省时间,但实际上会使应用程序很快变慢。此外,如果您最终没有用到那些资源,预先载入将只是浪费内存。

将程序连编为Thumb格式

加入-mthumb开关可以将代码的尺寸减少最多达35%。但是,对于具有大量浮点数运算的代码模块,请务必将这个选项关闭,因为对那样的模块使用Thumb反而会导致性能的下降。

恰当地分配内存

iPhone应用程序使用委托内存模式,因此,您必须显式保持和释放内存。表1-8列出了一些在程序中分配内存的技巧。

表1-8  分配内存的技巧

技巧

采取的措施

减少自动释放对象的使用

通过autorelease方法释放的对象会留在内存中,直到显式清理自动释放池或者程序再次回到事件循环。在任何可能的时候,请避免使用autorelease方法,而是通过release方法立即收回对象占用的空间。如果您必须创建一定数量的自动释放对象,则请创建局部的自动释放池,以便在返回事件循环之前定期对其进行清理,回收那些对象的内存。

为资源设置尺寸限制

避免装载大的资源文件,如果有更小的文件可用的话。请用适合于iPhone OS设备的恰当尺寸图像来代替高清晰度的图像。如果您必须使用大的资源文件,需要考虑仅装载当前需要的部分。举例来说,您可以通过mmapmunmap函数来将文件的一部分载入内存或从内存卸载,而不是操作整个文件。有关如何将文件映射到内存的更多信息,请参见文件系统性能指南

避免无边界的问题集

无边界的问题集可能需要计算任意大量的数据。如果该集合需要的内存比当前系统能提供的还要多,则您的应用程序可能无法进行计算。您的应用程序应该尽可能避免处理这样的集合,而将它们转化为内存使用极限已知的问题。

有关如何在iPhone应用程序中分配内存及使用自动释放池的详细信息,请参见Cocoa基本原理指南文档的Cocoa对象部分。

浮点数学运算的考虑

iPhone–OS设备上的处理器有能力在硬件上处理浮点数计算。如果您目前的程序使用基于软件的定点数数学库进行计算,则应该考虑对代码进行修改,转向使用浮点数数学库。典型情况下,基于硬件的浮点数计算比对应的基于软件的定点数计算快得多。

重要提示:当然,如果您的代码确实广泛地使用浮点数计算,请记住不要使用-mthumb选项来编译代码。Thumb选项可以减少代码模块的尺寸,但是也会降低浮点计算代码的性能。

减少电力消耗

移动设备的电力消耗一直是个问题。iPhone OS的电能管理系统保持电能的方法是关闭当前未被使用的硬件功能。此外,要避免CPU密集型和高图形帧率的操作。您可以通过优化如下组件的使用来提高电池的寿命:

  • CPU

  • Wi-Fi和基带(EDGE, 3G)无线信号

  • Core Location框架

  • 加速计

  • 磁盘

您的优化目标应该是以尽可能有效的方式完成大多数的工作。您应该总是采用Instruments和Shark工具对应用程序的算法进行优化。但是,很重要的一点是,即使最优化的算法也可能对设备的电池寿命造成负面的影响。因此,在写代码的时候应该考虑如下的原则:

  • 避免需要轮询的工作,因为轮询会阻止CPU进入休眠状态。您可以通过NSRunLoop或者NSTimer类来规划需要做的工作,而不是使用轮询。

  • 尽一切可能使共享的UIApplication对象的idleTimerDisabled属性值保持为NO。当设备处于不活动状态一段时间后,空闲定时器会关闭设备的屏幕。如果您的应用程序不需要设备屏幕保持打开状态,就让系统将它关闭。如果关闭屏幕给您的应用程序的体验带来负面影响,则需要通过修改代码来消除那些影响,而不是不必要地关闭空闲定时器。

  • 尽可能将任务合并在一起,以便使空闲时间最大化。每隔一段时间就间歇性地执行部分任务比一次性完成相同数量的所有任务开销更多的电能。间歇性地执行任务会阻止系统在更长时间内无法关闭硬件。

  • 避免过度访问磁盘。举例来说,如果您需要将状态信息保存在磁盘上,则仅当该状态信息发生变化时才进行保存,或者尽可能将状态变化合并保存,以避免短时间频繁进行磁盘写入操作。

  • 不要使屏幕描画速度比实际需求更快。从电能消耗的角度看,描画的开销很大。不要依赖硬件来压制应用程序的帧率,而是应该根据程序实际需要的帧率来进行帧的描画。

  • 如果你通过UIAccelerometer类来接收常规的加速计事件,则当您不再需要那些事件时,要禁止这些事件。类似地,请将事件传送的频率设置为满足应用程序需要的最小值。更多信息请参见“访问加速计事件”部分。

您向网络传递的数据越多,就需要越多的电能来进行无线发射。事实上,访问网络是您所能进行的最耗电的操作,您应该遵循下面的原则,使网络访问最小化:

  • 仅在需要的时候连接外部网络,不要对服务器进行轮询。

  • 当您需要连接网络时,请仅传递完成工作所需要的最少数据。请使用紧凑的数据格式,不要包含可被简单忽略的额外数据。

  • 尽可能快地以群发(in burst)方式传递数据包,而不是拉长数据传输的时间。当系统检测到设备没有活动时,就会关闭Wi-Fi和蜂窝无线信号。您的应用程序以较长时间传输数据比以较短时间传输同样数量的数据要消耗更多的电能。

  • 尽可能通过Wi-Fi无线信号连接网络。Wi-Fi耗电比基带无线少,是推荐的方式。

  • 如果您通过Core Location框架收集位置数据,则请尽可能快地禁止位置更新,以及将位置过滤器和精度水平设置为恰当的值。Core Location通过可用的GPS、蜂窝、和Wi-Fi网络来确定用户的位置。虽然Core Location已经努力使无线信号的使用最小化了,但是,设置恰当的精度和过滤器的值可以使Core Location在不需要位置服务的时候完全关闭硬件。更多信息请参见“获取用户的当前位置”部分。

代码的优化

和iPhone OS一起推出的还有几个应用程序的优化工具。它们中的大部分都运行在Mac OS X上,适合于调整运行在仿真器上的代码的某些方面。举例来说,您可以通过仿真器来消除内存泄露,确保总的内存开销尽可能小。借助这些工具,您还可以排除代码中可能由低效算法或已知瓶颈引起的计算热点。

在仿真器上进行代码优化之后,还应该在设备上用Instruments程序进行进一步优化。在实际设备上运行代码是对其进行完全优化的唯一方式。因为仿真器运行在Mac OS X上,而运行Mac OS X的系统具有更快的CPU和更多的可用内存,所以其性能通常比实际设备的性能好很多。在实际设备上用Instruments跟踪代码可能会发现额外的性能瓶颈,您需要进行优化。

更多有关Instruments的使用信息,请参见Instruments用户指南

窗口和视图

窗口和视图是为iPhone应用程序构造用户界面的可视组件。窗口为内容显示提供背景平台,而视图负责绝大部分的内容描画,并负责响应用户的交互。虽然本章讨论的概念和窗口及视图都相关联,但是讨论过程更加关注视图,因为视图对系统更为重要。

视图对iPhone应用程序是如此的重要,以至于在一个章节中讨论视图的所有方面是不可能的。本章将关注窗口和视图的基本属性、各个属性之间的关系、以及在应用程序中如何创建和操作这些属性。本章不讨论视图如何响应触摸事件或如何描画定制内容,有关那些主题的更多信息,请分别参见“事件处理”“图形和描画”部分。

什么是窗口和视图?

和Mac OS X一样,iPhone OS通过窗口和视图在屏幕上展现图形内容。虽然窗口和视图对象之间在两个平台上有很多相似性,但是具体到每个平台上,它们的作用都有轻微的差别。

UIWindow的作用

和Mac OS X的应用程序有所不同,iPhone应用程序通常只有一个窗口,表示为一个UIWindow类的实例。您的应用程序在启动时创建这个窗口(或者从nib文件进行装载),并往窗口中加入一或多个视图,然后将它显示出来。窗口显示出来之后,您很少需要再次引用它。

在iPhone OS中,窗口对象并没有像关闭框或标题栏这样的视觉装饰,用户不能直接对其进行关闭或其它操作。所有对窗口的操作都需要通过其编程接口来实现。应用程序可以借助窗口对象来进行事件传递。窗口对象会持续跟踪当前的第一响应者对象,并在UIApplication对象提出请求时将事件传递它。

还有一件可能让有经验的Mac OS X开发者觉得奇怪的事是UIWindow类的继承关系。在Mac OS X中,NSWindow的父类是NSResponder;而在iPhone OS中,UIWindow的父类是UIView。因此,窗口在iPhone OS中也是一个视图对象。不管其起源如何,您通常可以将iPhone OS上的窗口和Mac OS X的窗口同样对待。也就是说,您通常不必直接操作UIWindow对象中与视图有关的属性变量

在创建应用程序窗口时,您应该总是将其初始的边框尺寸设置为整个屏幕的大小。如果您的窗口是从nib文件装载得到,Interface Builder并不允许创建比屏幕尺寸小的窗口;然而,如果您的窗口是通过编程方式创建的,则必须在创建时传入期望的边框矩形。除了屏幕矩形之外,没有理由传入其它边框矩形。屏幕矩形可以通过UIScreen对象来取得,具体代码如下所示:

UIWindow* aWindow = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

虽然iPhone OS支持将一个窗口叠放在其它窗口的上方,但是您的应用程序永远不应创建多个窗口。系统自身使用额外的窗口来显示系统状态条、重要的警告、以及位于应用程序窗口上方的其它消息。如果您希望在自己的内容上方显示警告,可以使用UIKit提供的警告视图,而不应创建额外的窗口。

UIView是作用

视图UIView类的实例,负责在屏幕上定义一个矩形区域。在iPhone的应用程序中,视图在展示用户界面及响应用户界面交互方面发挥关键作用。每个视图对象都要负责渲染视图矩形区域中的内容,并响应该区域中发生的触碰事件。这一双重行为意味着视图是应用程序与用户交互的重要机制。在一个基于模型-视图-控制器的应用程序中,视图对象明显属于视图部分。

除了显示内容和处理事件之外,视图还可以用于管理一或多个子视图。子视图是指嵌入到另一视图对象边框内部的视图对象,而被嵌入的视图则被称为父视图或超视图。视图的这种布局方式被称为视图层次,一个视图可以包含任意数量的子视图,通过为子视图添加子视图的方式,视图可以实现任意深度的嵌套。视图在视图层次中的组织方式决定了在屏幕上显示的内容,原因是子视图总是被显示在其父视图的上方;这个组织方法还决定了视图如何响应事件和变化。每个父视图都负责管理其直接的子视图,即根据需要调整它们的位置和尺寸,以及响应它们没有处理的事件。

由于视图对象是应用程序和用户交互的主要途径,所以需要在很多方面发挥作用,下面是其中的一小部分:

  • 描画和动画

    • 视图负责对其所属的矩形区域进行描画。

    • 某些视图属性变量可以以动画的形式过渡到新的值。

  • 布局和子视图管理

    • 视图管理着一个子视图列表。

    • 视图定义了自身相对于其父视图的尺寸调整行为。

    • 必要时,视图可以通过代码调整其子视图的尺寸和位置。

    • 视图可以将其坐标系统下的点转换为其它视图或窗口坐标系统下的点。

  • 事件处理

    • 视图可以接收触摸事件。

    • 视图是响应者链的参与者。

在iPhone应用程序中,视图和视图控制器紧密协作,管理若干方面的视图行为。视图控制器的作用是处理视图的装载与卸载、处理由于设备旋转导致的界面旋转,以及和用于构建复杂用户界面的高级导航对象进行交互。更多这方面的信息请参见“视图控制器的作用”部分。

本章的大部分内容都着眼于解释视图的这些作用,以及说明如何将您自己的定制代码关联到现有的UIView行为中。

UIKit的视图类

UIView类定义了视图的基本行为,但并不定义其视觉表示。相反,UIKit通过其子类来为像文本框、按键、及工具条这样的标准界面元素定义具体的外观和行为。图2-1显示了所有UIKit视图类的层次框图。除了UIViewUIControl类是例外,这个框图中的大多数视图都设计为可直接使用,或者和委托对象结合使用。

图2-1  视图的类层次

View class hierarchy

这个视图层次可以分为如下几个大类:

  • 容器

    容器视图用于增强其它视图的功能,或者为视图内容提供额外的视觉分隔。比如,UIScrollView类可以用于显示因内容太大而无法显示在一个屏幕上的视图。UITableView类是UIScrollView类的子类,用于管理数据列表。表格的行可以支持选择,所以通常也用于层次数据的导航—比如用于挖掘一组有层次结构的对象。

    UIToolbar对象则是一个特殊类型的容器,用于为一或多个类似于按键的项提供视觉分组。工具条通常出现在屏幕的底部。Safari、Mail、和Photos程序都使用工具条来显示一些按键,这些按键代表经常使用的命令。工具条可以一直显示,也可以根据应用程序的需要进行显示。

  • 控件

    控件用于创建大多数应用程序的用户界面。控件是一种特殊类型的视图,继承自UIControl超类,通常用于显示一个具体的值,并处理修改这个值所需要的所有用户交互。控件通常使用标准的系统范式(比如目标-动作模式和委托模式)来通知应用程序发生了用户交互。控件包括按键、文本框、滑块、和切换开关。

  • 显示视图

    控件和很多其它类型的视图都提供了交互行为,而另外一些视图则只是用于简单地显示信息。具有这种行为的UIKit类包括UIImageViewUILabelUIProgressViewUIActivityIndicatorView

  • 文本和web视图

    文本和web视图为应用程序提供更为高级的显示多行文本的方法。UITextView类支持在滚动区域内显示和编辑多行文本;而UIWebView类则提供了显示HTML内容的方法,通过这个类,您可以将图形和高级的文本格式选项集成到应用程序中,并以定制的方式对内容进行布局。

  • 警告视图和动作表单

    警告视图和动作表单用于即刻取得用户的注意。它们向用户显示一条消息,同时还有一或多个可选的按键,用户通过这些按键来响应消息。警告视图和动作表单的功能类似,但是外观和行为不同。举例来说,UIAlertView类在屏幕上弹出一个蓝色的警告框,而UIActionSheet类则从屏幕的底部滑出动作框。

  • 导航视图

    页签条和导航条和视图控制器结合使用,为用户提供从一个屏幕到另一个屏幕的导航工具。在使用时,您通常不必直接创建UITabBarUINavigationBar的项,而是通过恰当的控制器接口或Interface Builder来对其进行配置。

  • 窗口

    窗口提供一个描画内容的表面,是所有其它视图的根容器。每个应用程序通常都只有一个窗口。更多信息请参见“UIWindow的作用”部分。

除了视图之外,UIKit还提供了视图控制器,用于管理这些对象。更多信息请参见“视图控制器的作用”部分。

视图控制器的作用

运行在iPhone OS上的应用程序在如何组织内容和如何将内容呈现给用户方面有很多选择。含有很多内容的应用程序可以将内容分为多个屏幕。在运行时,每个屏幕的背后都是一组视图对象,负责显示该屏幕的数据。一个屏幕的视图后面是一个视图控制器其作用是管理那些视图上显示的数据,并协调它们和应用程序其它部分的关系。

UIViewController类负责创建其管理的视图及在低内存时将它们从内容中移出。视图控制器还为某些标准的系统行为提供自动响应。比如,在响应设备方向变化时,如果应用程序支持该方向,视图控制器可以对其管理的视图进行尺寸调整,使其适应新的方向。您也可以通过视图控制器来将新的视图以模式框的方式显示在当前视图的上方。

除了基础的UIViewController类之外,UIKit还包含很多高级子类,用于处理平台共有的某些高级接口。特别需要提到的是,导航控制器用于显示多屏具有一定层次结构的内容;而页签条控制器则支持用户在一组不同的屏幕之间切换,每个屏幕都代表应用程序的一种不同的操作模式。

有关如何通过视图控制器管理用户界面上视图的更多信息,请参见iPhone OS的视图控制器编程指南

视图架构和几何属性

由于视图是iPhone应用程序的焦点对象,所以对视图与系统其它部分的交互机制有所了解是很重要的。UIKit中的标准视图类为应用程序免费提供相当数量的行为,还提供了一些定义良好的集成点,您可以通过这些集成点来对标准行为进行定制,完成应用程序需要做的工作。

本文的下面部分将解释视图的标准行为,并说明哪些地方可以集成您的定制代码。如果需要特定类的集成点信息,请参见该类的参考文档。您可以从UIKit框架参考中取得所有类参考文档的列表。

视图交互模型

任何时候,当用户和您的程序界面进行交互、或者您的代码以编程的方式进行某些修改时,UIKit内部都会发生一个复杂的事件序列。在事件序列的一些特定的点上,UIKit会调用您的视图类,使它们有机会代表应用程序进行事件响应。理解这些调用点是很重要的,有助于理解您的视图对象和系统在哪里进行结合。图2-2显示了从用户触击屏幕到图形系统更新屏幕内容这一过程的基本事件序列。以编程方式触发事件的基本步骤与此相同,只是没有最初的用户交互。

图2-2  UIKit和您的视图对象之间的交互

UIKit interactions with your view objects

下面的步骤说明进一步刨析了图2-2中的事件序列,解释了序列的每个阶段都发生了什么,以及应用程序可能如何进行响应。

  1. 用户触击屏幕。

  2. 硬件将触击事件报告给UIKit框架。

  3. UIKit框架将触击信息封装为一个UIEvent对象,并派发给恰当的视图(有关UIKit如何将事件递送给您的视图的详细解释,请参见“事件的传递”部分)。

  4. 视图的事件处理方法可以通过下面的方式来响应事件:

    • 调整视图或其子视图的属性变量(边框、边界、透明度等)。

    • 将视图(或其子视图)标识为需要修改布局。

    • 将视图(或其子视图)标识为布局需要重画。

    • 将数据发生的变化通报给控制器

    当然,上述的哪些事情需要做及调用什么方法来完成是由视图来决定的。

  5. 如果视图被标识为需要重新布局,UIKit就调用视图的layoutSubviews方法。

    您可以在自己的定制视图中重载这个方法,以便调整子视图的尺寸和位置。举例来说,如果一个视图具有很大的滚动区域,就需要使用几个子视图来“平铺”,而不是创建一个内存很可能装不下的大视图。在这个方法的实现中,视图可以隐藏所有不需显示在屏幕上的子视图,或者在重新定位之后将它们用于显示新的内容。作为这个过程的一部分,视图也可以将用于“平铺”的子视图标识为需要重画。

  6. 如果视图的任何部分被标识为需要重画,UIKit就调用该视图的drawRect:方法。

    UIKit只对那些需要重画的视图调用这个方法。在这个方法的实现中,所有视图都应该尽可能快地重画指定的区域,且都应该只重画自己的内容,不应该描画子视图的内容。在这个调用点上,视图不应该尝试进一步改变其属性或布局。

  7. 所有更新过的视图都和其它可视内容进行合成,然后发送给图形硬件进行显示。

  8. 图形硬件将渲染完成的内容转移到屏幕。

请注意:上述的更新模型主要适用于采纳内置视图和描画技术的应用程序。如果您的应用程序使用OpenGL ES来描画内容,则通常要配置一个全屏的视图,然后直接在OpenGL的图形上下文中进行描画。您的视图仍然需要处理触碰事件,但不需要对子视图进行布局或者实现drawRect:方法。有关OpenGL ES的更多信息,请参见“用OpenGL ES进行描画”部分。

基于上述的步骤说明可以看出,UIKit为您自己定制的视图提供如下主要的结合点:

  1. 下面这些事件处理方法:

  2. layoutSubviews方法

  3. drawRect:方法

大多数定制视图通过实现这些方法来得到自己期望的行为。您可能不需要重载所有方法,举例来说,如果您实现的视图是固定尺寸的,则可能不需要重载layoutSubviews方法。类似地,如果您实现的视图只是显示简单的内容,比如文本或图像,则通常可以通过简单地嵌入UIImageViewUILabel对象作为子视图来避免描画。

重要的是要记住,这些是主要的结合点,但不是全部。UIView类中有几个方法的设计目的就是让子类重载的。您可以通过查阅UIView类参考中的描述来了解哪些方法可以被重载。

视图渲染架构

虽然您通过视图来表示屏幕上的内容,但是UIView类自身的很多基础行为却严重依赖于另一个对象。UIKit中每个视图对象的背后都有一个Core Animation层对象,它是一个CALayer类的实例,该类为视图内容的布局和渲染、以及合成和动画提供基础性的支持。

和Mac OS X(在这个平台上Core Animation支持是可选的)不同的是,iPhone OS将Core Animation集成到视图渲染实现的核心。虽然Core Animation发挥核心作用,但是UIKit在Core Animation上面提供一个透明的接口层,使编程体验更为流畅。这个透明的接口使开发者在大多数情况下不必直接访问Core Animation的层,而是通过UIView的方法和属性声明取得类似的行为。然而,当UIView类没有提供您需要的接口时,Core Animation就变得重要了,在那种情况下,您可以深入到Core Animation层,在应用程序中实现一些复杂的渲染。

本文的下面部分将介绍Core Animation技术,描述它通过UIView类为您提供的一些功能。有关如何使用Core Animation进行高级渲染的更多信息,请参见Core Animation编程指南

Core Animation基础

Core Animation利用了硬件加速和架构上的优化来实现快速渲染和实时动画。当视图的drawRect:方法首次被调用时,层会将描画的结果捕捉到一个位图中,并在随后的重画中尽可能使用这个缓存的位图,以避免调用开销很大的drawRect:方法。这个过程使Core Animation得以优化合成操作,取得期望的性能。

Core Animation把和视图对象相关联的层存储在一个被称为层树的层次结构中。和视图一样,层树中的每个层都只有一个父亲,但可以嵌入任意数量的子层。缺省情况下,层树中对象的组织方式和视图在视图层次中的组织方式完全一样。但是,您可以在层树中添加层,而不同时添加相应的视图。当您希望实现某种特殊的视觉效果、而又不需要在视图上保持这种效果时,就可能需要这种技术。

实际上,层对象是iPhone OS渲染和布局系统的推动力,大多数视图属性实际上是其层对象属性的一个很薄的封装。当您(直接使用CALayer对象)修改层树上层对象的属性时,您所做的改变会立即反映在层对象上。但是,如果该变化触发了相应的动画,则可能不会立即反映在屏幕上,而是必须随着时间的变化以动画的形式表现在屏幕上。为了管理这种类型的动画,Core Animation额外维护两组层对象,我们称之为表示树渲染树

表示树反映的是层在展示给用户时的当前状态。假定您对层值的变化实行动画,则在动画开始时,表示层反映的是老的值;随着动画的进行,Core Animation会根据动画的当前帧来更新表示树层的值;然后,渲染树就和表示树一起,将变化渲染在屏幕上。由于渲染树运行在单独的进程或线程上,所以它所做的工作并不影响应用程序的主运行循环。虽然层树和表示树都是公开的,但是渲染树的接口是私有。

在视图后面设置层对象对描画代码的性能有很多重要的影响。使用层的好处在于视图的大多数几何变化都不需要重画。举例来说,改变视图的位置和尺寸并需要重画视图的内容,只需简单地重用层缓存的位图就可以了。对缓存的内容实行动画比每次都重画内容要有效得多。

使用层的缺点在于层是额外的缓存数据,会增加应用程序的内存压力。如果您的应用程序创建太多的视图,或者创建多个很大的视图,则可能很快就会出现内存不够用的情形。您不用担心在应用程序中使用视图,但是,如果有现成的视图可以重用,就不要创建新的视图对象。换句话说,您应该设法使内存中同时存在的视图对象数量最小。

有关Core Animation的进一步概述、对象树、以及如何创建动画,请参见Core Animation编程指南

改变视图的层

在iPhone OS系统中,由于视图必须有一个与之关联的层对象,所以UIView类在初始化时会自动创建相应的层。您可以通过视图的layer属性访问这个层,但是不能在视图创建完成后改变层对象。

如果您希望视图使用不同类型的层,必须重载其layerClass类方法,并在该方法中返回您希望使用的层对象。使用不同层类的最常见理由是为了实现一个基于OpenGL的应用程序。为了使用OpenGL描画命令,视图下面的层必须是CAEAGLLayer类的实例,这种类型的层可以和OpenGL渲染调用进行交互,最终在屏幕上显示期望的内容。

重要提示:您永远不应修改视图层的delegate属性,该属性用于存储一个指向视图的指针,应该被认为是私有的。类似地,由于一个视图只能作为一个层的委托,所以您必须避免将它作为其它层对象的委托,否则会导致应用程序崩溃。

动画支持

iPhone OS的每个视图后面都有一个层对象,这样做的好处之一是使视图内容更加易于实现动画。请记住,动画并不一定是为了在视觉上吸引眼球,它可以将应用程序界面变化的上下文呈现给用户。举例来说,当您在屏幕转移过程中使用过渡时,过渡本身就向用户指示屏幕之间的联系。系统自动支持了很多经常使用的动画,但您也可以为界面上的其它部分创建动画。

UIView类的很多属性都被设计为可动画的(animatable)。可动画的属性是指当属性从一个值变为另一个值的时候,可以半自动地支持动画。您仍然必须告诉UIKit希望执行什么类型的动画,但是动画一旦开始,Core Animation就会全权负责。UIView对象中支持动画的属性有如下几个:

虽然其它的视图属性不直接支持动画,但是您可以为其中的一部分显式创建动画。显式动画要求您做很多管理动画和渲染内容的工作,通过使用Core Animation提供的基础设施,这些工作仍然可以得到良好的性能。

有关如何通过UIView类创建动画的更多信息,请参见“实现视图动画”部分;有关如何创建显式动画的更多信息,则请参见Core Animation编程指南

视图坐标系统

UIKit中的坐标是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向右为坐标轴正向。坐标值由浮点数来表示,内容的布局和定位因此具有更高的精度,还可以支持与分辨率无关的特性。图2-3显示了这个相对于屏幕的坐标系统,这个坐标系统同时也用于UIWindowUIView类。视图坐标系统的方向和Quartz及Mac OS X使用的缺省方向不同,选择这个特殊的方向是为了使布局用户界面上的控件及内容更加容易。

图2-3  视图坐标系统

View coordinate system

您在编写界面代码时,需要知道当前起作用的坐标系统。每个窗口和视图对象都维护一个自己本地的坐标系统。视图中发生的所有描画都是相对于视图本地的坐标系统。但是,每个视图的边框矩形都是通过其父视图的坐标系统来指定,而事件对象携带的坐标信息则是相对于应用程序窗口的坐标系统。为了方便,UIWindowUIView类都提供了一些方法,用于在不同对象之间进行坐标系统的转换。

虽然Quartz使用的坐标系统不以左上角为原点,但是对于很多Quartz调用来说,这并不是问题。在调用视图的drawRect:方法之前,UIKit会自动对描画环境进行配置,使左上角成为坐标系统的原点,在这个环境中发生的Quartz调用都可以正确地在视图中描画。您唯一需要考虑不同坐标系统之间差别的场合是当您自行通过Quartz建立描画环境的时候。

更多有关坐标系统、Quartz、和描画的一般信息,请参见“图形和描画”部分。

边框、边界、和中心的关系

视图对象通过framebounds、和center属性声明来跟踪自己的大小和位置。frame属性包含一个矩形,即边框矩形,用于指定视图相对于其父视图坐标系统的位置和大小。bounds属性也包含一个矩形,即边界矩形,负责定义视图相对于本地坐标系统的位置和大小。虽然边界矩形的原点通常被设置为 (0, 0),但这并不是必须的。center属性包含边框矩形的中心点

在代码中,您可以将framebounds、和center属性用于不同的目的。边界矩形代表视图本地的坐标系统,因此,在描画和事件处理代码中,经常借助它来取得视图中发生事件或需要更新的位置。中心点代表视图的中心,改变中心点一直是移动视图位置的最好方法。边框矩形是一个通过boundscenter属性计算得到的便利值,只有当视图的变换属性被设置恒等变换时,边框矩形才是有效的。

图2-4显示了边框矩形和边界矩形之间的关系。右边的整个图像是从视图的(0, 0)开始描画的,但是由于边界的大小和整个图像的尺寸不相匹配,所以位于边界矩形之外的图像部分被自动裁剪。在视图和它的父视图进行合成的时候,视图在其父视图中的位置是由视图边框矩形的原点决定的。在这个例子中,该原点是(5, 5)。结果,视图的内容就相对于父视图的原点向下向右移动相应的尺寸。

图2-4  视图的边框和边界之间的关系

Relationship between a view's frame and bounds

如果没有经过变换,视图的位置和大小就由上述三个互相关联的属性决定的。当您在代码中通过initWithFrame:方法创建一个视图对象时,其frame属性就会被设置。该方法同时也将bounds矩形的原点初始化为(0.0, 0.0),大小则和视图的边框相同。然后center属性会被设置为边框的中心点。

虽然您可以分别设置这些属性的值,但是设置其中的一个属性会引起其它属性的改变,具体关系如下:

  • 当您设置frame属性时,bounds属性的大小会被设置为与frame属性的大小相匹配的值,center属性也会被调整为与新的边框中心点相匹配的值。

  • 当您设置center属性时,frame的原点也会随之改变。

  • 当您设置bounds矩形的大小时,frame矩形的大小也会随之改变。

您可以改变bounds的原点而不影响其它两个属性。当您这样做时,视图会显示您标识的图形部分。在图2-4中,边界的原点被设置为(0.0, 0.0)。在图2-5中,该原点被移动到(8.0, 24.0)。结果,显示出来的是视图图像的不同部分。但是,由于边框矩形并没有改变,新的内容在父视图中的位置和之前是一样的。

图2-5  改变视图的边界

Altering a view's bounds

请注意:缺省情况下,视图的边框并不会被父视图的边框裁剪。如果您希望让一个视图裁剪其子视图,需要将其clipsToBounds属性设置为YES

坐标系统变换

在视图的drawRect:方法中常常借助坐标系统变换来进行描画。而在iPhone OS系统中,您还可以用它来实现视图的某些视觉效果。举例来说,UIView类中包含一个transform属性声明,您可以通过它来对整个视图实行各种类型的平移、比例缩放、和变焦缩放效果。缺省情况下,这个属性的值是一个恒等变换,不会改变视图的外观。在加入变换之前,首先要得到该属性中存储的CGAffineTransform结构,用相应的Core Graphics函数实行变换,然后再将修改后的变换结构重新赋值给视图的transform属性。

请注意:当您将变换应用到视图时,所有执行的变换都是相对于视图的中心点。

平移一个视图会使其所有的子视图和视图本身的内容一起移动。由于子视图的坐标系统是继承并建立在这些变化的基础上的,所以比例缩放也会影响子视图的描画。有关如何控制视图内容缩放的更多信息,请参见“内容模式和比例缩放”部分。

重要提示:如果transform属性的值不是恒等变换,则frame属性的值就是未定义的,必须被忽略。在设置变换属性之后,请使用boundscenter属性来获取视图的位置和大小。

有关如何在drawRect:方法中使用变换的信息,请参见“坐标和坐标变换”部分;有关用于修改CGAffineTransform结构的函数,则请参见CGAffineTransform参考

内容模式与比例缩放

当您改变视图的边界,或者将一个比例因子应用到视图的transform属性声明时,边框矩形会发生等量的变化。根据内容模式的不同,视图的内容也可能被缩放或重新定位,以反映上述的变化。视图的contentMode属性决定了边界变化和缩放操作作用到视图上产生的效果。缺省情况下,这个属性的值被设置为UIViewContentModeScaleToFill,意味着视图内容总是被缩放,以适应新的边框尺寸。作为例子,图2-6显示了当视图的水平缩放因子放大一倍时产生的效果。

图2-6 使用scale-to-fill内容模式缩放视图

View scaled using the scale-to-fill content mode

视图内容的缩放仅在首次显示视图的时候发生,渲染后的内容会被缓存在视图下面的层上。当边界或缩放因子发生变化时,UIKit并不强制视图进行重画,而是根据其内容模式决定如何显示缓存的内容。图2-7比较了在不同的内容模式下,改变视图边界或应用不同的比例缩放因子时产生的结果。

图2-7  内容模式比较

Content mode comparisons

对视图应用一个比例缩放因子总是会使其内容发生缩放,而边界的改变在某些内容模式下则不会发生同样的结果。不同的UIViewContentMode常量(比如UIViewContentModeTopUIViewContentModeBottomRight)可以使当前的内容在视图的不同角落或沿着视图的不同边界显示,还有一种模式可以将内容显示在视图的中心。在这些模式的作用下,改变边界矩形只会简单地将现有的视图内容移动到新的边界矩形中对应的位置上。

当您希望在应用程序中实现尺寸可调整的控件时,请务必考虑使用内容模式。这样做可以避免控件的外观发生变形,以及避免编写定制的描画代码。按键和分段控件(segmented control)特别适合基于内容模式的描画。它们通常使用几个图像来创建控件外观。除了有两个固定尺寸的盖帽图像之外,按键可以通过一个可伸展的、宽度只有一个像素的中心图像来实现水平方向的尺寸调整。它将每个图像显示在自己的图像视图中,而将可伸展的中间图像的内容模式设置为UIViewContentModeScaleToFill,使得在尺寸调整时两端的外观不会变形。更为重要的是,每个图像视图的关联图像都可以由Core Animation来缓存,因此不需要编写描画代码就可以支持动画,从而使大大提高了性能。

内容模式通常有助于避免视图内容的描画,但是当您希望对缩放和尺寸调整过程中的视图外观进行特别的控制时,也可以使用UIViewContentModeRedraw模式。将视图的内容模式设置为这个值可以强制Core Animation使视图的内容失效,并调用视图的drawRect:方法,而不是自动进行缩放或尺寸调整。

自动尺寸调整行为

当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以适应原始视图的新尺寸。如果视图的autoresizesSubviews属性声明被设置为YES,则其子视图会根据autoresizingMask属性的值自动进行尺寸调整。简单配置一下视图的自动尺寸调整掩码常常就能使应用程序得到合适的行为;否则,应用程序就必须通过重载layoutSubviews方法来提供自己的实现。

设置视图的自动尺寸调整行为的方法是通过位OR操作符将期望的自动尺寸调整常量连结起来,并将结果赋值给视图的autoresizingMask属性。表2-1列举了自动尺寸调整常量,并描述这些常量如何影响给定视图的尺寸和位置。举例来说,如果要使一个视图和其父视图左下角的相对位置保持不变,可以加入UIViewAutoresizingFlexibleRightMarginUIViewAutoresizingFlexibleTopMargin常量,并将结果赋值给autoresizingMask属性。当同一个轴向有多个部分被设置为可变时,尺寸调整的裕量会被平均分配到各个部分上。

表2-1  自动尺寸调整掩码常量

自动尺寸调整掩码

描述

UIViewAutoresizingNone

这个常量如果被设置,视图将不进行自动尺寸调整。

UIViewAutoresizingFlexibleHeight

这个常量如果被设置,视图的高度将和父视图的高度一起成比例变化。否则,视图的高度将保持不变。

UIViewAutoresizingFlexibleWidth

这个常量如果被设置,视图的宽度将和父视图的宽度一起成比例变化。否则,视图的宽度将保持不变。

UIViewAutoresizingFlexibleLeftMargin

这个常量如果被设置,视图的左边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的左边界的相对位置将保持不变。

UIViewAutoresizingFlexibleRightMargin

这个常量如果被设置,视图的右边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的右边界的相对位置将保持不变。

UIViewAutoresizingFlexibleBottomMargin

这个常量如果被设置,视图的底边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的底边界的相对位置将保持不变。

UIViewAutoresizingFlexibleTopMargin

这个常量如果被设置,视图的上边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的上边界的相对位置将保持不变。

图2-8为这些常量值的位置提供了一个图形表示。如果这些常量之一被省略,则视图在相应方向上的布局就被固定;如果某个常量被包含在掩码中,在该方向的视图布局就就灵活的。

图2-8  视图的自动尺寸调整掩码常量

View autoresizing mask constants

如果您通过Interface Builder配置视图,则可以用Size查看器的Autosizing控制来设置每个视图的自动尺寸调整行为。上图中的灵活宽度及高度常量和Interface Builder中位于同样位置的弹簧具有同样的行为,但是空白常量的行为则是正好相反。换句话说,如果要将灵活右空白的自动尺寸调整行为应用到Interface Builder的某个视图,必须使相应方向空间的Autosizing控制为空,而不是放置一个支柱。幸运的是,Interface Builder通过动画显示了您的修改对视图自动尺寸调整行为的影响。

如果视图的autoresizesSubviews属性被设置为NO,则该视图的直接子视图的所有自动尺寸调整行为将被忽略。类似地,如果一个子视图的自动尺寸调整掩码被设置为UIViewAutoresizingNone,则该子视图的尺寸将不会被调整,因而其直接子视图的尺寸也不会被调整。

请注意:为了使自动尺寸调整的行为正确,视图的transform属性必须设置为恒等变换;其它变换下的尺寸自动调整行为是未定义的。

自动尺寸调整行为可以适合一些布局的要求,但是如果您希望更多地控制视图的布局,可以在适当的视图类中重载layoutSubviews方法。有关视图布局管理的更多信息,请参见“响应布局的变化”部分。

创建和管理视图层次

管理用户界面的视图层次是开发应用程序用户界面的关键部分。视图的组织方式不仅定义了应用程序的视觉外观,而且还定义了应用程序如何响应变化。视图层次中的父-子关系可以帮助我们定义应用程序中负责处理触摸事件的对象链。当用户旋转设备时,父-子关系也有助于定义每个视图的尺寸和位置是如何随着界面方向的变化而变化的。

图2-9显示了一个简单的例子,说明如何通过视图的分层来创建期望的视觉效果。在Clock程序中,页签条和导航条视图,以及定制视图混合在一起,实现了整个界面。

图2-9  Clock程序的视图层

Layered views in the Clock application

如果您探究Clock程序中视图之间的关系,就会发现它们很像“改变视图的层”部分中显示的关系,窗口对象是应用程序的页签条、导航条、和定制视图的根视图。

图2-10  Clock程序的视图层次

View hierarchy for the Clock application

在iPhone应用程序的开发过程中,有几种建立视图层次的方法,包括基于Interface Builder的可视化方法和通过代码编程的方法。本文的下面部分将向您介绍如何装配视图层次,以及如何在建立视图层次之后寻找其中的视图,还有如何在不同的视图坐标系统之间进行转换。

创建一个视图对象

创建视图对象的最简单方法是使用Interface Builder进行制作,然后将视图对象从作成的nib文件载入内存。在Interface Builder的图形环境中,您可以将新的视图从库中拖出,然后放到窗口或另一个视图中,以快速建立需要的视图层次。Interface Builder使用的是活的视图对象,因此,当您用这个图形环境构建用户界面时,所看到的就是运行时装载的外观,而且不需要为视图层次中的每个视图编写单调乏味的内存分配和初始化代码。

如果您不喜欢Interface Builder和nib文件,也可以通过代码来创建视图。创建一个新的视图对象时,需要为其分配内存,并向该对象发送一个initWithFrame:消息,以对其进行初始化。举例来说,如果您要创建一个新的UIView类的实例作为其它视图的容器,则可以使用下面的代码:

CGRect  viewRect = CGRectMake(0, 0, 100, 100);
UIView* myView = [[UIView alloc] initWithFrame:viewRect];

请注意:虽然所有系统提供的视图对象都支持initWithFrame:消息,但是其中的一部分可能有自己偏好的初始化方法,您应该使用那些方法。有关定制初始化方法的更多信息,请参见相应的类参考文档。

您在视图初始化时指定的边框矩形代表该视图相对于未来父视图的位置和大小。在将视图显示于屏幕上之前,您需要将它加入到窗口或其它视图中。在这个时候,UIKit会根据您指定的边框矩形将视图放置到其父视图的相应位置中。有关如何将视图添加到视图层次的信息,请参见“添加和移除子视图”部分。

添加和移除子视图

Interface Builder是建立视图层次的最便利工具,因为它可以让您看到视图在运行时的外观。在界面制作完成后,它将视图对象及其层次关系保存在nib文件中。在运行时,系统会按照nib文件的内容为应用程序重新创建那些对象和关系。当一个nib文件被装载时,系统会自动调用重建视图层次所需要的UIView方法。

如果您不喜欢通过Interface Builder和nib文件来创建视图层次,则可以通过代码来创建。如果一个视图必须具有某些子视图才能工作,则应该在其initWithFrame:方法中进行对其创建,以确保子视图可以和视图一起被显示和初始化。如果子视图是应用程序设计的一部分(而不是视图工作必需的),则应该在视图的初始化代码之外进行创建。在iPhone程序中,有两个地方最常用于创建视图和子视图,它们是应用程序委托对象的applicationDidFinishLaunching:方法和视图控制器loadView方法。

您可以通过下面的方法来操作视图层次中的视图对象:

  • 调用父视图的addSubview:方法来添加视图,该方法将一个视图添加到子视图列表的最后。

  • 调用父视图的insertSubview:...方法可以在父视图的子视图列表中间插入视图。

  • 调用父视图的bringSubviewToFront:sendSubviewToBack:、或exchangeSubviewAtIndex:withSubviewAtIndex:方法可以对父视图的子视图进行重新排序。使用这些方法比从父视图中移除子视图并再次插入要快一些。

  • 调用子视图(而不是父视图)的removeFromSuperview方法可以将子视图从父视图中移除。

在添加子视图时,UIKit会根据子视图的当前边框矩形确定其在父视图中的初始位置。您可以随时通过修改子视图的frame属性声明来改变其位置。缺省情况下,边框位于父视图可视边界外部的子视图不会被裁剪。如果您希望激活裁剪功能,必须将父视图的clipsToBounds属性设置为YES

程序清单2-1显示了一个应用程序委托对象的applicationDidFinishLaunching:方法示例。在这个例子中,应用程序委托在启动时通过代码创建全部的用户界面。界面中包含两个普通的UIView对象,用于显示基本颜色。每个视图都被嵌入到窗口中,窗口也是UIView 的一个子类,因此可以作为父视图。父视图会保持它们的子视图,因此这个方法释放了新创建的视图对象,以避免重复保持。

程序清单2-1  创建一个带有视图的窗口

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // Create the window object and assign it to the
    // window instance variable of the application delegate.
    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    window.backgroundColor = [UIColor whiteColor];
 
    // Create a simple red square
    CGRect redFrame = CGRectMake(10, 10, 100, 100);
    UIView *redView = [[UIView alloc] initWithFrame:redFrame];
    redView.backgroundColor = [UIColor redColor];
 
    // Create a simple blue square
    CGRect blueFrame = CGRectMake(10, 150, 100, 100);
    UIView *blueView = [[UIView alloc] initWithFrame:blueFrame];
    blueView.backgroundColor = [UIColor blueColor];
 
    // Add the square views to the window
    [window addSubview:redView];
    [window addSubview:blueView];
 
    // Once added to the window, release the views to avoid the
    // extra retain count on each of them.
    [redView release];
    [blueView release];
 
    // Show the window.
    [window makeKeyAndVisible];
}

重要提示:在内存管理方面,可以将子视图考虑为其它的集合对象。特别是当您通过addSubview:方法将一个视图作为子视图插入时,父视图会对其进行保持操作。反过来,当您通过removeFromSuperview方法将子视图从父视图移走时,子视图会被自动释放。在将视图加入视图层次之后释放该对象可以避免多余的保持操作,从而避免内存泄露。

有关Cocoa内存管理约定的更多信息,请参见Cocoa内存管理编程指南

当您为某个视图添加子视图时,UIKit会向相应的父子视图发送几个消息,通知它们当前发生的状态变化。您可以在自己的定制视图中对诸如willMoveToSuperview:willMoveToWindow:willRemoveSubview:didAddSubview:didMoveToSuperview、和didMoveToWindow这样的方法进行重载,以便在事件发生的前后进行必要的处理,并根据发生的变化更新视图的状态信息。

在视图层次建立之后,您可以通过视图的superview属性来取得其父视图,或者通过subviews属性取得视图的子视图。您也可以通过isDescendantOfView:方法来判定一个视图是否在其父视图的视图层中。一个视图层次的根视图没有父视图,因此其superview属性被设置为nil。对于当前被显示在屏幕上的视图,窗口对象通常是整个视图层次的根视图。

您可以通过视图的window属性来取得指向其父窗口(如果有的话)的指针,如果视图还没有被链接到窗口上,则该属性会被设置为nil

视图层次中的坐标转换

很多时候,特别是处理事件的时候,应用程序可能需要将一个相对于某边框的坐标值转换为相对于另一个边框的值。例如,触摸事件通常使用基于窗口指标系统的坐标值来报告事件发生的位置,但是视图对象需要的是相对于视图本地坐标的位置信息,两者可能是不一样的。UIView类定义了下面这些方法,用于在不同的视图本地坐标系统之间进行坐标转换:

convert...:fromView:方法将指定视图的坐标值转换为视图本地坐标系统的坐标值;convert...:toView:方法则将视图本地坐标系统的坐标值转换为指定视图坐标系统的坐标值。如果传入nil作为视图引用参数的值,则上面这些方法会将视图所在窗口的坐标系统作为转换的源或目标坐标系统。

除了UIView的转换方法之外,UIWindow类也定义了几个转换方法。这些方法和UIView的版本类似,只是UIView定义的方法将视图本地坐标系统作为转换的源或目标坐标系统,而UIWindow的版本则使用窗口坐标系统。

当参与转换的视图没有被旋转,或者被转换的对象仅仅是点的时候,坐标转换相当直接。如果是在旋转之后的视图之间转换矩形或尺寸数据,则其几何结构必须经过合理的改变,才能得到正确的结果坐标。在对矩形结构进行转换时,UIView类假定您希望保证原来的屏幕区域被覆盖,因此转换后的矩形会被放大,其结果是使放大后的矩形(如果放在对应的视图中)可以完全覆盖原来的矩形区域。图2-11显示了将rotatedView对象的坐标系统中的矩形转换到其超类(outerView)坐标系统的结果。

图2-11  对旋转后视图中的值进行转换

Converting values in a rotated view

对于尺寸信息,UIView简单地将它处理为分别相对于源视图和目标视图(0.0, 0.0)点的偏移量。虽然偏移量保持不变,但是相对于坐标轴的差额会随着视图的旋转而移动。在转换尺寸数据时,UIKit总是返回正的数值。

标识视图

UIView类中包含一个tag属性。借助这个属性,您可以通过一个整数值来标识一个视图对象。您可以通过这个属性来唯一标识视图层次中的视图,以及在运行时进行视图的检索(基于tag标识的检索比您自行遍历视图层次要快)。tag属性的缺省值为0

您可以通过UIViewviewWithTag:方法来检索标识过的视图。该方法从消息的接收者自身开始,通过深度优先的方法来检索接收者的子视图。

在运行时修改视图

应用程序在接收用户输入时,需要通过调整自己的用户界面来进行响应。应用程序可能重新排列界面上的视图、刷新屏幕上模型数据已被改变的视图、或者装载一组全新的视图。在决定使用哪种技术时,要考虑您的用户界面,以及您希望实现什么。但是,如何初始化这些技术对于所有应用程序都是一样的。本章的下面部分将描述这些技术,以及如何通过这些技术在运行时更新您的用户界面。

请注意:如果您需要了解UIKit如何在框架内部和您的定制代码之间转移事件和消息的背景信息,请在继续阅读本文之前查阅“视图交互模型”部分。

实现视图动画

动画为用户界面在不同状态之间的迁移过程提供流畅的视觉效果。在iPhone OS中,动画被广泛用于视图的位置调整、尺寸变化、甚至是alpha值的变化(以实现淡入淡出的效果)。动画支持对于制作易于使用的应用程序是至关重要的,因此,UIKit直接将它集成到UIView类中,以简化动画的创建过程。

UIView类定义了几个内在支持动画属性声明—也就是说,当这些属性值发生变化时,视图为其变化过程提供内建的动画支持。虽然执行动画所需要的工作由UIView类自动完成,但您仍然必须在希望执行动画时通知视图。为此,您需要将改变给定属性的代码包装在一个动画块中。

动画块从调用UIViewbeginAnimations:context:类方法开始,而以调用commitAnimations类方法作为结束。在这两个调用之间,您可以配置动画的参数和改变希望实行动画的属性值。一旦调用commitAnimations方法,UIKit就会开始执行动画,即把给定属性从当前值到新值的变化过程用动画表现出来。动画块可以被嵌套,但是在最外层的动画块提交之前,被嵌套的动画不会被执行。

表2-2列举了UIView类中支持动画的属性。

表2-2  支持动画的属性

属性

描述

frame

视图的边框矩形,位于父视图的坐标系中。

bounds

视图的边界矩形,位于视图的坐标系中。

center

边框的中心,位于父视图的坐标系中。

transform

视图上的转换矩阵,相对于视图边界的中心。

alpha

视图的alpha值,用于确定视图的透明度。

配置动画的参数

除了在动画块中改变属性值之外,您还可以对其它参数进行配置,以确定您希望得到的动画行为。为此,您可以调用下面这些UIView的类方法:

  • setAnimationStartDate:方法来设置动画在commitAnimations方法返回之后的发生日期。缺省行为是使动画立即在动画线程中执行。

  • setAnimationDelay:方法来设置实际发生动画和commitAnimations方法返回的时间点之间的间隔。

  • setAnimationDuration:方法来设置动画持续的秒数。

  • setAnimationCurve:方法来设置动画过程的相对速度,比如动画可能在启示阶段逐渐加速,而在结束阶段逐渐减速,或者整个过程都保持相同的速度。

  • setAnimationRepeatCount:方法来设置动画的重复次数。

  • setAnimationRepeatAutoreverses:方法来指定动画在到达目标值时是否自动反向播放。您可以结合使用这个方法和setAnimationRepeatCount:方法,使各个属性在初始值和目标值之间平滑切换一段时间。

commitAnimations类方法在调用之后和动画开始之前立刻返回。UIKit在一个独立的、和应用程序的主事件循环分离的线程中执行动画。commitAnimations方法将动画发送到该线程,然后动画就进入线程中的队列,直到被执行。缺省情况下,只有在当前正在运行的动画块执行完成后,Core Animation才会启动队列中的动画。但是,您可以通过向动画块中的setAnimationBeginsFromCurrentState:类方法传入YES来重载这个行为,使动画立即启动。这样做会停止当前正在执行的动画,而使新动画在当前状态下开始执行。

缺省情况下,所有支持动画的属性在动画块中发生的变化都会形成动画。如果您希望让动画块中发生的某些变化不产生动画效果,可以通过setAnimationsEnabled:方法来暂时禁止动画,在完成修改后才重新激活动画。在调用setAnimationsEnabled:方法并传入NO值之后,所有的改变都不会产生动画效果,直到用YES值再次调用这个方法或者提交整个动画块时,动画才会恢复。您可以用areAnimationsEnabled方法来确定当前是否激活动画。

配置动画的委托

您可以为动画块分配一个委托,并通过该委托接收动画开始和结束的消息。当您需要在动画开始前和结束后立即执行其它任务时,可能就需要这样做。您可以通过UIViewsetAnimationDelegate:类方法来设置委托,并通过setAnimationWillStartSelector:setAnimationDidStopSelector:方法来指定接收消息的选择器方法。消息处理方法的形式如下:

- (void)animationWillStart:(NSString *)animationID context:(void *)context;
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;

上面两个方法的animationIDcontext参数和动画块开始时传给beginAnimations:context:方法的参数相同:

  • animationID - 应用程序提供的字符串,用于标识一个动画块中的动画。

  • context - 也是应用程序提供的对象,用于向委托对象传递额外的信息。

setAnimationDidStopSelector:选择器方法还有一个参数—即一个布尔值。如果动画顺利完成,没有被其它动画取消或停止,则该值为YES

响应布局的变化

任何时候,当视图的布局发生改变时,UIKit会激活每个视图的自动尺寸调整行为,然后调用各自的layoutSubviews方法,使您有机会进一步调整子视图的几何尺寸。下面列举的情形都会引起视图布局的变化:

  • 视图边界矩形的尺寸发生变化。

  • 滚动视图的内容偏移量—也就是可视内容区域的原点—发生变化。

  • 和视图关联的转换矩阵发生变化。

  • 和视图层相关联的Core Animation子层组发生变化。

  • 您的应用程序调用视图的setNeedsLayoutlayoutIfNeeded方法来强制进行布局。

  • 您的应用程序调用视图背后的层对象的setNeedsLayout方法来强制进行布局。

子视图的初始布局由视图的自动尺寸调整行为来负责。应用这些行为可以保证您的视图接近其设计的尺寸。有关自动尺寸调整行为如何影响视图的尺寸和位置的更多信息,请参见“自动尺寸调整行为”部分。

有些时候,您可能希望通过layoutSubviews方法来手工调整子视图的布局,而不是完全依赖自动尺寸调整行为。举例来说,如果您要实现一个由几个子视图元素组成的定制控件,则可以通过手工调整子视图来精确控制控件在一定尺寸范围内的外观。还有,如果一个视图表示的滚动内容区域很大,可以选择将内容显示为一组平铺的子视图,在滚动过程中,可以回收离开屏幕边界的视图,并在填充新内容后将它重新定位,使它成为下一个滚入屏幕的视图。

请注意:您也可以用layoutSubviews方法来调整作为子层链接到视图层的定制CALayer对象。您可以通过对隐藏在视图后面的层层次进行管理,实现直接基于Core Animation的高级动画。有关如何通过Core Animation管理层层次的更多信息,请参见Core Animation编程指南

在编写布局代码时,请务必在应用程序支持的每个方向上都进行测试。对于同时支持景观方向和肖像方向的应用程序,必须确认其是否能正确处理两个方向上的布局。类似地,您的应用程序应该做好处理其它系统变化的准备,比如状态条高度的变化,如果用户在使用您的应用程序的同时接听电话,然后再挂断,就会发生这种变化。在挂断时,负责管理视图的视图控制器可能会调整视图的尺寸,以适应缩小的状态条。之后,这样的变化会向下渗透到应用程序的其它视图。

重画视图的内容

有些时候,应用程序数据模型的变化会影响到相应的用户界面。为了反映这些变化,您可以将相应的视图标识为需要刷新(通过调用setNeedsDisplaysetNeedsDisplayInRect:方法)。和简单创建一个图形上下文并进行描画相比,将视图标识为需要刷新的方法使系统有机会更有效地执行描画操作。举例来说,如果您在某个运行周期中将一个视图的几个区域标识为需要刷新,系统就会将这些需要刷新的区域进行合并,并最终形成一个drawRect:方法的调用。结果,只需要创建一个图形上下文就可以描画所有这些受影响的区域。这个做法比连续快速创建几个图形上下文要有效得多。

实现drawRect:方法的视图总是需要检查传入的矩形参数,并用它来限制描画操作的范围。因为描画是开销相对昂贵的操作,以这种方式来限制描画是提高性能的好方法。

缺省情况下,视图在几何上的变化并不自动导致重画。相反,大多数几何变化都由Core Animation来自动处理。具体来说,当您改变视图的frameboundscenter、或transform属性时,Core Animation会将相应的几何变化应用到与视图层相关联的缓存位图上。在很多情况下,这种方法是完全可以接受的,但是如果您发现结果不是您期望得到的,则可以强制UIKit对视图进行重画。为了避免Core Animation自动处理几何变化,您可以将视图的contentMode属性声明设置为UIViewContentModeRedraw。更多有关内容模式的信息,请参见“内容模式和比例缩放”部分。

隐藏视图

您可以通过改变视图的hidden属性声明来隐藏或显示视图。将这个属性设置为YES会隐藏视图,设置为NO则可以显示视图。对一个视图进行隐藏会同时隐藏其内嵌的所有子视图,就好象它们自己的hidden属性也被设置一样。

当您隐藏一个视图时,该视图仍然会保留在视图层次中,但其内容不会被描画,也不会接收任何触摸事件。由于隐藏视图仍然存在于视图层次中,所以会继续参与自动尺寸调整和其它布局操作。如果被隐藏的视图是当前的第一响应者,则该视图会自动放弃其自动响应者的状态,但目标为第一响应者的事件仍然会传递给隐藏视图。有关响应者链的更多信息,请参见“响应者对象和响应者链”部分。

创建一个定制视图

UIView类为在屏幕上显示内容及处理触摸事件提供了潜在的支持,但是除了在视图区域内描画带有alpha值的背景色之外,UIView类的实例不做其它描画操作,包括其子视图的描画。如果您的应用程序需要显示定制的内容,或以特定的方式处理触摸事件,必须创建UIView的定制子类。

本章的下面部分将描述一些定制视图对象可能需要实现的关键方法和行为。有关子类化的更多信息,请参见UIView类参考

初始化您的定制视图

您定义的每个新的视图对象都应该包含initWithFrame:初始化方法。该方法负责在创建对象时对类进行初始化,使之处于已知的状态。在通过代码创建您的视图实例时,需要使用这个方法。

程序清单2-2显示了标准的initWithFrame:方法的一个框架实现。该实现首先调用继承自超类的实现,然后初始化类的实例变量和状态信息,最后返回初始化完成的对象。您通常需要首先执行超类的实现,以便在出现问题时可以简单地终止自己的初始化代码,返回nil

程序清单2-2  初始化一个视图的子类

- (id)initWithFrame:(CGRect)aRect {
    self = [super initWithFrame:aRect];
    if (self) {
          // setup the initial properties of the view
          ...
       }
    return self;
}

如果您从nib文件中装载定制视图类的实例,则需要知道:在iPhone OS中,装载nib的代码并不通过initWithFrame:方法来实例化新的视图对象,而是通过NSCoding协议定义的initWithCoder:方法来进行。

即使您的视图采纳了NSCoding协议,Interface Builder也不知道它的定制属性,因此不知道如何将那些属性编码到nib文件中。所以,当您从nib文件装载定制视图时,initWithCoder:方法不具有进行正确初始化所需要的信息。为了解决这个问题,您可以在自己的类中实现awakeFromNib方法,特别用于从nib文件装载的定制类。

描画您的视图内容

当您改变视图内容时,可以通过setNeedsDisplaysetNeedsDisplayInRect:方法来将需要重画的部分通知给系统。在应用程序返回运行循环之后,会对所有的描画请求进行合并,计算界面中需要被更新的部分;之后就开始遍历视图层次,向需要更新的视图发送drawRect:消息。遍历的起点是视图层次的根视图,然后从后往前遍历其子视图。在可视边界内显示定制内容的视图必须实现其drawRect:方法,以便对该内容进行渲染。

在调用视图的drawRect:方法之前,UIKit会为其配置描画的环境,即创建一个图形上下文,并调整其坐标系统和裁剪区,使之和视图的坐标系统及边界相匹配。因此,在您的drawRect:方法被调用时,您可以使用UIKit的类和函数、Quartz的函数、或者使用两者相结合的方法来直接进行描画。需要的话,您可以通过UIGraphicsGetCurrentContext函数来取得当前图形上下文的指针,实现对它的访问。

重要提示:只有当定制视图的drawRect:方法被调用的期间,当前图形上下文才是有效的。UIKit可能为该方法的每个调用创建不同的图形上下文,因此,您不应该对该对象进行缓存并在之后使用。

程序清单2-3显示了drawRect:方法的一个简单实现,即在视图边界描画一个10像素宽的红色边界。由于UIKit描画操作的实现也是基于Quartz,所以您可以像下面这样混合使用不同的描画调用来得到期望的结果。

程序清单2-3  一个描画方法

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect    myFrame = self.bounds;
 
    CGContextSetLineWidth(context, 10);
 
    [[UIColor redColor] set];
    UIRectFrame(myFrame);
}

如果您能确定自己的描画代码总是以不透明的内容覆盖整个视图的表面,则可以将视图的opaque属性声明设置为YES,以提高描画代码的总体效率。当您将视图标识为不透明时,UIKit会避免对该视图正下方的内容进行描画。这不仅减少了描画开销的时间,而且减少内容合成需要的工作。然而,只有当您能确定视图提供的内容为不透明时,才能将这个属性设置为YES;如果您不能保证视图内容总是不透明,则应该将它设置为NO

提高描画性能(特别是在滚动过程)的另一个方法是将视图的clearsContextBeforeDrawing属性设置为NO。当这个属性被设置为YES时,UIKIt会在调用drawRect:方法之前,把即将被该方法更新的区域填充为透明的黑色。将这个属性设置为NO可以取消相应的填充操作,而由应用程序负责完全重画传给drawRect:方法的更新矩形中的部分。这样的优化在滚动过程中通常是一个好的折衷。

响应事件

UIView类是UIResponder的一个子类,因此能够接收用户和视图内容交互时产生的触摸事件。触摸事件从发生触摸的视图开始,沿着响应者链进行传递,直到最后被处理。视图本身就是响应者,是响应者链的参与者,因此可以收到所有关联子视图派发给它们的触摸事件。

处理触摸事件的视图通常需要实现下面的所有方法,更多细节请参见“事件处理”部分:

请记住,在缺省情况下,视图每次只响应一个触摸动作。如果用户将第二个手指放在屏幕上,系统会忽略该触摸事件,而不会将它报告给视图对象。如果您希望在视图的事件处理器方法中跟踪多点触摸手势,则需要重新激活多点触摸事件,具体方法是将视图的multipleTouchEnabled属性声明设置为YES

某些视图,比如标签和图像视图,在初始状态下完全禁止事件处理。您可以通过改变视图的userInteractionEnabled属性值来控制视图是否可以对事件进行处理。当某个耗时很长的操作被挂起时,您可以暂时将这个属性设置为NO,使用户无法对视图的内容进行操作。为了阻止事件到达您的视图,还可以使用UIApplication对象的beginIgnoringInteractionEventsendIgnoringInteractionEvents方法。这些方法影响的是整个应用程序的事件分发,而不仅仅是某个视图。

在处理触摸事件时,UIKit会通过UIViewhitTest:withEvent:pointInside:withEvent:方法来确定触摸事件是否发生在指定的视图上。虽然很少需要重载这些方法,但是您可以通过重载来使子视图无法处理触摸事件。

视图对象的清理

如果您的视图类分配了任何内存、存储了任何对象的引用、或者持有在释放视图时也需要被释放的资源,则必须实现其dealloc方法。当您的视图对象的保持数为零、且视图本身即将被解除分配时,系统会调用其dealloc方法。您在这个方法的实现中应该释放视图持有的对象和资源,然后调用超类的实现,如程序程序清单2-4所示。

程序清单2-4  实现dealloc方法

- (void)dealloc {
    // Release a retained UIColor object
    [color release];
 
    // Call the inherited implementation
    [super dealloc];
}


http://www.apple.com.cn/developer/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/WindowsandViews/WindowsandViews.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值