object-c 第四章

第四章 OS X 和 iOS 应用

对用户而言,应用不过是跟系统里的文件一样的东西。毕竟,电脑是为用户设计,而安装的程序又定义了电脑能够做什么。

作为一个开发者,很容易陷入构建app的细节-类,方法和结构。不过用户只关心你应用的卖点。

在本章,你将了解到OS X 和iOS 应用如何构造,应用能够做些什么,不能做些什么。

什么是应用

iOS 和OS X 应用的打包方式和其他平台的很不同,尤其Windows平台。在其他平台,工程编译后是一个包含编译代码的二进制文件,之后由开发者将二进制文件和资源文件打包。在Linux,你需要打包文件,在Window,通常要创建一个安装程序。

OS X 和iOS 则采用另一种方法。即使用所谓‘包’的概念-对用户而言是一个单独文件。
很多文档格式化是用包作为存储和组织数据的有效途径,因为将数据块不做不同的文件存储意味程序无需实现解压单独文件的逻辑。

所以应用实际是包含编译好的二进制文件和它需要的资源文件。OS X 和iOS 应用的结构稍有不同,不过如何打包程序的原理是一样的。你可以在Finder中右击应用去展示其内部包含的内容。

当你用XCode 编译和创建应用时,XCode 会创建一个包,并复制任何需要的资源到里面。如果你创建的是一个MAC 应用,你可以压缩它并发送到任何人去运行它。在iOS上就有点不同,因为应用必须有代码签名并且放到应用商店后才能在设备运行。

这样做的好处之一,就是应用可以在Mac任意变更目录。

应用,框架,工具和其他

XCode 不但可以创建应用,还可以创建框架。框架和应用的结构差不多,同样有二进制文件和资源文件,但不是独立的且设计给其他应用使用。

Mac上一个典型的例子就是 AppKit 框架,在iOS上等同的是UIKit 框架。

应用由什么组成?

组建以一个iOS 或 OS X应用至少包括两样:

  • 已编译的二进制文件
  • 描述app 系统信息的文件

已编译的二进制文件是由XCode编译并链接。
而描述信息会保存在Info.plist文件,其包含:

  • 应用图标名
  • 应用可以打开何种类型文件
  • 已编译的二进制文件名
  • 应用启动时加载的接口文件名
  • 语言支持
  • 应用是否支持多任务(用于iOS)
  • 应用归属Mac 应用商店何种分类(用于OS X)

这个文件非常重要,如果你移除了它,应用就无法启动。

应用同时也包含应编译进来的资源文件--图片,文件,音频和其他通过XCode工程添加进来的项目。应用可以在运行时引用这些资源。

你可以通过一下步骤了解OS X应用的结构:

  1. 打开XCode,创建一个新的OS X 应用,一切默认后,保存。
  2. 编译。
  3. 在导航栏,你可以看到编译后产生的.app 文件,右击文件,选择在Finder 中打开,就会展示app包含什么了。
  4. 右击应用,选择 ‘显示包内容’,Finder 就会显示其内容。

在iOS上,所有东西都包含在包文件的根目录,而OS X更为严格点。

比如一个名为MyApp 的Mac应用的结构如下:

MyApp.app 包的最上层

Contents 一个包含应用的文件夹
Info.plist 应用的系统信息描述
MacOS 一个包含已经编译的二进制文件的文件夹
PkgInfo 描述应用作者和这是个什么应用的文件。
Resources 一个包含已编译的资源文件的文件夹

而在iOS 的结构则如下:

MyApp 已编译的二进制文件
Info.plist 描述应用的系统信息的文件
Default.png 应用启动时默认展示的图片
Default@2x.png Default.png 的高分辨率版本
embedded。mobileprovision 描述标识应用可在设备上运行的文件
Entitlements.plist 描述应用可以做什么和不可以做什么的文件

因为你的应用可能安装在系统的任何地方,所以你的代码不可以用绝对路径寻找资源文件,幸运的是,Cocoa 已经考虑到这一点并指导你如何做。

使用NSBundle 查找应用的资源

随着编码的进行,你的应用需要考虑在不同平台上运行,这时就用到NSBundle 这个类。通过NSBundle 可以获得应用所在的磁盘位置,以及获取已编译的资源。

对于iOS应用,这显得尤其重要,因为应用可能安装到任意的位置,你的代码不能依赖某以路径,更不能写死路径。一旦这么做了,在iOS保证会出问题。

你可以使用NSBundle 检测应用所在的位置,不过一般只需要知道某单独资源所在的位置,你所要做的,就是给出资源的名字和类型。

比如,下面的代码就是返回SomeImage.png 的相对路径:

NSString* resourcePath = [[NSBundle mainBundle] pathForResource: @"SomeImage" ofType: @"png"];
// resourcePath is now a string containing the
// absolute path reference to SomeImage.png

注意 [NSBundle mainBundle] 这个调用-可能由多个bundle(记住,应用所有的资源都可以作为bundle)

你还可以得到资源的URL:

NSURL* resourceURL = [[NSBundle mainBundle] URLForResource: @"SomeImage" ofType: @"png"];

方法会在资源文件夹查找文件(在iOS,则从应用的根目录开始查找)

对磁盘的文件而言,使用绝对路径和URL 是一样的,但推荐使用URL。文件的URL 一般是这样的:file:///Applications/Xcode.app/。

如果你向工程添加了图像或其他资源,这会把资源都复制到工程编译所在的路径。对与Mac应用而言,资源文件会复制到Resources文件夹,而iOS,则是应用的根目录。

应用的生命周期

每个程序都有启动,运行和终止的状态。比较有意思的是这些状态切换时做了什么。大多数情况,OS X和iOS 的行为是一样的,除了iOS处理多任务时和标准桌面应用不一样外。

在本节,我们将分别讨论这两种应用的生命周期,以及生命周期的状态切换时所发生的事情。

####OS X 应用
当程序启动时,系统首要做的是打开Info.plist。系统根据这个文件查找到已编译的二进制文件的位置并加载。此时,你编写的代码接管系统。

除了编译的代码以外,还有为运行时准备和捆绑的对象集合。这些对象也叫接口对象–准备好窗口,控件和场景-程序编译时存储在nib文件。当应用运行时,这些nib文件就会打开,预先创建的对象就会加载到内存。

应用首先打开nib文件并反序列化其内容。即应用加载窗口,控件和取出存储在nib里面的任何东西并链接它们。主nib文件包含了应用的委托对象。

从nib文件加载完对象后,就发送awakeFromNib 消息。此时,对象就可以开始运行代码了。

总而言之,从nib文件加载的对象接收的时awakeFromNib消息。而自己代码创建的对象则接收init 方法。

此时,应用准备开始运行。接着要做的是向应用委托对象发送applicationDidFinishLaunching:方法,完了,应用就正式进入 run loop 状态。所谓的run loop 是个无限循环,由Cocoa管理,直到程序退出为止。run loop 的目的是监听事件-键盘输入、鼠标移动和点击,计时器暂停等-同时发送这些事件到相关的目标。比如,你有一个事件挂在一个按钮上,当点击按钮时就触发事件。若用户点击了按钮,鼠标点击的事件就发送到按钮,然后就会执行目标事件的代码。

在OS X,应用会继续运行即使用户正在运行其他应用。用户在切换其他应用时,应用的委托对象就会收到applicationWillResignActive: 消息,表明应用准备进入非活动状态,接着,会再接收到applicationDidResignActive: 消息。

以上两个方法之所以要分离开来,是为了让你的代码在iOS系统按下home键,或在OS X切换应用时更好地管理屏幕上所发生的事。当调用applicationWillResignActive:,应用仍然显示在屏幕,当应用不再可见时,应用就会接收到applicationDidResignActive:消息。

如果用户再次切换到原应用,应用就会接收到一对相对应的方法:applicationWillBecomeActive: 和 applicationDidBecomeActive:。这些方法会在应用再次被激活之前和之后立即发送。

应用退出时,事件循环就终止。这时,应用会接收到applicationWillTerminate: 消息,这也是应用保存文件最后的时机。

iOS 应用

iOS 应用的大部分行为和OS X 应用是相同的,仅小部分不同。iOS 设备有限的内存决定了其实现多任务时需要严格使用内存。

iOS 同一时间只能有一个应用在屏幕上-其他应用则是隐藏的。可见的应用称为前台应用,而隐藏的则称为后台应用。一个应用在后台运行多长时间时有严格限制的,后续会介绍。

当使用iOS应用时,用户可能被其他事情打断-比如电话来电,这时用户正在交互的应用就会被切换。从技术角度来看,应用依然在前台运行,只是非活动状态。如果用户接听电话,电话应用变成了运行在前端,而之前使用的程序就切换到后台运行。

还有其他途径使得应用变成非活动状态,比如用户下拉通知中心,打开任务切换器,或者执行其他操作。当应用进入了非活动状态,可能时它退出的信号,所以最好做一些保存的工作。

iOS应用生命周期和OS X的也几乎一致。当应用加载时,检查Info.plist 文件,查找可运行文件并加载,于是应用开始执行代码,取出资源文件等。

应用加载完毕后,接着接收到applicationDidFinishLaunching: withOptions: 方法。这也和OS X的类似,不过会有一个额外的参数-dictionary,包含了应用为何和怎样加载的信息。

通常,用户触摸应用图标就可以启动应用,也可以由其他应用启动,比如一个应用传递文件到另一个应用。可选参数dictionary 包含了描述应用在什么情况下启动应用。

和OS X一样,iOS应用也会接收到applicationWillResignActive: 和applicationDidBecomeActive: 消息(有一点不同,消息里的参数时NSNotification 兑对象,而iOS 则是UIApplicaton)。

当用户终止运行OS X 应用时,我们直到,应用会接收到applicationWillTerminate: 消息。iOS4 之前也是这么做的,之后,引入了多任务,iOS应用的生命周期就被改变了。

iOS 多任务

iOS 应用在后台运行,但只能在非常有限的条件下。因为比起OS X 设备,iOS 设备的CPU,内存空间和电池都非常苛刻。一台MacBook Pro,在运行全部的软件-比如文字处理,浏览器等等,可预期续航7个小时。相反,一台iPhone 4S,在用wifi 上网一般可续航8小时。除此外,MacBook pro 有8GB的内存,而iPhone5s 仅有1G。

因此,没空间一次装下那么多应用,iOS 需强制一些应用可运行在后台一起持续的时间。

如果应用退出了(比如,用户按下home键或打开另一个应用),应用就被挂起-其实并未退出,但停止执行代码,内存也上锁了。当应用恢复时,只需简单地恢复即可。

这就是说应用依然保留在内存,但不再消耗系统的资源,比如cpu和定位。不过,内存依然会变少,因此当另一个应用需要更多内存时,应用就悄无声息地终止了。

需要注意的是,应用挂起时不执行任何代码,就算被终止了也未必通知用户,因此应用切换至后台时需要提前保存重要的数据。

当应用切换到后台运行时,会接收到消息,挂起和唤醒时则不会,这时可以用如下方法:

-(void)applicationDidEnterBackground:(UIApplication *)application;
-(void)applicationWillEnterForground:(UIApplication *)application;

前者会在应用切换到后台时立即调用,之后,应用进入挂起状态,这时需要保存某些数据,因为期间应用可能会被终止。

后者在应用再次回到屏幕之前被调用,此时应用可以重置状态再次运行。

上面提到,如果一个前台应用需要更多内存时,挂起的应用可能将被终止。作为开发者,你可以通过减少应用使用的内存降低这种情况发生的机率-释放占内存大的对象,不加载图片等等。

任何应用都可以请求在后台运行一段较短的时间,但不能超过10分钟,期间,应用可以完成一些耗时较长的处理-比如写文件到磁盘,完成下载,或者其他耗时操作。10分钟快结束时,应用需要通知系统你处理完毕,或应用将会终止(不是挂起而是终止-完全从内存中消失)。

切换至后台运行时,你可以这么做:

-(void)applicationDidEnterBackground:(UIApplication *)application
{
  background = [applicaton beginBackgroundTaskWithExpirationHandler:^{
  // Stop performing the task in the background(stop calculations, etc)
  // This expiration handler block is optional, but recommanded!]
  // Then, tell the system that the task is complete.
  [application endBackgroundTask: backgroundTask];
  backgroundTask = UIBackgroundTaskInvalid;
  }];

  // Start running a block in the background to do the work.
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFALUT, 0), ^{
  // Start doing the background work: write, calculate, etc.
  // Once the work is done, tell the system
  // that the task is complete.
  [application endBackgroundTask: backgroundTask];
  backgroundTask = UIBackgroundTaskInvalid;
  });

}

在iOS 7 ,系统不会保证后台任务一次性执行完设定的时间,而是分成多个时间片执行,以便改善电池的寿命。另外,iOS 7里,有两种途径运行后台任务:background fetching 和 background notifications

Background fetching 是为需要周期性更新的应用设计,比如天气应用或者像Twitter等社交网络应用。通过开启 Background fetching,应用可以在后台得到唤醒并在后台接收更新信息以便用户把应用切换到前台时可以立即使用更新的信息。

要使用Background fetching,需要做如下工作:

在Project Navigator栏,打开Capabilities页,在Background Modes 里启用Background Fetch

在代码中,需要调用setMinimumBackgroundFetchInterval: 设置应用要更新的周期。如果不设置,系统默认不唤醒应用接收更新。

在实际编码中,你可以这么做:

-(void)application: (UIApplication*) application performFetchWithCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler {
  // check for new data to fetch
  // then tell the system you are finished
  // what you tell the system changes if you found new data or not

  // newData is BOOL representing here if there was new data fetched or not
  if (newData)
    completionHandler(UIBackgroundFetchResultNewData);
  else
    completionHandler(UIBackgroundFetchReslutNoData);
}

Background notifications 允许应用在后台接收和处理通知信息。即时通信应用使用
Background notifications 以便应用在后台自动接收到对话的更新或提醒应用有新的内容可以在接收。

Background notifications 其实和background fetching 的操作方式比较相似,两者都需要一些相似的步骤,在第十七章会详细讨论应用如何处理通知,到时你的应用还需要启用远程消息通知。

启动远程通知,可在project -> capabilities tab -> Background Modes 勾选 Remote notifacations。

同 background fetch 类似,应用任何时候接收到通知消息都可以调用方法处理。捕获通知消息的方法如下:

- (void)application: (UIApplication* )application didReceiveRemoteNotification: (NSDictionary* )userInfo fetchCompletionHandler: (void (^)(UIBackgroundFetchResult result))handler

上述方法和background fetch 主要不同的是 userInfo 参数,它是一个包含通知消息数据的字典。

另外还有其他在后台运行较长时间的情况,它们适用于特殊的应用:

  • 播放音频的应用可以在后台运行任意长时间,直到用户切换至其他播放应用。比如,网络播客应用Pandora 应用可以一直播放,直到用户开始用其他音乐软件播放音乐。
  • 追踪用户位置的应用。
  • IP 语音应用(VoIP),比如Skype,允许周期性检查和服务的连接,但不允许无限长时间运行,除非激活了语音通信。

总而言之,如果你要开发一个iOS应用,想要运行你的应用只能寄望于用于直接使用你的应用。一旦用户未使用,应用就会直接停止。

应用沙盒

OS X 和 iOS 实现一系列特性为用户改善整体使用的安全性。其中一个特性就是应用沙盒,一个规定应用可做什么的工具。存在于沙盒中的应用,可能不允许访问沙盒之外的任何资源(硬件,用户数据等等)

沙盒在某些情况对于Mac应用是可选的,但对iOS 应用则是强制性的。

沙盒通过禁止应用操作苹果公司或用户认为不允许的行为来提高系统的安全性,这对改善应用的安全性尤为重要,因为黑客通常使用软件的漏洞进行入侵。比如怀有恶意的人通过入侵adobe的Acrobat Reader 和 微软的 ie6 来危机用户的系统(这两款软件允许安装第三方插件,收集个人数据等等)。

为解决安全问题,沙盒在内核层面限制应用访问用户数据,连接网络,访问诸如相机,麦克风等硬件。即使软件有漏洞,入侵者也无法返回用户的数据,因为应用不允许绕过沙盒。

在iOS应用商店下载的软件会自动加入到沙盒中。后续会详细讨论‘应用限制’。而通过Mac 应用商店发布的应用也要求加入到沙盒之中,但自己发布的则没要求。

作为开发者,想了解沙盒的影响,参考后面的‘使用沙盒’章节。

应用限制

上述提及沙盒会对应用的行为有所限制。这种限制在iOS 和 OS X 是差异非常大,因为OS X 一直一来对应用的限制都较少。

比如,Mac 的应用可以请求对用户的文件进行读写访问。而iOS 只能处理自身的文件,任何其他文件都不允许打开。

iOS 应用限制

当设备安装了iOS应用,它就会放到一个规定的文件目录中,结构如下:

Documents 存储所有属于应用的文件
Library 存储配置信息
Caches 存储缓存文件,如果系统需要释放内存,可以删除这些文件。
Preference 存储偏爱设置
tmp 存储临时文件,文件会被周期性删除。
Application.app 应用包

iOS 应用不许访问超出自己范围的文件。这样可以禁止应用读取隐私数据(比如通话记录)或修改系统文件。

这些严格的限制进使用iOS,Mac 限制相对较宽。

Mac 应用限制

Mac 的限制仅使用在应用商店发布的应用。

当你想将应用加入到沙盒,XCode 有一系列选项供选择,即应用享有所谓的权利。

下面是Mac 应用适用的权利:

文件系统 你可以决定应用对文件系统的读写权限。你也可以控制应用是否需要用下载文件的目录。
网络 你可以决定应用是否允许传出连接或接收传入连接。
硬件 你可以决定应用是否允许访问内置摄像头和麦克风,USB 和打印机
应用通信 你可以决定应用是否允许使用地址薄或日历数据,以及用户位置信息。
音乐、影片和图片目录 你可以决定应用是否允许使用用户的音乐,图片或影片,可对这西恩目录分别设置权限。
私有API

在苹果应用商店发布的应用都需要强制满足一条限制,即应用只能通过苹果公布的类和方法和系统交互。

当然,苹果自身会在很多场景使用自用的类和方法。比如iOS设备锁屏的代码,苹果并没有公布给开发者。

苹果在审核应用是,会扫描所提交的应用。之前是人工进行,后面都是自动的了。一旦你的应用因使用私有api备具,你就必须移除并重新提交。如果上架的应用存在使用私有api的情况,苹果就直接下架应用。

很明显,苹果绝不喜欢开发者使用非官方公布的API。这是因为苹果认为官方公布的API 较为安全和少bug,而且得到官网的维护,较为稳定。而并未公布的api,从某方面来说,可能处于正在开发阶段,或者可能越界访问系统某部分。