ObjC-内存管理精要-全-

ObjC 内存管理精要(全)

原文:zh.annas-archive.org/md5/8f9b1618d016255e027e6b9ac6e0a069

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在 Objective-C 中,管理内存是我们面临的最棘手的问题之一。这本书将为您提供有关您应用程序中有效内存管理的最重要信息。

实践元素也将确保程序员能够以更吸引人的方式积极学习内存管理的关键方法和概念,而不仅仅是简单地阅读书籍。在这本书的整个过程中,我将给出代码示例。

这些示例代码将展示编程和内存管理的根本原理,以及涵盖 iOS 开发的一些方面,如 Core Data。所有这些 Xcode 项目都可以直接运行,您不需要进行任何额外的设置即可运行代码。只需确保您拥有最新的 Xcode 版本,目前是版本 6。

因此,这本书将帮助你了解内存管理以及如何正确有效地实现它,同时让你意识到其带来的好处。这本基于教程的书籍将积极展示内存管理实现的技术,展示其对性能和有效实施的影响。

我必须提到,在这本书中,我将讨论 Objective-C 的最新标准和 Objective-C 2.0。苹果公司建议 Objective-C 作为他们平台的主要开发工具,并致力于持续改进产品。

我必须说,苹果公司尝试改进 Objective-C 的努力并非全部成功。垃圾回收就是一个无效的内存管理例子。自 OS X 版本 10.8 以来,它已被弃用,转而使用自动引用计数(ARC),并计划在未来的 OS X 版本中移除。

我已经使用 Objective-C 多年,C++则更久。因此,内存管理对我来说不是一个陌生的概念,因为我已经在 Azukisoft Pte Ltd 的工作中调试和追踪内存泄漏多年。

在 Azukisoft Pte Ltd 的工作中,我主要使用 Objective-C,偶尔也会用到 C++。这是一个非常有趣的组合,这本书也会对此进行重点介绍。

本书涵盖内容

第一章,Objective-C 内存管理简介,将向您介绍引用计数、手动保留释放(MRR)、对象所有权、沙盒生命周期和内存泄漏。

第二章,自动引用计数,将向您介绍 ARC 及其工作原理,其优势,以及如何设置项目以使用 ARC,Objective-C 中的内存模型,以及与 ARC 的 UIKit。

第三章,使用自动释放池,将向您介绍自动释放池,自动释放池的机制,Apple 自动释放类的概述,ARC 和自动释放,以及块和线程。

第四章, 对象创建和存储,将涵盖创建对象的多种方式;不同内存管理选项的比较:ARC、MRC、自动释放池、垃圾回收、内存模型;以及@property 如何使您的生活更轻松。

第五章, 管理您的应用程序数据,将涵盖磁盘缓存、UI 技术部分数据显示、序列化和归档对象、编码和解码对象的方法、需要 SQLite 的情况,以及 SQLite 与 Core Data 的比较。

第六章, 使用 Core Data 进行持久化,解释了 Core Data 是什么以及为什么应该使用它,NSManagedObject 及其在您应用程序中的使用,使用 Core Data 时的内存管理,以及常见错误。

第七章, 键值编程方法,解释了键值编码或 KVC 是什么,NSKeyValueCoding 协议,NSKeyValueCoding 行为的手动子集,关联对象,选择器作为键,最大灵活性,以及处理键/值。

第八章, Swift 简介,突出了 OS X 中的 Cocoa 绑定、自动与手动键值观察之间的差异,以及键值观察的实现方式。

第九章, 内存管理和调试,涵盖了内存过度使用、收集应用程序数据、如何在 Xcode 中使用工具、使用 LLVM/Clang 静态分析器、使用 NSZombie 帮助查找过度释放的对象,以及处理泄漏。

第十章, 内存管理技巧和窍门,解释了访问器方法的使用、使用属性声明访问器、性能指南,以及何时应避免 KVC 和 KVO。

第十一章, Xcode 6 的功能, 介绍了新的工具,如视图层次结构调试器、预览编辑器,以及新增的功能,如允许故事板和 NIBs 作为应用程序的启动图像,而不是仅仅使用静态图像。

本书所需条件

对于本书,您需要一个基于 Apple 的 Intel Macbook、iMac 或 Mac mini,2010 型号或更高版本,并已安装 Xcode,版本 4.3 或更高(可在 Mac Apple Store 获取)。

本书面向对象

本书特别为具有最少 Objective-C 或另一种面向对象编程语言经验的开发者以及具有最少编程逻辑、面向对象编程和 Apple OS X 环境知识的科技学生设计。

习惯用法

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:"当你执行newmallocalloc时,操作系统所做的就是给你的程序在堆上分配一块内存。"

代码块设置如下:

int main(int argc, char *argv[]) {

  SomeObject *myOwnObject;
  // myOwnObject is created in main
   myOwnObject = [[SomeObject alloc] init];

    // myOwnObject can be used by other objects
  [anotherObject using:myOwnObject];

新术语重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中将以如下方式显示:"在 Xcode 中,转到目标构建阶段选项卡,打开编译源组,你将能够看到源文件列表。"

注意

警告或重要提示将以这样的框中显示。

小贴士

小技巧和窍门将如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。

下载示例代码

您可以从您在www.packtpub.com的账户下载示例代码文件,适用于您购买的所有 Packt 出版社的书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给您。

错误更正

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误更正,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误更正提交表单链接,并输入您的错误更正详情。一旦您的错误更正得到验证,您的提交将被接受,错误更正将被上传到我们的网站或添加到该标题的错误更正部分下的现有错误更正列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将显示在勘误部分。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章: Objective-C 内存管理简介

在本章中,我们将主要关注内存管理问题的核心问题以及基于 Objective-C 的解决方案。我们将研究对象的所有权和生命周期。这个基本思想被称为手动引用计数,或手动保留释放MRR),其中你需要声明和放弃每个对象的所有权。它定义了对象的生命周期。最后,我们将更深入地探讨NSObject,以便更好地理解正在发生的事情。

在本章中,我们将涵盖以下主题:

  • 为什么我们需要在 Objective-C 中进行内存管理?

  • 对象的所有权和生命周期

  • 引用计数的原理

  • 什么是内存泄漏,为什么要注意它?

为什么我们需要在 Objective-C 中进行内存管理?

无论使用什么编程语言,内存管理的问题总是存在的。一般来说,这是一个无法避免的资源管理问题,因为内存始终是有限的资源。

脚本语言和 Java,在内存管理由虚拟机或应用程序(在代码中隐藏)处理的情况下,并不总是足够有效。虽然这种方式对程序员来说更容易,但它可能会对资源产生负面影响,因为你没有对其绝对的控制权,而且当不再需要时,仍然有对象“活着”,此外,这些“活着”的对象仍然占用宝贵的内存空间,这些空间本可以由其他对象使用。另外,根据你的要求,另一种观点认为,自动内存管理是唯一正确的方向。

这样的讨论通常开始于像“哪种编程语言最好?”和“内存管理的最佳方式是什么?”这样的讨论。让我们把那些无意义的争论留给博客和论坛上的“圣战”吧。每个工具在正确的上下文中都有其用途,而 Objective-C 的内存管理概念在时间和资源节约方面都相当高效。

Objective-C 中的内存管理方式与一些广泛使用的语言(如 C/C++、Java 或 C#)不同,这些语言通常在学校教授,因为它引入了新的概念,如对象所有权。对于运行在有限内存上的设备(如手机、智能手表等)来说,内存管理至关重要,因为有效的内存管理将允许你从这些小型设备中榨取每一滴性能,在这些设备上,内存非常稀缺。

对象的所有权和生命周期

对象所有权抽象的想法很简单——一个实体简单地负责另一个实体,并且实体有拥有对象的能力。当一个实体拥有一个对象时,该实体也负责释放该对象。

让我们回到我们的代码示例。如果一个对象在主函数中被创建并使用,那么主函数对该对象负责,如下面的代码列表所示:

int main(int argc, char *argv[]) {

  SomeObject *myOwnObject;
  // myOwnObject is created in main
   myOwnObject = [[SomeObject alloc] init];

    // myOwnObject can be used by other objects
  [anotherObject using:myOwnObject];

    // but main is responsible for releasing it
   [myOwnObject release];

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户下载示例代码文件,以获取您购买的所有 Packt 出版物的代码。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

使这个概念变得稍微复杂一点的是,对象可能被多个实体拥有。因此,一个对象可能被在主函数中创建和拥有,同时也会被另一个实体使用,该实体将声明对对象的拥有权。

您会在使用数组时看到多个对象拥有权的情况是一个常见的情况。数组是对象的索引列表,当一个对象被放入数组中时,数组声明对对象的拥有权。因此,如果我在主函数中创建一个对象并将其放入数组中,主函数和数组将同时声明对对象的拥有权并为其创建一个引用。拥有权和引用是不同的,因为一个对象引用另一个对象,它并不拥有,并且两者都负责清理对象。以下代码演示了这一点:

int main (int argc, char *argv[]) {

  SomeObject *myOwnObject;
  // myOwnObject is created in main
myOwnObject = [[SomeObject alloc] init];

// myOwnObject can be used by other objects
NSMutableArray *myArray;
// add my object to myArray    
myArray = [[NSMutableArray alloc] initWithObjects:myOwnObject, nil];

// main does not need myOwnObject any more
[myOwnObject release];

// but myOwnObject still is needed inside the array
[anotherObj usingArray: myArray];

就像现实世界中的对象一样,Objective-C 对象被创建;它们存在,然后在应用程序关闭时消失。这就是对象生命周期的工作方式。显然,数组必须声明对对象的拥有权,并防止在主函数中调用的释放方法中删除对象。

然而,实体如何正确地对其拥有的对象声明其权利呢?让我们更深入地看看这个问题。

对象拥有权和引用计数

为了表示使用对象的拥有者数量,这些对象被赋予了一个引用计数。

在开始时,对象的引用计数为 1。这是因为创建对象的函数将要使用该对象。当任何实体需要声明对对象的拥有权时,由于该实体将要访问和使用该对象,它会向其发送保留消息,并且其保留计数增加 1。当实体完成对对象的操作后,它会向对象发送释放消息,并且其保留计数减少 1。只要这个对象的引用计数大于零,一些“东西”正在使用它。当它降到零时,该对象对那些“东西”就不再有用,并且可以安全地释放。

让我们回到由数组拥有的对象的例子。以下代码注释和图中给出了解释:

int main(int argc, char *argv[]) {

  SomeObject *myOwnObject;
  // myOwnObject is created in main
   myOwnObject = [[SomeObject alloc] init];
  // myOwnObject has retain count equal to 1

// myOwnObject can be used by other objects
NSMutableArray *myArray;
// add my object to myArray    
myArray = [[NSMutableArray alloc] initWithObjects:myOwnObject, nil];
//inside myOwnObject got another retain message
//and now its retain count equal 2

// main does not need myOwnObject any more
[myOwnObject release];
// release decrements retain count
// and now myOwnObject retain count now is 2-1 = 1

// but myOwnObject still is needed inside the array
[anotherObj usingArray: myArray];

[myArray release];
// on array destruction every object inside array gets release message

//myOwnObject retain count decreases this time to 0 and myOwnObject will be deleted together with the array

以下图表说明了引用计数的原理:

对象拥有权和引用计数

在将指针设置为指向其他对象之前忘记向对象发送释放消息将保证你会有内存泄漏。为了在初始化之前创建一个对象,操作系统会分配一块内存来存储它。此外,如果你向一个之前未发送过释放语句的对象发送了释放语句,则会向该对象发送一个保留语句。这将被视为过早释放,即之前分配给它的内存不再与其相关。调试这些问题会花费很多时间,在大项目中这些问题很容易变得非常复杂。如果你不遵循一些关于内存管理的坚实基础原则,你经常会忘记,并且很快就会发现自己花费数小时检查每一个保留和释放语句。更糟糕的是,如果你正在查看别人的代码,而他们搞砸了。在别人的代码中修复内存管理问题可能需要很长时间。

什么是内存泄漏,为什么要注意它?

内存泄漏是指你的程序失去了对已分配但忘记释放的内存的跟踪。结果是,“泄漏”的内存将永远不会被程序释放。在某个时间点之后,如果继续泄漏更多内存,将没有更多空闲内存,这将导致你的应用程序崩溃。通常,这倾向于发生在代码执行 newmallocalloc,但从未执行相应的“delete”、“free”或“release”操作时。

当你执行 newmallocalloc 时,操作系统所做的就是给你的程序在堆上分配一块内存。操作系统会说:“这里,拿这个内存地址,并在它上面有一个内存块。”因此,你需要创建对该内存地址的引用(通常以指针的形式),根据操作系统,例如,“我完成了这个,它不再有用”(通过调用“free”、“delete”或“release”)。

当你丢弃指向该内存的指针时,内存泄漏就会发生。如果你的程序不知道你的内存是在堆上分配的,你怎么能释放它呢?以下代码行展示了如果你从未调用释放方法,它将是一个内存泄漏的例子:

NSMutableString *str = [[NSMutableString alloc] initWithString:@"Leaky"];

那为什么你应该关心呢?最好的情况是,你是当用户退出你的应用程序时将被释放的消耗内存。最坏的情况是,每个屏幕都可能发生内存泄漏。这并不是结束程序的好方法,特别是如果用户让它长时间运行。程序崩溃很难调试,因为它可以在应用程序的随机时刻崩溃,因为内存泄漏很难复制。创建一个经常崩溃的应用程序会导致你的程序在 App Store 上或通过口碑收到差评,这是你不想发生的事情。

正因如此,在进化的过程中,Objective-C 中有其他内存管理方法,你将在本书中进一步了解。

Objective-C 中的对象是什么?

Objective-C 内部是如何工作的?NSObject是大多数 Objective-C 类层次结构的根类,通过它,对象继承基本方法,并表现出 Objective-C 对象的特性。

这个对象是一个类的实例,也可以是类的一个成员或其派生类之一。因此,让我们更深入地了解NSObject。在早期,Objective-C 有一个名为Object的类。它有一个名为+new的方法,该方法封装了malloc(),还有一个名为-free的方法。由于 Objective-C 对象通常被别名化,并且管理对象的生命周期变得相当复杂,这很麻烦。

NSObject 被 NeXT(史蒂夫·乔布斯在 1985 年被苹果公司解雇后创立的第二家公司)使用,以便提供引用计数,因此,将对象指针分为两类:拥有引用的指针和不拥有引用的指针。那些对对象引用计数有贡献的指针是拥有引用指针。如果确定一个引用将在变量生命周期的某个时刻被其他地方持有,可以使用非拥有引用指针,从而避免引用计数操作的额外开销,因为非拥有引用指针没有跟踪对象拥有的额外成本。

非拥有引用指针通常用于自动释放值。自动释放池使得临时对象能够接收非拥有引用指针作为回报。通过接收一个-autorelease消息,对象被添加到一个列表中,之后将被释放,与当前自动释放池的销毁同时进行。您可以使用如下所示的自动释放方法调用自动释放:

 [myObject autorelease];

以下表格显示了自动释放和释放的一些描述:

释放类型 描述
自动释放方法 对象收到释放消息,但放入自动释放池,对象将在运行循环中稍后池被耗尽时释放,但仍然占用内存
释放方法 对象被立即释放,并在对象释放后释放内存

任何收到自动释放消息的对象将在自动释放池耗尽时被释放。使用自动释放而不是常规释放方法将延长对象的生命周期,直到运行循环结束时池被耗尽。

在 2011 年的全球开发者大会WWDC)上,苹果公司引入了 ARC(自动引用计数)的缩写。它强制编译器在编译时处理内存管理调用,而不是传统的垃圾回收功能,该功能在运行时发生。ARC 还向语言模型中添加了一些内容。它自 iOS5、OS X 10.7 以及 GNUstep 以来一直得到支持。

首先,我们将发现 Cocoa 中有两个 NSObjects,一个类和一个协议。为什么会这样,这个设计的目的是什么?让我们来看看类和协议。

在 Objective-C 中,协议定义了一组对象在运行时预期遵循的行为。例如,一个表格视图对象预期能够与某个数据源进行通信,以便表格视图知道要显示哪些数据和信息。协议和类不共享相同的命名空间(包含名称、类和协议名称的标识符集合,因此相同的名称可以存在于不同的命名空间中)。两者都可以存在,在语言层面上它们是无关的,但具有相同的名称。这种情况在 NSObject 中就是这样。

在语言中,没有地方可以使用协议或类名。将类名用作消息发送的目标、类型名称和在 @interface 声明中是允许的。同样,在几个相同的位置可以使用协议名称;然而,方式不同。具有与类相同名称的协议不会引起任何问题。

根类不可能有超类,因为它们位于层次结构的顶部,因此没有根类之上的超类,NSObject 类就是其中之一。我强调说“其中之一”,因为在与其他编程语言相比的 Objective-C 中,存在多个根类的存在是完全可能的。

Java 的单一根类名为 java.lang.Object,它是任何其他类的父终极类。因此,任何来自对象的 Java 代码,都添加了由 java.lang.Object 添加的基本方法。

Cocoa 可以有多个根类。除了 NSObject,还有 NSProxy 和其他一些根类;这些根类部分是 NSObject 协议存在的原因。NSObject 协议确定了一组特定的基本方法,期望其他根类实现这些方法,从而使得这些方法在需要时随时可用。

NSObject 类遵循 NSObject 协议,这导致了该基本方法的实现:

   //for NSObject class 
  @interface NSObject <NSObject>

对于 NSProxy,实现相同的方法也是适用的,它也遵循 NSObject 协议:

   // for NSProxy class @interface NSProxy <NSObject>

NSObject 协议中可以找到诸如 hash、description、isEqualisKindOfClassisProxy 等方法。NSProxyNSObject 协议表示,即使实现了基本的 NSObject 方法,仍然可以依赖 NSProxy 实例。

继承 NSObject 会带来很多可能引起问题的负担。NSProxy 通过提供一个更简单的超类来帮助防止这种情况,这个超类中包含的额外内容较少。

对于大多数 Objective-C 编程来说,NSObject协议对根类的有用性并不那么有趣,因为简单的事实是我们不经常使用其他根类。然而,当你需要创建自己的协议时,这将非常方便。

假设,你有一个以下协议:

    @protocol MyOwnProtocol
    - (void)myFunction;
    @end

并且有一个指向简单对象myOwnObject的指针,它符合它:

    id<MyProtocol> myOwnObject;

你可以告诉这个对象执行myFunction

    [myOwnObject myFunction];

然而,你不能要求对象提供其描述:

    [myOwnObject description]; // no such method in the protocol

你无法检查它的相等性:

    [myOwnObject isEqual: anotherObject];
    // no such method in the protocol

通常,你不能要求它执行任何正常对象可以执行的操作。有时候这并不重要,但在某些情况下,你可能会希望能够执行这个任务。

如前所述,NSObject是大多数 Objective-C 类层次结构的根类,通过NSObjects,你的 Objective-C 类可以继承系统接口,并获得作为 Objective-C 对象的行为能力。因此,如果你想让你的对象能够访问isEqual等方法,NSObject就很重要。这就是NSObject协议出现的地方。协议可以继承自其他协议,这意味着MyProtocol可以继承自NSObject协议:

    @protocol MyOwnProtocol <NSObject>
    - (void)myFunction;
    @end

这表示不仅符合MyOwnProtocol的对象会响应myFunction,它们还会响应NSObject协议中的所有那些常见消息。既然知道你的应用程序中的任何对象都是直接或间接地从NSObject类继承而来,并且符合NSObject协议,那么对实现MyOwnProtocol的人就没有额外的要求,同时允许你在实例上使用这些基本方法。

注意

对于框架来说,存在两个不同的NSObject是不正常的;然而,当你深入了解时,这开始变得有意义。NSObject协议允许所有具有相同基本方法的根类获得权限,这也为声明一个包含任何对象预期功能的基本功能协议提供了一个非常简单的方法。NSObject类将所有这些功能一起引入,因为它符合NSObject协议。这里需要注意的是,一个创建的且没有继承NSObject的自定义类可以被视为根类,但一旦你的自定义类从NSObject继承,那么根类就不再是你的自定义类,而是NSObject。然而,通常,你的大多数自定义类都应该继承自 NSObjects;它将实现 NSObject 的功能,如allocinitrelease等,如果没有从 NSObject 继承,这些功能需要由你编写和实现。

摘要

在本章中,你学习了 Objective-C 中的内存管理是什么以及它是如何工作的。你还学习了在手动管理保留和释放时的一些最佳实践,并了解了自动引用计数(ARC)、Objective-C 对象和根类的基础知识。ARC 基本上可以被视为一种编译时防止内存泄漏的防护措施,因为编译器会在编译时自动为你编写释放语句。因此,你不需要在代码中编写冗长的释放语句来保持代码的简洁和紧凑。

在使用内存管理进行编码时,需要注意的一个小贴士是,无论何时你执行 allocinit,然后在该之后编写你的释放代码,并将其放在你的类中适当的位置,你可能会忘记在编写某些代码或修复某些错误后调用释放方法。因此,在你执行 allocinit 之后编写你的对象释放语句将帮助你将内存泄漏降到最低,这样你就不会因为忘记编写对象释放语句而出现内存泄漏的情况。

在下一章中,你将学习更多关于 ARC 的内容,包括它是如何工作的、它的优点,以及如何设置你的项目以使用 ARC 和 Objective-C 及 UI Kit 中的内存模型。

第二章:自动引用计数

好的想法可以长久生存,而坏的想法则很快就会消失。在 Objective-C 中,引用计数的长期存在被视为一个非常好的想法。在这个进化过程中的下一步是它变得自动,所以我们称之为自动引用计数ARC),这是苹果公司在 2011 年为桌面和移动操作系统 Mac OS X Lion 和 iOS 5 上的应用程序开发引入的。它将初始引用计数的名称更改为手动引用计数

在本章中,我们将涵盖以下主题:

  • ARC 及其工作原理

  • ARC 的优缺点

  • ARC 的项目设置

  • 将不支持 ARC 的代码与您的项目混合

  • Objective-C 中的内存模型

  • UI 套件中的 ARC

什么是 ARC 以及它是如何工作的?

如果你还记得,引用计数的想法涵盖了从内存中实际删除对象。有了引用计数,Objective-C 负责实际的对象销毁。拥有对象的对象只负责释放其对对象的所有权。因此,下一个出现的想法是使一切完全自动化,就像在 Java 和 C#等语言中做的那样。这个想法在垃圾回收分支和自动引用计数中得到了发展。

垃圾回收仅适用于 Mac OS X,从版本 10.5 开始。此外,请注意,iOS 应用程序不能使用垃圾回收;因为它依赖于设备的性能,这将需要一些时间来处理,迫使用户等待处理结束,从而产生不良的用户体验。它也被弃用了,因为从 OS X 版本 10.8 开始,它支持 ARC,并计划在即将到来的 OS X 版本中删除。

ARC 是一种新的创新方式,它包含了垃圾回收的许多优点,但又不同于垃圾回收。ARC 没有后台进程来处理对象的释放,这使得 ARC 在性能比较中相对于垃圾回收具有很大的优势。

然而,在解释 ARC 如何做到这一点之前,了解 ARC 不做什么是很重要的:

  • ARC 不强制运行时内存模型,如垃圾回收所做的那样。在 ARC 下编译的代码使用与纯 C 或非 ARC Objective-C 代码相同的内存模型,并且可以链接到相同的库。

  • ARC 只为从NSObject继承的 Objective-C 对象提供自动内存管理,注意在 Objective-C 中,块也偶然是底层对象)。

  • 以任何其他方式分配的内存不会被触及,并且仍然必须手动管理。对于其他资源,如文件句柄和套接字(如流)也是如此。

ARC 的外观

首先,想象一个由专家 Cocoa 程序员编写的传统 Objective-C 源代码文件。retainreleaseautorelease消息在所有正确的位置发送,并且处于完美的平衡状态。

现在,想象一下编辑源代码文件,移除所有retainreleaseautorelease消息的实例,并在 Xcode 中更改一个单一的构建设置,该设置指示编译器在编译源代码时将所有合适的内存管理调用放回你的程序中。这就是 ARC。它正是名字所暗示的——传统的 Cocoa 引用计数,是自动完成的。

在其核心,ARC(自动引用计数)不是一个运行时服务;它不像垃圾回收那样作用于程序执行。另一方面,新的 Clang,C、C++、Objective-C 和 Objective-C++的编译器前端,将其提供为两个阶段(我们将这些阶段称为“周期”)。在下面的图中,你可以看到这两个阶段。在名为前端的周期中(如下所示),Clang将分析每个预处理的文件以查找属性和对象。然后,依靠一些固定的规则,它将插入正确的语句——retainreleaseautorelease

ARC 的外观

例如,如果一个对象被分配并且局部对应一个方法,这个对象将在该方法端点附近有一个release语句。如果这个release语句是一个类属性,它将进入类的dealloc方法中,这可能是一个自定义类或任何 Objective-C 类。如果它是一个集合对象或返回值,它将得到一个autorelease语句。然而,如果它被作为弱引用引用,它将被留在那里。

前端也会为局部释放的对象插入retain语句。它访问每个声明的访问器,并使用指令@property更新它们。它包括对超类如NSObjectUIViewController或甚至你自己的customer超类的dealloc例程的调用。它还会报告任何显式的管理调用和双重所有权。

在优化周期中,修改后的源代码被发送到 Clang 的负载均衡。因此,它计算为每个对象创建的retainrelease调用,并将它们减少到最优的最小值。这个动作避免了过多的retainrelease消息,这些消息可能会影响完全的性能:

To see how it works, take a look at the following code:
@class MyBar;
@interface MyFoo
{
@private
    NSString *myOwnString;
}
@property(readonly) NSString *myOwnString; 

- (MyBar *)getMyBarWithString:(NSString *)myString;
- (MyBar *)getMyBar;

@end

@implementation MyFoo;
@dynamic myOwnString;

– (MyBar *)getMyBarWithString:(NSString *)myString
{
    MyBar *yBar;

    if (![self.myString isEqualToString:myString]) 
    {
        myOwnString = myString;
    } 
    return [self getMyBar];
}

- (MyBar *)getMyBar
{
    MyBar *yBar

    return yBar;
}
@end

现在,它是一个没有retainrelease的 Objective-C 类。有一个名为myOwnString的私有属性,它是一个NSString的实例。这个类导入了MyBar类的头文件(第 1 行)并声明了一个同名只读获取器,myOwnString。还有一个名为getMyBarWithString的修饰符和一个名为getMyBar的内部函数。

以下代码是使用手动引用计数MRC)的相同代码片段:

@class MyBar;
@interface MyFoo
{
@private
    NSString *myOwnString;
}
@property (readonly) NSString *myOwnString; 

- (MyBar *)getMyBarWithString:(NSString *)myString;
- (MyBar *)getMyBar;

@end

@implementation MyFoo;
@dynamic myOwnString;

– (MyBar *)getMyBarWithString:(NSString *)myString
{
    MyBar *yBar;

    if (![self.myString isEqualToString:myString]) 
    {
        [myString retain];
        [myOwnString release];
        myOwnString = myString;
    }
    return [self getMyBar];
}

- (MyBar *)getMyBar
{
    MyBar *yBar

    [yBar autorelease];
    return yBar;
}

- (void)dealloc
{
    [myOwnString release];
    [super dealloc];
}
@end

注意,类接口仍然是相同的。然而,现在,getMyBarWithString修饰符有一些新的语句;更具体地说,两个:

[myString retain];
[myOwnString release];

myOwnString属性(第 24 行)发送释放语句是其中之一的责任。另一个向myString参数(第 25 行)发送retain消息。在将最后一个返回作为其结果之前,getMyBar函数向yBar局部变量发送本地autorelease语句。最后,MRC 取代了该类的dealloc方法。MRC 还释放了myOwnString属性(第 44 行)并调用了其超类的dealloc方法(第 45 行);仍然在该方法中,如果已经存在dealloc方法,MRC 会适当地更新其代码。

当使用 ARC 时,你不需要显式插入retainrelease消息,因为 ARC 将在编译期间自动插入它们。由于 ARC 会自行决定如何更好地管理 Objective-C 对象,因此不再需要开发类代码所需的时间。因此,ARC 避免了任何空指针。你还可以在单个文件的基础上排除 ARC,选择你的目标,转到构建阶段,并在编译器标志中添加-fno-objc-arc标志。

然而,Clang 编译器是构建在 LLVM 3.0 中的,仅从 Xcode 4.2 版本开始可用。自从 Mac OS X 版本 10.7 和 iOS 版本 5.0 以来,已经对 ARC 提供了优化运行时支持。使用 Mac OS X 10.6 和 iOS 4.3 的二进制文件启用 ARC 并不困难,但对于 iOS 4.3,只能通过蓝代码实现;而对于 OS X 10.6,最新版本根本不使用弱指针。

关于 ARC 的一些要点如下:

  • 它不适用于AppleScriptObjC或甚至PyObjC源;它仅适用于 Objective-C 源。

  • 然而,或多或少,当有PyObjCAppleScriptObjC类通过 Objective-C 代码连接到 Cocoa 时,ARC 将影响底层代码。

  • 注意,对于某些第三方框架,如果启用了 ARC,它们在编译时可能会崩溃。请确保该框架的开发者能够并愿意更新它。

ARC 的项目设置

当项目设置为使用 ARC 时,编译器标志-fobjc-arc默认为每个 Objective-C 源文件设置。可以通过编译器标志-fno-obj-arc禁用特定类的 ARC。在 Xcode 中,转到构建阶段标签,打开编译源组,你将能够看到源文件列表。当你双击想要设置的文件时,将出现一个弹出面板。在该面板中,输入-fno-obj-arc标志,然后点击完成以完成设置。

如果在创建项目时没有启用 ARC,那么要启用它,请按照以下步骤操作:

  1. 打开项目。

  2. 前往编辑 | 重构 | 转换为 Objective-C ARC

  3. 如果没有问题并且准备转换,它将检查你的代码。

默认情况下,Xcode 5 中所有新创建的 Objective-C 项目都启用了 ARC。但是,如果你需要禁用它,请按照以下步骤操作:

  1. 选择项目

  2. 选择目标

  3. 从右侧面板,转到构建设置

  4. 选择自动引用计数

  5. 选择Apple LLVM 编译器 3.0 – 语言

  6. 定位到Objective-C++ 自动引用计数,并在所有三个部分中,选择NOARC 的项目设置

Objective-C 的内存模型

Objective-C 2.0 的一个非常重要的改进是其内存模型。作为预处理器的第一个 Objective-C 实现中无数问题的残留都被清理了。在旧版本中,Objective-C 对象仅仅是 C 结构,它们在第一个字段中包含指向其类的指针,并且当你想发送消息时,它们的指针只能接收消息。

现在每一个对象指针都属于以下类别之一:自动释放不安全未保留。当 ARC 被禁用时,程序员需要负责处理它们所有,确保它们都是安全的,因为这些指针仅仅适合最后一个类别。

默认类别(类型限定符)是一个指针;它们在很大程度上对应于编写无缺陷的防御性保留/释放代码所带来的后果。对指针的赋值相当于保留新值并释放旧值,因为拥有引用的存储就在这些指针中。

为了存储自动释放的值,你需要使用自动释放指针。在 Objective-C 中,这样的指针是最常见的非拥有引用指针形式;它们是在堆上存储自动释放值的变量。一个拥有引用指针,也称为实例变量,只有在它被存储到一个非拥有引用指针,即自动释放变量中时,才会被自动释放。如果你只是存储一个自动释放的引用指针,你将有一个简单的属性。

为了减少关键代码段中释放和保留语句的数量,可以使用四个 ARC 类型限定符之一_autoreleasing。然而,由于对象将被包含在自动释放池中,并且 ARC 通常可以消除这一点,所以通常不需要使用这个类型限定符,除了它可能会使事情变慢之外。

是指针的最后一个类别(类型限定符)。如果你在 Objective-C 中使用了垃圾回收器模式,你可能已经通过在这样一个指针中存储对象而遇到了指针。它不被视为一个拥有引用指针(例如,一个变量),当对象被释放时,这个指针立即被设置为 nil。

我们可以数出 GC 和 ARC 模式之间的许多不同之处,但非常重要的一点是关于 ARC 的确定性。你可以通过指针看到这一点。以下代码是一个示例:

    id strong = [NSObject new];
    __weak id weak = strong;
    strong = nil;
    NSLog(@"%@", weak);

首先,由于在垃圾回收模式下__weak对栈变量是不被允许的,所以前面的代码甚至无法通过编译。然而,如果将弱引用声明移动到有效位置会发生什么?我们假设此时对象的最后一个引用已经消失了。然而,日志语句会显示对象仍然存在,是活跃的。依赖于编译器运行的优化,如果强制运行垃圾回收器,收集器可能可以在堆上看到它的引用。

这段代码将在 ARC 模式下编译——现在,堆上允许存在弱变量。

你需要了解的关于 ARC 和弱引用的内容

自从 GNUstep Objective-C 运行时版本 1.5 以来,iOS 版本 5 和 OS X 版本 10.7 以来,弱引用一直被支持。ARC 通过兼容性库工作,但它需要对许多类进行修改才能与弱引用一起工作。

摘要

在本章中,我们关注了自动引用计数(ARC),它的优势、工作原理以及如何正确设置和将其集成到当前项目中。

在下一章中,我们将讨论自动释放池机制及其类、块和线程。我们还将了解 Objective-C 中的内存模型。我希望这一章能让你对 ARC 有一个良好的理解。

第三章:使用 Autorelease Pools

假设你正在返回一个你创建(因此拥有)的对象给调用者。如果它在你的方法中被释放,返回的对象将是无效的。另一方面,有一个基本规则,你必须释放你拥有的对象;那么,你该如何释放它们?简单来说,将对象放入自动释放池。当自动释放池被清空时,对象就会被释放。

本章将涵盖以下主题:

  • 理解自动释放池机制

  • Autorelease Pool 如何帮助

  • 自动释放类

  • 自动释放池块和线程

  • Objective-C 中的内存模型

  • 弱引用 ARC

理解自动释放池机制

当你刚开始为 Cocoa(iOS 或 Mac OS)开发时,你会很快学会遵循标准的allocinit和(最终)release周期:

// Allocate and init
NSMutableDictionary *dictionary = [[NSDictionary alloc] init];

// Do something with dictionary
// ...

// Release
[dictionary release];

这很好,直到你发现以下操作的便利性:

// Allocate and init
NSMutableDictionary *dictionary = [NSDictionary dictionary];

// Do something with dictionary
// …

让我们看看内部实际发生了什么:

NSMutableDictionary *dictionary = [[NSDictionary alloc] init];
return [dictionary autorelease];

这种方法被称为自动释放池,它们是 Cocoa 平台使用的自动引用计数(ARC)内存管理模型的一部分。

ARC 编译器会为你自动释放任何对象,除非它来自一个以newallocinitcopymutableCopy开头的方法。和之前一样,这些对象被放入自动释放池,但为了引入一个新的语言结构,NSAutoreleasePool已被@autoreleasepool编译器指令所取代。即使使用 ARC,我们仍然可以自由地使用autorelease消息在任何时候清空/创建我们的池。它不会影响实现retainrelease消息时的编译器,但提供了当自动释放对象安全退出作用域时的提示。

Cocoa 框架(Foundation Kit、Application Kit 和 Core Data)为从NSObject继承的基本类提供了一些合适的方法来处理,例如NSStringNSArrayNSDictionary等。这些方法会快速为你分配、初始化并返回创建的对象,而且你无需过多担心,该对象将会自动释放。

注意

注意,我真正想说的是“不必过多担心”,而不是“完全不担心”,因为即使有这些方便的框架为你创建和清除对象,也仍然会有你想要更多控制并自己创建额外的自动释放池的情况。

基本上,Autorelease Pool 存储对象,当它被清空时,它只是向对象发送一个release消息。NSAutoreleasePool类用于支持 Cocoa 的引用计数内存管理系统。

自动释放池是由苹果公司制作的,并且自 OS X 10.7 以来一直是语言的一部分。如果在 ARC 模式下程序引用了NSAutoreleasePool类,则被视为无效,并在构建阶段被拒绝。相反,在 ARC 模式下,你需要用@autoreleasepool块来替换它,从而定义一个自动释放池有效的区域,如下面的代码所示:

// Code in non-ARC mode NSAutoreleasePool *myPool = [[NSAutoreleasePool alloc] init];
// Taking advantage of a local autorelease pool.
[myPool release];

然而,在 ARC 模式下,你应该这样写:

@autoreleasepool {
    // Taking advantage of a local autorelease pool.
}

即使你不使用自动引用计数(ARC),你也可以利用比NSAutoreleasePool类更有效的@autoreleasepool块。

与使用垃圾回收的环境相反,在引用计数的环境中,每个收到autorelease消息的对象都会被放入一个NSAutoreleasePool对象中。这个NSAutoreleasePool类就像是一个这些对象的集合,当它被清空时,会逐个发送释放消息。当你超出作用域时,它会清空池子。然后,每个对象的保留计数会减少 1。通过使用autorelease作为释放消息的替代,你延长了对象的生命周期,这次可能甚至更长,如果对象后来被保留,或者至少直到NSAutoreleasePool类被清空。如果你多次将对象放入同一个池子中,每次都会收到一个release消息。

在引用计数的环境中,Cocoa 假设始终会有一个可用的autorelease池;否则,收到autorelease消息的对象将不会被释放。这种做法会导致内存泄漏并生成适当的警告消息。

在事件循环的周期开始时,应用程序套件(Cocoa 框架之一,也称为 AppKit)会创建一个autorelease池。它提供了创建和交互 GUI 的代码,并在该周期的末尾清空,然后处理事件时创建的每个自动释放对象都会被释放。这意味着你不需要自己创建池子,因为应用程序套件会为你做这件事。然而,如果你的应用程序创建了大量的自动释放对象,你应该考虑创建“局部”的自动释放池;这有助于避免峰值内存占用。

要创建一个NSAutoreleasePool对象,你可以使用常规的allocinit方法,并使用drain来销毁它。池子不能被保留;drain的效果就像是一个释放操作,这在同一个上下文中创建它非常重要。

每个线程都有自己的 autorelease pool 栈。这些栈包含NSAutoreleasePool对象,这些对象反过来包含 autoreleased 对象。每个新的 autoreleased 对象都放在池的顶部,每个新的 pool 都放在栈的顶部。当池被清空时,它从栈中被移除。在线程完成之前,它会清空其栈上的所有 autorelease pool。尽管可以手动创建 autorelease pool 并手动将其添加到对象中,但 ARC 仍然会自动清空池:不允许你自己这样做。

为了确保你不必担心所有权,这是 ARC 所做的事情:轻松创建 autorelease pools,并暂时为你处理 autoreleased 对象的持有和释放。

Autorelease pool 机制

有时候你需要放弃一个对象的所有权,而使用 autorelease pool blocks 是一个好方法。这些 blocks 提供了一个机制,让你可以放弃所有权并避免对象立即被释放。即使有时你需要创建自己的 blocks,或者这样做对你有利,你通常不需要创建它们,但有些情况下你可能需要。

正如以下代码所示,autorelease pool block 是通过使用@autoreleasepool来标记的:

@autoreleasepool {
     //-----
	 // Here you create autoreleased objects.
	 //-----
}

在 block 内部创建的对象在 block 结束时收到释放消息。一个对象在 block 内部接收的释放消息次数与它接收的 autorelease 消息次数相同。

Autorelease pool blocks 也可以嵌套:

@autoreleasepool {
    // . . .
    @autoreleasepool {
        // . . .
    }
    //. . .
}

如果在 autorelease pool block 内部没有发送 autorelease 消息,Cocoa 将返回错误消息,你的应用程序将发生内存泄漏。你通常不需要创建自己的 autorelease pool blocks,但有三种情况下你需要:

  • 在创建一个不基于 UI 的程序时,例如命令行程序

  • 在创建一个生成大量临时对象的循环时

  • 当需要创建一个次要线程时

使用 autorelease pool blocks 减少峰值内存占用

内存占用基本上是程序在运行时使用的内存的主要数量。在无数应用程序中,临时 autoreleased 对象被创建,并且它们会添加到应用程序的内存占用中,直到 block 结束。在某些情况下,允许这种积累直到当前事件循环最终结束,可能会导致过高的开销,你可能希望快速去除这些临时对象;毕竟,它们极大地增加了内存占用。在这种情况下,创建自己的“局部”autorelease pool blocks 是一个解决方案。最终,所有对象都会被释放,从而被释放,有益地减少了内存占用。

这里,你可以看到如何为for循环使用 autorelease pool block:

NSArray *myUrls = <# Sample Array of URLs #>;
for (NSURL *url in myUrls) {
    @autoreleasepool {

/* Two objects are created inside this pool:
NSString "contents", NSError "error"
At the end of the pool, they are released. */

       NSError *error;
       NSString *contents = [NSString
        stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];

         /* Here you can process the NSString contents,
    thus creating and autoreleasing more objects. */
    }
}

有一个包含许多文件 URL 的NSArray,循环一次处理一个文件。在块内部创建的每个对象在结束时都会被释放。

在 autorelease pool 块内部自动释放的每个对象,在块终止后都被视为已废弃。如果你想在 autorelease pool 块结束后保持一个临时对象并使用它,你必须做两件事:在块内部,向该对象发送一个retain消息,然后,只有在块之后,发送autorelease消息,如下面的代码示例所示:

– (id)findTheMatchingObject:(id)myObject {

   id myMatch;
   while (myMatch == nil) {
  @autoreleasepool {

  /*
      This search creates a large number of temporary      
      objects
  */
           myMatch = [self expensiveSearchForObject:myObject];

           if (myMatch != nil) {
	/*
      Keep myMatch in order to use it even after the block is
      ended.
    */
      [myMatch retain];
      break;
            }
        }
    }
     /*
        Here - outside the block - you send it an autorelease message and return it to the method's invoker
    */
    return [myMatch autorelease];
}

如前述代码中的注释所解释,通过在 autorelease pool 块内部向myMatch发送retain消息,然后,只有在块之后,再发送autorelease消息,可以增加该对象的生存期,使其能够接收外部消息并正确地返回给方法的调用者。

Apple 自动释放类的概述

正如之前所说,Cocoa 框架为许多基本类提供了带 autorelease 的工厂方法,例如NSStringNSArrayNSDictionaryNSColorNSDate。然而,同时也有一些类值得特别注意。

NSRunLoop

当使用NSRunLoop时,在每次运行循环的开始,都会创建一个 autorelease pool,并且它只会在这次运行循环结束时被销毁。为了澄清,在它内部创建的每个临时对象将在运行迭代的结束时被释放。如果你在块内部创建大量临时对象,这可能并不有利;在这种情况下,你应该考虑创建一个新的 autorelease pool,如下所示:

NSRunLoop

以下代码演示了之前讨论的内容:

id myPool = [NSAutoreleasePool new];
[myObject somethingThatCreatesManyObjects];
[myPool drain];

注意,为了结束 autorelease pool,我们发送的是drain消息而不是release消息。这样做的原因是因为在垃圾回收器模式下,Objective-C 运行时会简单地忽略release消息,而drain消息则不会被忽略,这为回收器提供了一个提示;然而,它并不会销毁 autorelease pool。

Application Kit 在每个迭代的开始时在主线程中创建一个 autorelease pool,并在每个迭代的结束时释放它,从而免除了在事件处理过程中创建的所有 autorelease 对象。

基本上,iOS 中的运行循环会等待事件的完整执行,直到应用程序执行其他操作。这些事件可以是触摸屏交互、来电等。

对于每个 iOS 事件处理,在事件处理的开始时创建一个新的 autorelease pool,并在事件处理完成后释放(排空)。理论上,可以有任意数量的嵌套 autorelease pool,但请记住它们是在事件处理开始时创建的。

NSException

可能会发生异常,如果确实发生了,异常发生后的自动释放池会自动清理。自动释放池被证明是编写异常安全代码的有力工具。

即使是异常对象本身也应该自动释放:

// This exception will be autoreleased
 +[NSException exceptionWithName:...]

// Or the alternative below
 +[NSException raise:...]

使用前面提到的任何一种模式,如果抛出异常,将正确释放内存。它还会在垃圾回收器模式下释放内存,即使在这个 GC 模式下不是必需的:

    id myObj = [[[SampleClass alloc] init] autorelease];
    ThisMightThrowAnException();

    id myObj = [[SampleClass alloc] init];
    @try {
        ThisMightThrowAnException();
    } @finally {
        [myObj release];
    }

ARC 和自动释放

ARC 仍然使用自动释放作为机制,但除此之外,其编译代码被创建为与 MRC 编译代码无问题地交互操作,因此自动释放存在。

尽管 ARC 为我们很好地处理了内存管理,但仍然存在需要使用自动释放的情况。有时,我们创建大量临时对象,其中许多只使用一次。在这种情况下,你可能希望释放它们使用的内存。

为了将这些对象释放到自动释放池而不是等待它们自然释放,请查看以下非 ARC 环境中的代码示例:

/*
  -------------------------------------------------------
  Non-ARC Environment with Memory Leaks
*/
@autoreleasepool 
{
  // No autorelease call here
   MyObject *obj = [[MyObject alloc] init];

   /* Since MyObject is never released its
     a leak even when the pool exits
  */
}

  /*
  -------------------------------------------------------
  Non-ARC Environment with NO Memory Leaks
*/
@autoreleasepool 
{
  // Memory is freed once the block ends
  MyObject *obj = [[[MyObject alloc] init] autorelease]; 
}

以下示例代码是为 ARC 环境编写的:

/*
  -------------------------------------------------------
  ARC Environment
*/
@autoreleasepool 
{

    MyObject *obj = [[MyObject alloc] init]; 
  /* 
     No need to do anything once the obj variable
     is out of scope. There are no strong pointers
     so the memory will be free
  */

}

/*
  -------------------------------------------------------
  ARC Environment
*/
MyObject *obj; // Strong pointer from elsewhere in scope

@autoreleasepool 
{
    obj = [[MyObject alloc] init]; 
    // Not freed still has a strong pointer 
}

自动释放池块和线程

如果你正在应用程序套件的主线程之外进行 Cocoa 调用,你需要创建自己的自动释放池。例如,你可能创建了一个仅使用基础库的应用程序,或者分离了一个线程。

如果你的应用程序生成大量自动释放对象,而不是维护单个自动释放池,强烈建议你频繁地排空自动释放池并创建一个新的。这种行为在应用程序套件的主线程上被使用。如果你忽视了这一点,你的自动释放对象不会释放,从而增加了内存占用。另一方面,如果你的线程没有进行 Cocoa 调用,你可以轻松忽略这一建议。

摘要

在本章中,我们回顾了自动释放池及其正确使用方法。我们还强调了NSAutoreleasePool和新的@autoreleasepool类及其好处之间的差异。

在下一章中,我们将讨论与对象创建和初始化相关的一些概念,例如不可变性、继承等。我们将深入研究如单例模式等设计模式,这些模式在 iOS SDK 中常用,例如具有名为sharedApplication方法的UIApplication类。我们还将探讨属性作为定义类意图封装的信息的方式。我们还将探讨在第四章中自定义方法和格式说明符,对象创建和存储。下一章我们将涵盖大量内容,所以请坐稳,在我们前往第四章,对象创建和存储时,请紧握不放!

第四章:对象创建和存储

在本章中,我们将更深入地探讨对象和类,展示它们创建、处理和定制的机制。

我们将涵盖以下主题:

  • 对象创建和初始化

  • 对象不可变性

  • 对象可变性

  • 对象继承

  • 便利初始化器

  • 单例模式

  • 使用 @property

  • 类的类型

  • 自定义方法

  • 格式说明符

对象的创建和初始化

对于开发者来说,构建 iOS 和 OS X 应用程序需要在创建和处理对象上花费大量时间。在 Objective-C 中,像任何其他面向对象编程语言一样,对象就像一个具有预定义行为的数据包。我们可以将应用程序视为一个包含对象的 环境,这些对象相互连接,传递和接收信息,例如如何构建图形界面、如何处理用户交互、如何以及在哪里存储和获取数据、如何执行计算等等。对象可以执行的任务的复杂性可能非常大,但这并不反映在创建对象的复杂性上。

Cocoa(用于 OS X)和 Cocoa Touch(iOS)已经提供了一个库,其中包含了一个广泛的对象列表,您可以将其直接使用或基于它们创建自己的对象——我们称之为代码重用。

最重要的发展过程之一是思考应用程序的基本结构,当你决定使用哪个对象、如何组合、定制,以及它们如何通信以生成预期的输出等。其中一些由 Cocoa 和 Cocoa Touch 提供供即时使用,如 NSString、NSArray、NSDictionary、UIView 和 UILabel,但这种重要性归因于其他人可能需要定制以执行所需操作,或者为了创建独特的框架——为您的应用程序提供功能。

什么是类?

在面向对象编程方法中,对象是类的实例。类将确定对象的行为,它接收的消息(方法),有时以及谁有权发送这些消息以获得响应。

一个类描述了指定对象的属性和行为,就像房子的蓝图会描述房子的属性,例如房子里的门数。同样,对于一个数字,名为 NSNumber 的类的实例,其类提供了许多获取、分析、比较和转换对象内部数值的方法。

除了存储在类多个实例中的内部内容外,所有属性和行为表现相同。查看以下示例:

/*
  =============================================
  Our object is created here as instance of NSNumber.
  We directly assign a float number to it;
  =============================================
*/

NSNumber *sampleNumber = @(3.1415);

/*
  =============================================
  Now, we send the built-in message "intValue" to convert the float value stored in it to an integer value.
  =============================================
*/

NSNumber firstNumber = @([ sampleNumber intValue]);

我们的数值对象 firstNumber 现在具有数值 3,这是一个整数,在发送了 NSNumber 类中预定义的 intValue 消息之后。对象将按照预期行为,将其值转换为整数。该类的任何对象实例都将以相同的方式行事。

对象被创建出来是为了以不同的预期方式使用,但您不需要知道它们行为内部机制是如何发生的,这也就是封装。相反,唯一的要求是知道如何处理对象以实现您想要的行为。这意味着您需要知道发送给对象的预定义消息。如果您有一个包含六个大写字母的NSString类的字符串实例,并且希望它们变为小写,您只需要知道要发送的消息:

/*
  =============================================
  We create our string with the uppercase characters: "QWERTY"
  =============================================
*/

NSString *sampleString = @"QWERTY";

/*
  =============================================
  Now, we send a message to it, requesting to convert the uppercase characters to lowercase
  =============================================
*/

sampleString = [sampleString lowercaseString];

/*
  =============================================
  After this process, our string has now the characters: "qwerty"
  =============================================
*/

为了指定对象的使用意图,我们使用类接口。它定义了一个公共接口,用于在代码的其他部分使用,而不仅仅是类本身。

为了创建自己的类,请转到菜单栏中的文件 | 新建,或者直接点击 Command + N,根据您的项目选择iOSOS X,然后选择Cocoa 类Cocoa Touch 类。之后,您可以命名您的类并选择其超类(它将从中继承)。Xcode 将自动为您创建一个头文件和一个实现文件,.h.m。与其他编程语言一样,头文件相当于一个摘要,快速查看类中的内容,将要使用的内容,等等。

您的公共方法和属性将在头文件中声明。在这里,您可以看到一个新创建的头文件示例(mySpecialTableViewController.h):

类

我们的这个类名为mySpecialTableViewController,是UITableViewController的一个子类。它创建了一个 UI 元素,正如其名所示,一个表格视图,这在 iOS 应用中非常常见。

仍然在我们的头文件中,我们将创建一个公共属性NSArray,用于接收和存储将在每个UITableViewCell上显示的数据。我们的表格视图将显示编程语言列表:

类

通过在创建时指定超类,Xcode 已经为您准备好了内置的可用/必需方法,以便运行它。正如我们在实现文件(mySpecialTableViewController.m)中看到的那样,我们只需要实现我们的代码:

类

我们的表格视图将很简单,只显示存储在myProgrammingLanguages数组中的不同单元格上的编程语言。它将只有一个部分,这意味着我们可以在numberOfSectionsInTableView:方法中自由返回这个数字:

类

下一个修改是指定行数,也就是单元格的数量。如果这个数字依赖于可能不同的属性,我们就不能像处理部分数量那样硬编码它;相反,我们返回我们的数组所持有的对象数量:

类

在 Objective-C 中创建表格视图的下一步是设置单元格的内容。我们使用tableView:cellForRowAtIndexPath:方法(已在实现文件中提供)。默认情况下,它是注释掉的。取消注释该方法以便使用它:

类

你首先应该注意到的第一件事是,它创建了UITableViewCell并将其返回以在表格视图中显示。这两个步骤之间是我们配置单元格的地方。

UITableViewCell类已经包含一个名为textLabel的属性。我们将使用它来显示存储在myProgrammingLanguages数组中的值。一旦tableView:numberOfRowsInSection:方法返回数组中的元素数量,对于每次迭代,它都会配置并返回数组中相应项的单元格。第一个单元格是第一个项,第二个单元格是第二个项,依此类推。在这个方法中,当前单元格已经是indexPath的正确单元格,但为了获取正确的值设置给它,我们使用indexPath.row来选择数组中的正确项:

类

上述代码将myProgrammingLanguages数组中的第一个元素设置为第一个单元格的textLabel属性,依此类推,直到达到表格视图中的行数(数组中元素的数量)。

通过在viewDidLoad方法中硬编码我们的数组,设置myProgrammingLanguages的项,并构建我们的项目,我们能够看到表格视图中每个单元格上的数组项:

类

这里,你可以看到我们的自定义UITableViewController,包含三个UITableViewCell类,以及myProgrammingLanguages数组中的项:

类

注意

使用[tableView dequeueReusableCellWithIdentifier:@"anyReusableIdentifier" forIndexPath:indexPath]创建单元格时,为单元格设置一个标识符,以便在它不再可见于屏幕上时与其他内容一起重用。

例如,如果一个表格视图有 15 个元素,在你的 iOS 设备上,屏幕上可以看到 12 个单元格,当你向上滚动以查看其他 3 个元素时,仍然会有 12 个单元格可见。在这种情况下,使用重用标识符,而不是创建 15 个UITableViewCells,它至少会创建 13 个不同的单元格(11 个完全可见的单元格和 2 个部分可见的单元格),当一个单元格从屏幕上消失(向上滚动)时,它会被重用来加载最新的可见元素,出现在底部。

对象不可变性

大多数由 Cocoa 和 Cocoa Touch 提供的类都使用不可变值创建对象。简而言之,一个不可变对象的内容只设置一次,之后永远不能修改其值。这些对象的内容在创建时指定。对象的创建可能发生在初始化过程中或之后,但只发生一次。

这里,我们可以看到一个初始化和创建的同时进行的数组。其内容是不可变的:

/*
  =============================================
  sampleArray is allocated, initialized and created with the strings "Item 1" and "Item 2"
  =============================================
*/
NSArray *sampleArray = [[NSArray alloc] initWithArray:@[
              @"Item 1",
              @"Item 2"]];
//This will throw a compile time error as NSArray is not mutable.
[sampleArray addObject:@"Item 3"];

在前一行代码中,[sampleArray addObject:@"Item 3"];将显示一个编译时错误,因为sampleArray被声明为NSArray而不是NSMutableArray,所以sampleArray在初始化后不能添加任何对象。

现在,我们创建另一个数组,首先在创建之前初始化它,这可能在代码的某个地方稍后发生:

/*
  =============================================
  secondSampleArray is allocated and initialized but not yet created.
  =============================================
*/
NSArray *secondSampleArray = nil;

/*
  =============================================
  Later in our code, we can create it setting contents to it, but it also happens once, the contents won't be changed.
  =============================================
*/
secondSampleArray = @[@"Item 1", @"Item 2"];

你可以看到我们将secondSampleArray设置为nil,在 Objective-C 中,nil表示secondSampleArray没有值或地址。只有在之后我们才将两个NSString字符串"Item 1""Item 2"插入到secondSampleArray中,使数组的大小变为 2。

对象可变性

Cocoa 和 Cocoa Touch 还为其不可变类提供了一些可变版本。一旦创建了一个可变对象,其内容可以被部分或完全删除或修改。正如我们在前一个主题中看到的不变数组对象——NSArray的一个实例——现在我将向你展示它的可变版本,即NSMutableArray类,我们将从这个类创建对象,如以下代码所示:

/*
  =============================================
  We will create now a mutable version of an array, using the class NSMutableArray.
  =============================================
*/
NSMutableArray *mutableSampleArray = [[NSMutableArray alloc] init];

/*
  =============================================
  Now, we assign to it the list of strings:
  "String 1", "String 2", "String 3"
  =============================================
*/
mutableSampleArray = @[@"String 1",
       @"String 2",
        @"String 3"];

/*
  =============================================
  Later, we change the 2nd item of the list with the string "Replacement String", having our array the list: "String 1", "Replacement String", "String 3"
  The indexes are 0 based and starts from 0
  =============================================
*/

[mutableSampleArray replaceObjectAtIndex:1 withObject:@"Replacement String"];

类的可变版本(在我们的例子中是NSMutableArray)与原始不可变版本NSArray有许多相似之处;然而,它们是不同的类。试图使用一个不可用于另一个的方法将生成编译错误。通常,不可变性是你应该尝试使用的,因为不可变性提供了一个保证,即在使用对象时其值不会改变。不可变性在字符串和字典等使用时也会带来性能优势,因为可变性在修改字符串或字典时需要分配和释放内存块,这会带来一些开销。

NSArrayNSMutableArray的情况下,NSMutableArray不是线程安全的,如果在多线程中使用可能会出现奇怪的 bug。因此,通常情况下,除非你确实需要NSMutableArray,否则尽量使用NSArray作为默认的数组。

继承

要理解继承,可以将其想象成一个完美的生物树,你从父亲那里继承了一些行为特征,但不仅如此,你还有自己的。在 Objective-C 中,当一个类从另一个类继承时,就会发生类似的事情。

基本样本是 Cocoa 和 Cocoa Touch 提供的以NS开头命名的类,例如NSStringNSArrayNSDictionary。它们都继承自NSObject。每个类都有它们特有的方法来处理它们所持有的不同类型的内容,但它们都共享如allocinit这样的方法。这两个从NSObject继承来的类方法分别分配内存和初始化对象:

继承

alloc方法很少被覆盖,执行单一任务并为创建的对象分配内存。然而,另一个继承示例是init方法,它也是从NSObject继承的。它在每个子类中进行了修改,创建了其他初始化方法以快速分配内容给对象。这些新的init方法是从原始的init方法继承的。这是一个NSString的例子:

  /*
  =============================================
    The variable is allocated and initialized but still has no content, its value is nil.
  ============================================= */
  NSString *simpleInitializedString = [[NSString alloc] init];
  /*
  =============================================
    Allocated and initialized by it's custom method, initWithString:, inherited from init. In this case, the variable is initialized with a content, "Hey!"
  ============================================= */
  NSString *customInitializedString = [[NSString alloc] initWithString:@"Hey!"];

继承

便利初始化器

分配和初始化方法将为对象的内容分配一块内存,并将其设置为空值,直到你分配自己的值。空值取决于对象的类型:布尔(BOOL)对象接收值NO,整数(int)接收0,浮点数(float)接收0.0,其余对象接收nil

你可以先为你的对象分配内存,然后在代码中稍后初始化它,但这根本不推荐。

另一方面,你可以使用甚至创建我们所说的便利初始化器,这些初始化方法接收参数以分配不同和/或额外的值给实例变量。

为了更好地理解,我们现在将创建我们自己的对象类,并创建便利初始化器以用于不同的场景。首先,我们将创建一个从NSObject继承的类。它将返回一个浮点数,这是乘法分数的结果;我们将称之为MultiFraction

便利初始化器

在我们的头文件MultiFraction.h中,我们将指定要包含在我们对象中的实例变量。它将包含三个值,我们将使用property关键字来定义MultiFraction类打算封装的信息,在这种情况下是类型为NSInteger的对象,分别命名为firstNumeratorsecondNumeratordenominator

便利初始化器

在实现文件MultiFraction.m中,通过省略init方法,它将使用从超类继承的初始化方法,在我们的例子中是NSObject,这将返回一个nil值。然而,我们想要实现一个便利初始化器,接受三个参数,将值保存以便其他方法使用以执行计算,并返回其结果。我们的初始化方法将被命名为initWithFirstNumerator:secondNumerator:denominator:

便利初始化器

在我们的初始化方法内部,我们将存储传递给我们的对象的参数到各自的实例变量中,以防我们将来想要访问这些值中的任何一个,而不是直接计算结果:

便利初始化器

现在,我们可以在我们的 Xcode 项目中其他地方创建我们的对象,通过导入我们的头文件:

#import MultiFraction.h

/*
  =============================================
  Creating a MultiFraction object with the default init method, inherited from NSObject.
  ============================================= */
MultiFraction *firstMultiFraction = [[MultiFraction alloc] init];
// Later, when calling a method to calculate the fraction we will
// get a nil if we handle our instance variables or an error, if
// we try to calculate as they are, nil values.

/*
  =============================================
  Creating a MultiFraction object with the convenience initialization method we've created.
  ============================================= */
MultiFraction *secondMultiFraction = [[MultiFraction alloc] initWithFirstNumerator:25 secondNumerator:3 denominator:4];
// For the secondMultiFraction, when trying to calculate the
// fraction, we will get 18.75 as a float, if we take any
// argument as float when calculating the result.

Objective-C 程序员的职责

如果您在其他编程语言(如 Java)中有所经验,现在正在转向 Objective-C,请忘记构造函数,它们在 Objective-C 中不存在。构造函数是语言级别的结构,它合并了分配和初始化操作,但它们有约束:

  • 它们不返回任何内容。虽然 Objective-C 类的初始化方法+ (void) initialize不返回任何内容,但 Objective-C 类的默认(id) init方法返回一个id类型的对象。

  • 构造函数的名称必须与类名相同。

  • 当您调用超类时,作为第一条语句是必须的。

最后一点确保您不会处理垃圾数据,但这是一个限制。在 Objective-C 中,就像在 C 中一样,如果没有这个限制,您,作为程序员,将有更多的灵活性和能力,但这也意味着您需要负责处理垃圾数据。

单例模式

除了负责垃圾回收外,一个优秀的程序员还应该了解编程设计模式。设计模式是解决常见问题的解决方案,通常是可重用的代码解决方案,使开发者的生活更加轻松。在本节中,我将向您展示单例模式。单例在您需要一个单一实例并需要管理该单一实例(如写入日志文件)时非常有用。然而,单例可能会被误用为全局变量,这会导致不良的编程实践。单例也是通过静态方法实现的,这对单元测试来说并不好,因为它们不能被模拟或存根。因此,仅在正确的上下文中使用单例,而不是在遇到每个情况时都使用。

在 Objective-C 中,一次可以有多个类的实例(对象)。然而,如果您不需要呢?如果您出于某种原因只需要一个实例,而不需要更多,并希望避免该类的多个实例,在这种情况下,您使用单例模式。它确保只有一个类的实例,并且有一个全局方法可用于它。

苹果公司在UIScreen类中已经实现的一个例子是mainScreen方法。它是全局可用的,并返回其类的一个实例,确保它是唯一的。原因很明显,我们不需要超过一个主屏幕。它可以从项目的任何地方调用,如下所示:

 [UIScreen mainScreen]

当您第一次调用该方法时,实例尚未创建。它将被初始化并按预期返回;然而,从第二次调用该方法开始,它不会创建一个新的实例,而是返回现有的一个。这就是它确保只有一个实例存在的方式。让我们通过以下示例代码来了解:

在头文件中,我们首先创建一个全局方法来访问其实例:

@interface connectionLibrary : NSObject
+ (connectionLibrary*)mySharedInstance;
@end

然后,在你的实现文件中,实现这个方法,如下所示:

+ (connectionLibrary*)mySharedInstance {
  // First, we create a static variable to hold our instance 
  static connectionLibrary *_mySharedInstance = nil;
  /*
  Create a static variable to ensure the instance will be initialized only once
  */
  static dispatch_once_t initOnce;
/*
    Now, the core of the singleton pattern is GCD, Grand Central Dispatch, that executes a block where the initialization method is never called once the class was already initiated.
*/
    dispatch_once(&initOnce, ^{
    _mySharedInstance = [[connectionLibrary alloc] init];
});
  return _mySharedInstance;
}

现在,您可以从代码的任何地方初始化和访问这个实例:

connectionLibrary *sharedInstance = [connectionLibrary mySharedInstance];

创建@property

在对象中存储数据有两种方式,它们是属性和实例变量。后者应仅用于对象和由类本身独家处理的数据,而不是来自外部。另一方面,属性和值对于对象和值是可从外部(由其他类)访问的。

当使用实例变量时,你可以创建公开或私有的实例变量。区别基本上在于你声明它们的位置,有时你需要它们可以被其他类访问,而在其他情况下,没有必要将它们暴露给其他类。如果它们在头文件中作为@interface块的一部分声明,则具有公共作用域,如果它们在实现文件中作为@implementation块的一部分声明,则具有私有作用域。通常,它们应该是私有的:

@implementation Book {
  int _numberOfPages;
  int _numberOfChapters;
  NSArray *_authorsInfo;
}

为了更容易理解你的代码,实例变量以一个下划线开头;这不会影响它们的工作方式,但它是一个高度推荐的约定。

实例变量是私有的,只能由类或子类访问,并且它被包含它的类封装,而属性是公开的,可以被其他类访问。属性在作为类扩展的一部分声明时也可以是私有的,但它们通常是公开的,因为你想从外部访问它们。通过访问,有两种选项,获取或设置其内容。Objective-C 会自动为每个声明的属性生成获取器和设置器。为了声明具有公共作用域的属性,请在你的头文件中按照以下方式操作:

@interface Book : NSObject
@property (strong, nonatomic) NSString *chapterNote;
@end

上述代码主要告诉其他类Book类有一个公开属性,可以通过chapterNote访问:

Book *objCBook = [[Book alloc] init];
// This is our setter, we are setting an value to it
objCBook.chapterNote = "I really love this chapter";
//This non dot syntax setter is also valid [objCBook setChapterNote:@"I really love this chapter"];
/*
  This is our getter, we get the value hold on chapterNote and save it in myLastNote
*/
NSString *myLastNote = objCBook.chapterNote

创建自定义方法

在 Objective-C 中,声明方法时以-+开头,正如你将在本节中看到的那样。后者声明了一个静态方法,而前者-声明了实例方法。作为开发者,你不会经常声明静态方法(以+开头)。

静态方法通常用于你不需要在该方法中创建类的实例时,而实例方法用于你需要该实例来修改其状态时。实例方法更常用,因为实例方法让你可以访问类的实例变量。

要声明一个方法,你需要遵循一种语法。你需要以下实体:

  • 指定方法类型的符号

  • 它将返回的数据类型

  • 方法名称

  • 对于每个参数:

    • 参数类型

    • 参数的名称

  • 方法内的代码

按照我们的示例,在mySpecialTableViewController中,让我们声明一个实例方法,它将接受一个参数,一个字符串(NSString)。我们的方法将返回myProgrammingLanguages数组的内容作为一个单独的字符串。每个对象都将跟随提供的参数。我们的方法将被命名为convertToStringWith

在我们转到实现文件之前,必须在头文件中声明该方法,不这样做可能会导致调用方法时出错,因为头文件定义了暴露给外部的哪些方法。

创建自定义方法

现在,转到实现文件并实现该方法:

创建自定义方法

在此情况下,当调用方法时,如果 myProgrammingLanguages 数组包含以下字符串值:"Objective-C", "Swift", 和 "PHP",结果将是一个包含传递参数的唯一的字符串,如下面的示例所示:

创建自定义方法

有时候你不想向方法传递任何参数。这是可能的;你只需要知道返回的数据类型和方法名称:

-(BOOL) doYouLikeThisBook
{
  return true;
}

关于方法返回的数据类型有两个特殊情况,当你不知道它时和当你不会返回任何内容时。在第一种情况下,你应该使用 id,如下所示:

-(id) initSomethingWithoutKnowingTheType
{
  self = [super class];
  return self;
}

另一方面,如果你不想返回任何内容,使用 void

-(void) storeUserDetails:(NSString *)userName withID(int)userID
{
  self.name = userName;
  self.id = userID;
}

字符串格式化

当处理不同类型的对象时,特别是将它们插入/附加到字符串中,你需要在字符串中指定它们的类型,为此我们使用格式说明符。例如,打印到控制台需要打印一个字符串;这是唯一接受的格式。让我们看看如何将不同的对象插入其中,以便正确打印到控制台:

// Here we print a message, it's already a string.
NSLog(@"I'm a message. A string");

然而,如果你想打印存储在属性或实例变量中的值,你应该在内部指定其类型,以便正确地用外部的值替换它:

/*
    Now we print the string value stored on a property
    The console will print the message: "Hello, Mr. Gaius Julius Caesar"
*/
NSString *myStringObject = @"Gaius Julius Caesar";
NSLog(@"Hello, Mr. %@", myStringObject);

注意消息中的 %@。它指定值是一个字符串。这就是我们如何使用百分号 % 后跟一个特定的关键字(转换说明符)来指定对象的类型。为字符串使用不同的转换说明符会导致编译错误。

大多数说明符支持多种数据类型:

格式说明符 支持的对象类型
%d 整数(有符号整型),32 位
%u 整数(无符号整型),32 位
%x 以十六进制值表示的整数(无符号整型),32 位
%o 以八进制值表示的整数(无符号整型),32 位
%% 打印 %
%f 浮点数,双精度浮点数(点浮点数),64 位
%e 浮点数,双精度浮点数(点浮点数)以科学记数法表示,64 位
%g 浮点数,双精度浮点数(点浮点数)如果指数小于-4,则按 %e 格式,否则按 %f 格式,64 位
%c 无符号字符(无符号字符),8 位
%S 以空指针结尾的 16 位 Unicode 字符数组
%p 以十六进制表示的空指针字符(void *),以 0x 开头
%a 以科学记数法表示的双精度浮点数(点浮点数),以 0x 开头,小数点前有一个十六进制数字,使用小写的 p 来引入指数,64 位
%F 以十进制表示的双精度浮点数(点浮点数)
%hhd BOOL

摘要

在本章中,我们能够详细地看到对象,了解继承的工作原理以及如何利用它来创建更强大的类。你学习了关于对象的可变性和不可变性,实例变量和属性的工作方式,它们是什么,以及除了分配、初始化和自定义方法之外如何创建它们,以及如何创建自己的对象。在下一章中,我们将涵盖应用数据管理,例如资源优化、缓存和数据保存。所以,我们下一章见。

第五章:管理您的应用程序数据

在本章中,您将了解管理应用程序数据的概念,以确保您的应用程序在运行时表现最佳。以下内容将涵盖:

  • 资源优化

  • 磁盘和内存缓存

  • 序列化

  • 不同形式的数据保存

  • 各种数据保存方法的优缺点

我们还将涵盖一些常见的陷阱和假设,人们通常将这些与 iOS 应用程序的开发联系起来。一个例子是图片加载,如果开发者没有仔细规划他们应用程序的正确架构,他们可能会遇到应用程序卡顿或内存不足的情况,从而导致应用程序崩溃。

设备内存

就像所有计算设备一样,iPad 和 iPhone 都有有限的内存,您可能会被诱惑在不考虑内存使用的情况下开发应用程序。这样做并不理想,因为无论您在哪个平台上进行开发,内存优化和管理都应该始终是您心中的首要任务。

让我们来看看 iOS 设备中每个设备有多少内存,我们将从 iPhone 开始:

|   | iPhone 4S | iPhone 5 | iPhone 5C | iPhone 5S |
| --- | --- | --- | --- |
| RAM | 512 MB | 1 GB | 1 GB | 1 GB |

这里是 iPad 的 RAM:

iPad Air iPad Mini 2 iPad Mini Wi-Fi + Cellular iPad 2 Wi-Fi + 3G iPad Mini Wi-Fi iPad 3 Wi-Fi iPad 3 Wi-Fi + Cellular iPad 4 Wi-Fi iPad 2 Wi-Fi
RAM 1 GB 1 GB 512 MB 512 MB 512 MB 1 GB 1 GB 1 GB 512 MB

现在,内存的量看起来确实很令人印象深刻,您可能会深情地回忆起那些旧日子,当时您的旧台式机运行在 256 MB 的 RAM 上,但请记住,iOS 不会让您玩转完整的 512 MB 或 1 GB RAM。操作系统会为您的设备中的系统进程分配一些,您将只能获得可用 RAM 的一个子集用于您的应用程序。

在您的应用程序中,所有内容都将占用内存和存储空间。其中一些最大的罪魁祸首是二进制资源,例如视频和图片,它们甚至可能成为您类对象的大消耗者,如果您在开发时没有注意到它们,它们可能会占用宝贵的空间。因此,让我们从图片优化开始,因为几乎每个应用程序都会以某种方式使用图片。

图片优化

任何应用程序如果没有使用 .png 格式和一些漂亮的图片,看起来都会显得单调乏味。然而,关于图片的一个问题是,它们占用的内存比文件大小所暗示的要多得多。一个单独的 1 MB .png 文件在加载到内存中时,可能会占用其内存大小的两倍或三倍。原因是 PNG 基本上是一种压缩文件格式,就像 ZIP 文件一样。因此,所有的图像数据都被压缩进 PNG 文件中,当你的应用程序需要显示 PNG 图像时,它需要将 PNG 文件加载到内存中,解压缩它,然后才能获取用于代码的图像数据,在这个过程中会消耗更多的内存。所以,如果你的应用程序有 20 MB 的 PNG 文件,你可能会看到 40 MB 或更多的 RAM 分配仅用于图像。因此,以下是一些图像优化的技巧:

  • 将你的图像保存为 PNG-8 而不是 PNG-24,因为 PNG-8 比等效的 PNG-24 消耗更少的 RAM。只有在你需要透明通道时才使用 PNG-24。PNG-8 和 PNG-24 之间的区别在于图像质量和你可以拥有的颜色数量。8 和 24 分别代表每像素 8 位和每像素 24 位。因此,PNG-8 只能支持多达 256 种颜色,而 PNG-24 可以支持多达 1600 万种颜色,所以如果你需要显示颜色丰富的图像,如照片,PNG-24 是最佳选择,而标志和用户界面元素,如图标,可能可以用 PNG-8 来处理。PNG-24 也支持透明度,这对于需要透明背景的图像来说是个优点。因此,了解在哪种情况下使用哪种格式将有助于你减少应用程序的内存消耗。

  • 如果你可以使用 JPG 文件,那么请使用它们,因为它们是一种有损格式,这意味着你会有一些图像退化,但通常这种图像退化对肉眼几乎不可见。然而,请注意,JPG 文件不支持透明度。

PNG 是一种无损格式,这意味着当你使用 PNG 文件时,没有图像退化,但代价是它加载到你的设备中时比有损格式的 JPG 消耗更多的 RAM。

所以,如果可能的话,尽量将 PNG 文件和 JPG 文件的数量控制在绝对最小,并且只有在必要时才使用它们。

懒加载

懒加载是什么?它是一种设计模式,或者是在软件设计中处理事情的一种方式,其中你只在需要时才加载资源,如 PNG、MP3 文件等。这有助于减轻一次性加载所有资源时内存不足的问题。你只有在需要时以“懒”的方式加载它。还有一个优点,那就是它最小化了应用程序的启动时间,因为你只按需加载资源,这需要更少的时间来加载。因此,你在时间上获得了速度提升。

假设您有多个UIView,每个视图有 10 个UIImages,但任何时候只能看到一个视图。如果没有经过适当的思考,您可能会倾向于编写一次性加载所有 10 个UIImages的代码。然而,经过进一步的反思,问题出现了,即是否有必要这样做。如果您将代码重构为仅在用户可以看到该UIView时加载 10 个UIImages,并在用户不再查看它时清理它,然后从下一个视图加载下一批UIImages,这将对该视图可见,这将更好。这将为您增加一些编码工作,但就有效内存使用而言,这种权衡将是值得的。

这是一种最简单的实现,我们只是覆盖了类的获取方法:

- (A_Class *)aObject {
    if (aObject == nil) {//Check if the object exists and if not
        aObject = [[A_Class alloc] init];//then create the object
    }

    return aObject;//returns the object
}

您可以将前面的代码替换为您的类的正常获取方法。前面的代码检查对象是否存在,如果不存在,则创建对象。然而,如果对象已经存在,则不会再次创建它。

控件创建

控件是每个 iOS 应用程序的一部分,它们也会消耗您的设备上的内存,每个实例都会消耗字节和比特的内存。当您创建大量的UITableViewCell类时,例如,您迟早会看到一个消耗大量内存的控件。

此外,像加载图像和从远程服务器获取数据这样的任务被认为是缓慢的过程,并将减慢您的应用程序。我相信您已经使用过 iOS 应用程序,当您向下滚动UITableView视图对象时,您会注意到当新图像被加载到新出现的单元格中时,会有明显的延迟。在这个人们习惯于在桌面和手机上快速加载图像的世界里,这种缓慢和卡顿的用户界面是不可接受的,并且可能意味着用户是继续与您的应用程序互动还是卸载您的应用程序之间的区别。

基本原则是,您绝对不能让用户等待超过 1 秒或 1 毫秒。为了弥补应用程序感知到的缓慢,一个技巧是在显示旋转器后淡入一个图像,以使用户感觉到应用程序实际上并不非常慢,因为有一个动画正在播放。

如果您正在经历巨大的内存使用,这影响了您 iOS 应用程序的可用性,那么重用您的控件是必须的。稍后,我们将介绍如何使用 Xcode 中的Instrument工具来监控内存使用。创建对象是一个昂贵的流程,并且有性能成本。

如果您需要在短时间内动态创建对象,例如快速滚动UITableView视图对象,您将体验到一些延迟,因为您的代码将创建新的UITableViewCell类而不是重用旧的类。

重复使用 UITableViewCell 可以大大提高应用程序的性能。幸运的是,苹果已经为我们创建了重复使用单元格的代码,这可以通过几行代码轻松实现。因此,让我们以以下代码为例,看看 dequeueReusableCellWithIdentifier 方法:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        //create a new cell
    }

    //Do what we need with our cell here

    return cell;
}

查看前面的代码,你可以看到我们尝试使用 dequeueReusableCellWithIdentifier 方法分配一个单元格,如果该单元格已经存在,它将返回对该单元格的指针。接下来,我们的代码(!cell)将检查该指针是否不为空,然后创建单元格。这正是我们在上一节 懒加载 中使用的相同技术,只不过这次我们将此技术应用于 iOS 控件,在这种情况下,是一个 UITableViewCell 对象。这几行代码有三个功能:

  • 这有助于防止在向上滚动时应用程序出现卡顿的情况,因为它消除了创建新的 UITableViewCell 实例的需求。

  • 如果你有一千行数据,但任何给定时间只有四行可见,那么当你只需要创建五个单元格时创建一千个 UITableViewCell 是没有意义的。一些其他单元格可能部分可见,因此也需要创建。所以,只有当单元格需要对用户可见时才会创建这五个单元格,而其余单元格则不会加载。

  • 虽然 UITableViewCell 类本身占用大量内存,但存储一千个这样的类并不容易,通过几行额外的代码,你可以避免不必要的内存使用,并将节省的内存用于代码的其他部分。

缓存

缓存是一个在磁盘或内存中存储资源以实现更快访问的概念。缓存将占用更多空间,但在需要更多地考虑加载速度而不是内存的情况下,缓存可以是一个非常有效的技术。考虑以下常见场景:

  • 下载大文件,如图片甚至电影

  • 将文件写入磁盘

  • 从磁盘读取文件并显示它们

如果你遵循前面提到的常规方法,你将面临的一个瓶颈是文件从磁盘加载缓慢。磁盘访问可能比内存访问慢 10,000 倍甚至 1,000,000 倍,这不会提供良好的用户体验,因为用户在等待你的应用程序从磁盘加载文件时会被保持等待。缓存可以帮助减缓这个问题,因为你的文件被保存在内存中,而读取访问更快。

从用户的角度来看,这是很好的,因为他们不需要等待很长时间来加载文件,并且可以帮助优化应用程序的用户体验,因为每一秒钟的浪费都可能导致用户离开你的应用程序。磁盘或内存上的缓存有其优缺点,如下表所示:

磁盘 内存
存储 持久性,因为当设备关闭时数据不会丢失 临时性,因为当设备关闭时数据会丢失
速度
存储大小 小,因为内存通常小于磁盘存储

所以,作为一个经验法则,最好首先在内存中进行所有缓存,然后在您的应用程序内存不足或遇到内存警告错误时,才将缓存移动到磁盘上。如果您正在下载大文件,如电影,您需要将电影文件存储在磁盘上,因为文件通常无法适应内存。

作为旁注,缓存实现使用了几个算法,例如 最近最少使用MRU)或 最近最少使用LRU)。MRU 表示当缓存满时,缓存将首先丢弃最近最少使用的项目,而 LRU 则相反,将丢弃最少使用的项目。实现策略超出了本书的范围,由制造商决定。

幸运的是,我们不需要编写很多代码来实现高效的缓存。有一些 iOS 缓存库我们可以使用,它们可供我们使用。因此,在本节中,我们将查看最受欢迎的缓存库之一。

SDWebImage

我们将要查看的第一个库名为 SDWebImage。源代码可以通过 Git 克隆从 github.com/rs/SDWebImage 下载,并且它还附带了一个演示项目。因此,让我们看看这个演示项目的重要部分。我已经为您总结了以下步骤:

  1. 打开 Xcode 项目。

  2. 打开 masterviewcontroller

  3. 导入 UIImageView+WebCache.h

  4. 查找 cellforrowatindexpath 方法。

  5. 将此方法命名为 setImageWithURL:placeholderImage

现在,让我们详细看看这些步骤:

打开 SDWebImage Demo.xcodeproj 项目并运行它。您应该看到以下屏幕,其中包含带有图片和文本的表格视图单元格列表:

SDWebImage

如果您点击表格视图单元格,它将显示此屏幕,显示您点击的图片的较大尺寸:

SDWebImage

接下来,打开 MasterViewController 并查找以下代码片段:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"Image #%d", indexPath.row];
    cell.imageView.contentMode = UIViewContentModeScaleAspectFill;
    [cell.imageView setImageWithURL:[NSURL URLWithString:[_objects objectAtIndex:indexPath.row]]
                   placeholderImage:[UIImage imageNamed:@"placeholder"] options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];
    return cell;
}

这里的代码将从服务器获取图片,然后在设备上进行缓存。

要在自己的代码中实现此功能,您需要导入 UIImageView+WebCache.h,然后调用 setImageWithURL:placeholderImage 方法,您可以在其中添加自己的占位符 PNG 和 JPG 图片来替换 @"placeholder"

因此,当您再次运行应用程序时,您会注意到图片不再从服务器拉取,而是从设备上的缓存中提供,因此您会看到图片加载速度更快。

对象序列化

什么是序列化?这是一个很多人觉得难以解释或理解的问题。序列化是将数据结构或对象转换成一种格式的方法或概念,以便将其存储在内存或磁盘上进行存储,或者通过网络链路进行传输。它还可以帮助进行内存管理,因为它提供了一个替代机制,即我们将一些文件保存到磁盘而不是内存中,这对于大文件,如电影文件通常是这种情况。序列化格式包括 JSON、XML、YAML 等。幸运的是,对于 iOS 开发者来说,Apple 提供了一个强大的框架,帮助我们在我们想要进行序列化时移除底层代码。因此,当我们想要将我们的数据结构或对象存储在内存或磁盘上时,我们可以使用 Apple 的框架,如 Core Data 或 NSCoding,它提供了一个抽象层,并隐藏了底层的编码工作。

当涉及到数据保存或序列化时,我们往往倾向于坚持我们最熟悉的方法。然而,这并不是一个好的做法,因为各种方法都有其优缺点,我们在决定最佳方法之前应该考虑我们的用例。在这方面,Apple 为我们提供了一些不同的数据序列化方法,这取决于我们,即开发者,来决定哪种方法最适合我们。其中最简单的方法之一是使用 NSCoding。什么是 NSCoding?NSCoding 是 Apple 提供的一个协议,用于将您的数据编码和解码到缓冲区,然后可以持久化到磁盘进行持久存储。

使用 NSCoding 协议也涉及到 NSKeyedArchiverNSKeyedUnarchiver 方法,因为 NSCoding 是一个协议,它提供了用于将我们的数据结构和自定义对象序列化成可以存储在内存或磁盘上的格式的代理方法。NSKeyedArchiverNSKeyedUnarchiver 是实际将我们的序列化数据存储到磁盘以进行持久存储的方法。因此,为了开始,我们将使用一个示例来帮助我们理解序列化和归档在 iOS 应用程序中的工作方式。

使用以下步骤进行以下示例:

  1. NSCoding 协议添加到您的自定义对象中。

  2. 实现 encodeWithCoderinitWithCoder 方法,并分配您希望存储的值。

  3. 调用 archiveRootObjectunarchiveObjectWithFile 方法来将您的序列化数据保存到磁盘,并从磁盘加载它。

  4. 例如,我们创建了一个名为 OurCustomObject 的自定义对象,然后为了使用 NSCoding 协议,我们需要将其添加到我们的接口声明中:

    @interface OurCustomObject : NSObject <NSCoding>
    {
        bool isReset;
        NSString *userName;
        int score;
    }
    @property (nonatomic, retain) NSString *userName;
    @property (nonatomic, assign) bool  isReset;
    @property (nonatomic, assign) int score;
    @end
    
  5. 然后,我们需要编写 encodeWithCoder 方法来保存数据:

    - (void)encodeWithCoder:(NSCoder *)coder {
        //do encoding to save the data
        [coder encodeBool:isReset forKey:@"isReset"];
        [coder encodeObject:userName    forKey:@"userName"];
        [coder encodeInt:score forKey:@"score"];
    }
    To load the data back into our objects, we add in the initWithCoder method:
    - (id)initWithCoder:(NSCoder *)decoder {
        if (self = [super init]) {
            self.isReset = [decoder decodeBoolForKey:@"isReset"];
            self.userName = [decoder decodeObjectForKey:@"userName"];
            self.score = [decoder decodeIntForKey:@"score"];
        }
        return self;
    }
    
  6. 现在我们已经有了将数据编码和解码成序列化格式的代码,我们需要输入实际的代码来将数据保存到我们的设备磁盘上,因此我们可以使用 NSKeyedArchiver 来执行实际的磁盘写入操作,同时我们使用 NSKeyedUnarchiver 来从磁盘获取数据:

    OurCustomObject *ourObj = [[OurCustomObject alloc] init];
        ourObj.userName = @"John Doe";
        ourObj.isReset   = true;
        ourObj.score = 99;    
       //get our file path
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectoryPath = [paths objectAtIndex:0];
        NSString *filePath = [documentsDirectoryPath stringByAppendingPathComponent:@"OurData"];
        [NSKeyedArchiver archiveRootObject: ourObj toFile:filePath];
    
  7. 然后要从磁盘加载我们的对象,我们只需使用以下代码:

        OurCustomObject *ourObj2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
        NSLog(@"Score is %d", [ourObj2 score]);
        NSLog(@"Name is %@", [ourObj2 userName]);
    

在我们的代码中,没有必要调用initWithCoderencodeWithCoder,因为这些方法调用是在你调用unarchiveObjectWithFilearchiveRootObject时进行的。然而,你需要实现initWithCoderencodeWithCoder,因为这两个方法需要包含必要的代码来编码和解码构成OurCustomObjectisResetuserNamescore变量。正如你所见,与NSUserDefaults相比,NSCoding是一种相对强大的将数据存储到磁盘的方法,代码也相当容易理解和编写。然而,如果你需要更多数据存储功能,NSCoding 可能不是最佳选择,而 Core Data 将是更好的选择,因为它具有更多功能,例如能够执行查询,针对速度优化,支持不同的序列化格式,如 XML、SQLite 或 NSDate,以及其他好处。

SQLite

对于熟悉关系型数据库管理系统RDBMS)的人来说,SQLite 是一个基于关系模型的数据库。一个 SQLite 数据库,作为 iOS 中可用的 RDBMS,具有许多人们熟悉的功能和功能,如 ACID 属性、查询等。Core Data 是 Apple 的数据存储框架,你可以使用 Core Data 将数据存储到 SQLite 数据库中。然而,在某些情况下,你需要使用 SQLite 而不是 Core Data。因此,我将进一步阐述这一点:

  • SQLite 作为一个数据库,除了 iOS 平台外,还可在多个平台上使用。这意味着,如果你正在开发一个需要在多个平台或有可能在其他非 iOS 平台上运行的应用程序,SQLite 将是你需要认真考虑的选项,因为你将避免使用 Core Data 时框架锁定的问题。SQLite 也比 NSCoding 更快,并且它增加了查询功能,这是使用NSUserDefaults时无法实现的。

  • 此外,如果你有 SQLite 的使用经验,并且你的数据存储用例非常直接,没有 Core Data 的使用经验,那么你应该选择 SQLite。

  • 它不需要模型-视图-控制器MVC)概念模型。

现在,这并不意味着当你需要将数据存储到磁盘时,SQLite 应该成为你的默认数据存储解决方案。这是因为还有其他选项,如 Core Data,以及诸如速度和编码的简便性等因素,这些因素将在我们稍后在本章和 Core Data 章节中看到,将在你的决策中扮演重要角色。

SQLite 与 Core Data 的比较

Core Data 是一个丰富且复杂的对象图管理框架,具有许多你为复杂用例所需的功能。在 Core Data 编程指南简介 中,苹果提到 Core Data 框架为与对象生命周期和对象图管理相关的常见任务提供了通用和自动化的解决方案,包括持久化,这意味着它防止你编写大量代码来完成日常数据存储任务。

Core Data 使用的是你的对象模型,这些模型在常用的 MVC 架构中被称为模型。这些模型使你能够存储整个对象,并且与你的 iOS 应用程序中的控制器和视图类紧密相连。因此,使用 MVC 架构的开发者将不会在吸收 Core Data 概念和模型时遇到问题。

使用 Core Data 框架进行开发的工具与 Xcode 深度集成,它使开发者能够快速编写代码并以快速高效的方式布局他们的数据模型,从而节省时间,这让你可以将时间投入到项目的其他部分。

Core Data 框架也适用于 Mac OS,如果你打算创建应用程序的 Mac 版本,这将使你的代码具有可重用性。

通过苹果的 iCloud 存储和计算平台,你可以使用 Core Data 利用 iCloud 在多个设备(如 iPad 等)之间同步你的应用程序和用户数据。iOS 8 通过引入 CloudKit 框架与 iCloud 的集成更加紧密,该框架具有新的功能,例如允许数据集的部分下载,所有这些功能都只能通过 Core Data 实现。

SQLite 是一个纯关系型数据库管理系统(RDBMS),很多人将 Core Data 与 SQLite 混淆。SQLite 是一个 RDBMS,纯粹而简单。因此,它具有许多你将与 RDBMS 相关的特性,例如 ACID 属性、查询等。然而,这就结束了。Core Data 是在数据存储之上的一层抽象,这个数据存储可以是 SQLite 或其他形式的数据持久化,例如 XML 文件。所以,使用 Core Data 仍然可以让你在 SQLite 中存储数据,但有些情况下你可能更愿意使用 SQLite 而不是 Core Data。

如果数据可移植性对你来说是一个重要的特性,那么使用 SQLite 应该是你的首选选择,因为 SQLite 是平台无关的,而 Core Data 仅适用于苹果平台。所以,如果你使用 SQLite,你可以确信你的数据文件几乎可以在支持 SQLite 的任何平台上移动和访问,而不仅仅是苹果支持的平台上。

Core Data 最终是在你的代码和数据库之间的一个抽象层。然而,有时你想要深入到代码的底层,避免抽象层来理解代码是如何工作的。因此,使用 SQLite 将允许你做到这一点,因为它允许你在处理大量数据集时进行底层优化。Core Data 也可以用来抽象对 SQLite 的访问,以节省开发时间并使你的代码更简洁。

最终,关于何时何地使用 Core Data 或 SQLite 并没有硬性规定。在每一个工程项目中,都会有问题和决策需要做出,这涉及到资源数量和平台可扩展性等因素,因为 Core Data 仅支持 Apple 平台,如果你打算支持非 Apple 平台,Core Data 可能不是一个好的选择。因此,使用 Core Data 框架可以让你为简单应用快速找到解决方案,但它也把你绑定在 Apple 的框架上,这阻碍了数据可移植性,就像你创建的应用需要用户数据(如游戏数据)存在于另一个非 Apple 设备上时。如果你使用 Core Data,你将遇到技术锁定。

另一方面,SQLite 允许根据各种原因轻松调整和优化。最终,你的用例复杂性、数据模型以及平台需求将是帮助你做出选择正确选项的因素。

摘要

总结来说,本章涵盖了应用数据的管理,包括将数据缓存到内存以及将数据存储到磁盘上。我们还讨论了在不同情况下使用各种存储框架的优缺点,并给出了一些使用 NSCoding 协议和 SDWebImage 开源缓存框架的代码示例。

本章简要介绍了 Core Data,这将有助于我们在下一章深入探讨 Core Data 时,结合一些代码示例。下一章将全部关于 Core Data 及其用途。

第六章. 使用 Core Data 进行持久化

如果您进行任何严肃的 iOS 开发,数据持久化是您迟早会遇到的事情。毕竟,如果一个应用程序不能保存用户数据,并且需要在您再次启动应用程序时重新填写,那么这个应用程序有什么好处呢?

这就是数据持久化出现的地方。实际上,iOS 开发者有几个数据持久化的选项,从属性列表、二进制格式到 SQLite 等。

与这些选项一样,每个选项都有其优点和缺点,何时使用每种特定的持久化方法将取决于您的用例。您还必须编写特定的代码来处理 SQLite 和二进制数据的数据持久化。Core Data 可以用于将数据存储在plist、SQLite 和其他格式中,这使得它本身就是一个相当强大的框架,正如我们将在本章中看到的那样。

在本章中,我们将涵盖以下主题:

  • 为什么使用 Core Data?

  • Core Data 概念

  • 将 Core Data 付诸实践

  • 进入代码编写

  • 将数据保存到持久存储中

  • 从持久存储中删除数据

为什么使用 Core Data?

您可能正在想,“为什么我必须学习另一种方法,因为我们已经有了这么多方法?”因此,在本节和随后的页面上,我们将看到为什么 Core Data 是 iOS 和 Mac OS 平台上存储数据的首选方式。

您需要知道的第一件事是,Core Data 本身不是另一种数据持久化方法;它实际上是 SQLite、plists 等之上的抽象。这意味着您实际上可以使用 Apple 的 Core Data API 将数据保存到持久存储中,只需使用 Core Data API,无需编写 plist 特定或 SQLite 特定代码,如果您选择将数据存储为 plists 或 SQLite。这个抽象层说明了为什么 Core Data 如此强大的基本概念。

现在您已经感到震惊,抽象层意味着您只需使用 Core Data API,抽象层将为您处理所有特定存储的代码,因为所有这些高级功能都将帮助您摆脱编写低级代码,这些代码针对每种不同的数据持久化格式,如 SQLite、属性列表等。

Core Data 与 iCloud 紧密集成,并提供了一系列与 iCloud 相关的优势,如数据同步。它还允许您在查询的同时进行实体建模,使其在访问速度方面非常快,并给您选择存储类型的自由,可以是 SQLite、XML 或 NSDate。鉴于 Core Data 提供的所有优势,它需要与 NSCoding 相比编写更多的代码。然而,正如我们稍后将会看到的,代码量并不多,Core Data 框架也不难理解。

关于 Core Data,我还想提到几点:由于它与 Apple 平台紧密集成,你可以访问许多相关类,如NSFetchedResultsController,这使得你轻松地将实体添加到UITableViews中。它还提供了一个不错的图形对象模型编辑器,允许你轻松地思考你的对象/实体设计,并使用 Core Data 的视觉工具轻松地概念化它。有了所有这些好处,现在让我们深入了解 Core Data。

理解 Core Data 概念

Core Data 允许你以多种存储类型存储数据。因此,如果你想使用其他类型的内存存储,如 XML 或二进制存储,你可以使用以下存储类型:

  • NSSQLiteStoreType: 这是最常用的选项,因为它只是将你的数据库存储在 SQLite 数据库中。

  • NSXMLStoreType: 这种存储方式会将你的数据保存在一个 XML 文件中,速度较慢,但你可以打开 XML 文件,它将是可读的。这个选项可以帮助你调试与数据存储相关的错误。然而,请注意,这种存储类型仅适用于 Mac OS X。

  • NSBinaryStoreType: 这种存储方式占用的空间最少,并且由于它将所有数据存储为二进制文件,因此速度最快,但整个数据库二进制文件需要能够适应内存才能正常工作。

  • NSInMemoryStoreType: 这种存储方式将所有数据保存在内存中,并提供最快的访问速度。然而,要保存的数据库大小不能超过内存中可用的空闲空间,因为数据是保存在内存中的。然而,请注意,内存存储是短暂的,并且不会永久保存在磁盘上。

接下来,有两个概念你需要了解,它们是:

  • 实体

  • 属性

现在,这些术语可能对你来说很陌生。然而,对于那些了解数据库的人来说,你会知道它们是表格和列。所以,为了便于理解,可以把 Core Data 实体看作是你的数据库表格,Core Data 属性看作是你的数据库列。

因此,Core Data 通过使用实体和属性的概念来处理数据持久化,这些是抽象数据类型,实际上将数据保存到属性列表、SQLite 数据库或甚至 XML 文件(仅适用于 Mac OS)。回顾一下,Core Data 是 Apple 的企业对象框架EOF)的后代,EOF 由 NeXT, Inc 在 1994 年引入,EOF 是一个对象关系映射器ORM),但 Core Data 本身不是一个 ORM。Core Data 是一个用于管理对象图框架,它的一项强大功能是它允许你在必要时将对象放入和移出内存,从而处理通常无法适应内存的极大规模数据集和对象实例。Core Data 将 Objective-C 数据类型映射到相关数据类型,如字符串、日期和整数,分别由NSStringNSDateNSNumber表示。所以,正如你所见,Core Data 不是一个需要学习的全新概念,因为它基于我们所有人都知道的简单数据库概念。由于实体和属性是抽象数据类型,你不能直接访问它们,因为它们在物理上不存在。因此,要访问它们,你需要使用 Apple 提供的 Core Data 类和方法。

Core Data 的类实际上相当多,你不会经常使用它们的所有类。所以,这里有一个更常用类的列表:

类名 示例用途
NSManagedObject 访问属性和数据行
NSManagedObjectContext 检索数据和保存数据
NSManagedObjectModel 存储
NSFetchRequest 请求数据
NSPersistentStoreCoordinator 持久化数据
NSPredicate 数据查询

现在,深入探索这些类:

  • NSManagedObject:这是一个你将使用并对其执行操作的记录,所有实体都将扩展这个类。

  • NSManagedObjectContext:这可以被视为一个智能便签本,在你从持久化存储中检索对象后,临时副本会被带入其中。因此,在这个智能便签本中进行的任何修改都不会保存,直到你将这些更改保存到持久化存储,即NSManagedObjectModel。如果你愿意,可以将其视为实体集合或数据库模式。

  • NSFetchRequest:这是一个描述搜索条件的操作,你将使用它从持久化存储中检索数据,类似于大多数开发者熟悉的常见 SQL 查询。

  • NSPersistentStoreCoordinator:这就像是粘合剂,将你的托管对象上下文和持久化存储关联起来。

  • NSPersistentStoreCoordinator:没有这个,你的修改将不会保存到持久化存储中。

  • NSPredicate:这用于定义在搜索或内存过滤中使用的逻辑条件。基本上,这意味着NSPredicate用于指定数据如何被检索或过滤,并且您可以与NSFetchRequest一起使用,因为NSFetchRequest有一个谓词属性。

付诸实践

现在我们已经涵盖了 Core Data 的基础知识,让我们继续一些代码示例,展示如何使用 Core Data,其中我们使用 Core Data 在Customer表中存储客户详细信息。我们想要存储的信息是:

  • name

  • email

  • phone_number

  • address

  • age

注意

请注意,所有属性名称必须全部小写,并且不应包含空格。例如,我们将使用 Core Data 存储之前提到的客户详细信息,以及使用 Core Data 框架和方法检索、更新和删除客户记录。

  1. 首先,我们将选择文件 | 新建 | 文件,然后选择iOS | Core Data付诸实践

  2. 然后,我们将通过点击屏幕左下角的添加实体按钮来创建一个新的实体,名为Customer,如下截图所示:付诸实践

  3. 然后,我们将继续添加Customer实体的属性,并给它们分配适当的类型,例如,对于nameaddress等属性,可以使用String类型,对于age可以使用Integer 16类型:付诸实践

  4. 最后,我们需要添加CoreData.framework,如下截图所示:付诸实践

  5. 因此,我们已经创建了一个由Customer实体和一些属性组成的 Core Data 模型类。请注意,所有核心模型类都有.xcdatamodeld文件扩展名,对于我们来说,我们可以将 Core Data 模型保存为Model.xcdatamodeld

  6. 接下来,我们将创建一个示例应用程序,该应用程序以下列方式使用 Core Data:

    • 保存记录

    • 搜索记录

    • 删除记录

    • 加载记录

现在,我不会涵盖 UIKit 和 storyboard 的使用,而是专注于核心代码,以向您展示 Core Data 的工作示例。因此,为了开始,这里有一些应用程序的截图供您查看,以了解我们将做什么:

  • 这是启动应用程序时的主屏幕:付诸实践

  • 插入记录的屏幕如下所示:付诸实践

  • 列出我们从持久存储中所有记录的屏幕如下所示:付诸实践

  • 通过从持久存储中删除记录,您将得到以下输出:付诸实践

进入代码

让我们从代码示例开始:

  1. 对于我们的代码,我们首先需要在AppDelegate.h文件中的AppDelegate类中声明一些 Core Data 对象,例如:

    @property (readonly, strong, nonatomic) NSManagedObjectContext
    *managedObjectContext;
    @property (readonly, strong, nonatomic) NSManagedObjectModel
    *managedObjectModel;
    @property (readonly, strong, nonatomic) NSPersistentStoreCoordinator
    *persistentStoreCoordinator;
    

    这些在这里声明,以便我们可以轻松地从任何屏幕访问它们。

  2. 接下来,我们将声明在 AppDelegate.m 中每个对象的代码,例如以下创建 NSManagedObjectContext 实例并返回已存在实例的代码行。这很重要,因为你希望只有一个上下文实例存在,以避免对上下文的冲突访问:

    - (NSManagedObjectContext *)managedObjectContext
    {
        if (_managedObjectContext != nil) {
            return _managedObjectContext;
        }
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _managedObjectContext = [[NSManagedObjectContext alloc] init];
            [_managedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    
        if (_managedObjectContext == nil)
            NSLog(@"_managedObjectContext is nil");
        return _managedObjectContext;
    }
    

    此方法将创建 NSManagedObjectModel 实例并返回该实例,但如果已经存在,则返回现有的 NSManagedObjectModel 实例:

    // Returns the managed object model for the application.
    - (NSManagedObjectModel *)managedObjectModel
    {
        if (_managedObjectModel != nil) {
            return _managedObjectModel;//return model since it already exists
        }
    
        //else create the model and return it
        //CustomerModel is the filename of your *.xcdatamodeld file
        NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CustomerModel" withExtension:@"momd"];
        _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    
        if (_managedObjectModel == nil)
            NSLog(@"_managedObjectModel is nil");
        return _managedObjectModel;
    }
    

    此方法将在不存在时创建 NSPersistentStoreCoordinator 类的实例,并在存在时返回现有实例。我们还将使用 NSLog 方法在我们的 Xcode 控制台中添加一些日志,以告知用户 NSPersistentStoreCoordinator 的实例是否为 nil,并使用 NSSQLiteStoreType 关键字向系统表明我们打算将数据存储在 SQLite 数据库中:

    // Returns the persistent store coordinator for the application.
    - (NSPersistentStoreCoordinator *)persistentStoreCoordinator
    { NSPersistentStoreCoordinator
        if (_persistentStoreCoordinator != nil) {
            return _persistentStoreCoordinator;//return persistent store
        }//coordinator since it already exists
    
        NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CustomerModel.sqlite"];
    
        NSError *error = nil;
        _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    
        if (_persistentStoreCoordinator == nil)
            NSLog(@"_persistentStoreCoordinator is nil");
    
        if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
            NSLog(@"Error %@, %@", error, [error userInfo]);
            abort();
        }
    
        return _persistentStoreCoordinator;
    }
    

    以下代码行将返回设备上存储数据的 URL:

    #pragma mark - Application's Documents directory// Returns the URL to the application's Documents directory.
    - (NSURL *)applicationDocumentsDirectory
    {
        return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    }
    

    如你所见,我们所做的是检查像 _managedObjectModel 这样的对象是否为 nil,如果不是 nil,则返回该对象,或者我们会创建该对象然后返回它。这个概念与我们在第五章中讨论的懒加载概念完全相同,即管理你的应用程序数据。我们将同样的方法应用到 managedObjectContextpersistentStoreCoordinator 上。我们这样做是为了确保在任何给定时间我们只有一个 managedObjectModelmanagedObjectContextpersistentStoreCoordinator 的实例被创建和存在。这是为了帮助我们避免有多个这些对象的副本,这将增加内存泄漏的机会。

注意

注意,在 ARC 之后的世界上,内存管理仍然是一个真正的问题。所以我们所做的是遵循最佳实践,这将帮助我们避免内存泄漏。

在之前展示的示例代码中,我们采用了一种结构,以确保在任何给定时间只有一个 managedObjectModelmanagedObjectContextpersistentStoreCoordinator 的实例可用。

接下来,让我们继续展示如何在我们的持久存储中存储数据。如前一个截图所示,我们有 nameageaddressemailphone_number 等字段,它们对应于我们的 Customer 实体中的相应字段。

注意

本章中的示例代码将在 Packt Publishing 网站上提供完整内容,你可以下载它并直接运行 Xcode 项目。

将数据保存到持久存储

要成功使用 Core Data 进行保存,你需要:

  • NSManagedObject

  • NSManagedObjectContext

  • NSPersistentStoreCoordinator

  • NSManagedObjectModel

因此,在我们的屏幕中,这些变量被保存到我们的 Customer 实体中,以下代码片段为 (IBAction)save:(id)sender 方法执行所有魔法。这将使我们能够从新客户或更新现有客户的信息中保存数据:

- (IBAction)save:(id)sender {
    if ([nameTxtField text].length == 0)
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                message:@"Name must not be empty" delegate:self
                        cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
        return;
    }
    NSString *name = [nameTxtField text];
    NSString *phone = [phoneTxtField text];
    NSString *email = [emailTxtField text];
    NSString *address = [addressTxtField text];
    int age = [[ageTxtField text] intValue];

    //save using core data
    NSManagedObjectContext *context = nil;
    id delegate = [[UIApplication sharedApplication] delegate];
    if ([delegate performSelector:@selector(managedObjectContext)]) {
        context = [delegate managedObjectContext];
    }//prepare the context for saving

    if (customer)//if we are showing existing customer data
    {
        NSNumber *age = [NSNumber numberWithInt:[[ageTxtField text] intValue]];
        [customer setValue:[nameTxtField text] forKey:@"name"];
        [customer setValue:age forKey:@"age"];
        [customer setValue:[addressTxtField text] forKey:@"address"];
        [customer setValue:[emailTxtField text] forKey:@"email"];
        [customer setValue:[phoneTxtField text] forKey:@"phone_number"];
    }
    else
    {
        // Insert new object into the context
        NSManagedObject *newCustomer = [NSEntityDescription insertNewObjectForEntityForName:@"Customer" inManagedObjectContext:context];
        [newCustomer setValue:name forKey:@"name"];
        [newCustomer setValue:phone forKey:@"phone_number"];
        [newCustomer setValue:email forKey:@"email"];
        [newCustomer setValue:address forKey:@"address"];
        [newCustomer setValue:[NSNumber numberWithInteger:age] forKey:@"age"];
    }

    NSError *error = nil;
    // Save the object to persistent store
    NSString *str;
    if (![context save:&error]) {
        str = [NSString stringWithFormat:@"Error saving %@ with localized description %@", error, [error localizedDescription]];
        NSLog(@"%@", str);
    }
    else
    {
        str = @"Customer record saved to persistent store";
        if (customer)
            str = @"Customer record updated to persistent store";
        NSLog(@"%@", str);
    }

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert"
                                        message:str delegate:self
                                cancelButtonTitle:@"OK" otherButtonTitles:nil];

    [alert show];
}

因此,我们需要记住的步骤是:

  1. 获取NSManagedObjectContext的实例,它使用managedObjectModel设置persistentStoreCoordinator

  2. 创建一个NSManagedObject的实例,并设置你想要保存的值。

  3. 使用 NSManagedObjectContext 类型的对象并调用 save 方法,因为上下文将代表你所做的所有更改,你需要调用 save 方法以将上下文中的更改保存到磁盘。

从持久存储中删除数据

现在,我们将继续学习如何从持久存储中删除记录。在我们的表格视图中,我们将使用 NSFetchRequest 的实例来加载客户,如下所示:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    //Get the context first
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];

    //load data from Customer entity
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Customer"];
    self.customers = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];

    [tblView reloadData];
}

在这里,我们将customers声明为一个可变数组,用于存储来自Customer实体的记录:

@property (strong) NSMutableArray *customers;

要删除一条记录,我们只需从customers数组中获取我们的Customer记录,这是一个NSManagedObject的实例,然后使用managedObjectContext的实例调用其上的deleteObject方法,最后调用save方法来保存我们的更新记录:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSManagedObjectContext *context = [self managedObjectContext];

    if (editingStyle == UITableViewCellEditingStyleDelete) {

        NSManagedObject *obj = [self.customers objectAtIndex:indexPath.row];
        [context deleteObject: obj];

        NSError *error = nil;
        NSString *str;
        // Attempt to delete record from database
        if (![context save:&error]) {
            str = @"Cannot delete record! %@", [error localizedDescription];
            NSLog(@"%@", str);
        }
        else
        {
            // Remove customer from table view
            [self.customers removeObject:obj];

            //update tableview
            [tblView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                           withRowAnimation:UITableViewRowAnimationNone];
            str = @"Record removed";
            NSLog(@"%@", str);
        }

        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert"
                                        message:str delegate:self
                                cancelButtonTitle:@"OK" otherButtonTitles:nil];

        [alert show];
    }
}

更新数据

最后,要更新一条记录,比你想象的要简单得多,多亏了抽象层。要更新数据,我们只需在(IBAction)save:(id)sender方法中将值分配给我们的customer对象,这个方法你之前已经看到了:

if (customer)//if we showing existing customer data
{
        NSNumber *age = [NSNumber numberWithInt:[[ageTxtField text] intValue]];
        [customer setValue:[nameTxtField text] forKey:@"name"];
        [customer setValue:age forKey:@"age"];
        [customer setValue:[addressTxtField text] forKey:@"address"];
        [customer setValue:[emailTxtField text] forKey:@"email"];
        [customer setValue:[phoneTxtField text] forKey:@"phone_number"];
    }

在我们设置customer对象的值之后,我们将添加以下代码:

NSError *error = nil;
    // Save the object to persistent store
    NSString *str;
    if (![context save:&error]) {
        str = [NSString stringWithFormat:@"Error saving %@ with localized description %@", error, [error localizedDescription]];
        NSLog(@"%@", str);
    }

在这里,customer是一个NSManagedObject的实例:

@property (strong) NSManagedObject *customer;

更新数据的代码需要添加到以下代码片段之后,在-(IBAction)save:(id)sender方法内部:

if ([delegate performSelector:@selector(managedObjectContext)]) {
        context = [delegate managedObjectContext];
    }//prepare the context for saving

摘要

因此,总结一下,Core Data 并不是特别复杂的东西,使用 Core Data 的代码如我们之前在代码示例中所见,相当直接。Core Data 框架是一个相对容易使用的框架,用于处理数据存储抽象,无需担心不同的数据存储格式。

你必须了解的概念是 Core Data 类,如 NSManagedObjectNSManagedObjectContextNSPersistentStoreCoordinator 等,以及相关的 savedeleteObject 等方法。通过这些简单的代码行,你可以利用 Core Data 框架在高级抽象层面上进行数据持久化,无需关心低级数据格式规范。

在下一章中,我们将介绍键值编程以及它是如何被用来允许我们通知状态变化的。所以,我希望你喜欢我们关于 Core Data 的这一章!

第七章. 键值编程方法

键值编码是一个真正酷的功能,它与键值观察配合得很好。它允许你编写更少的代码,并创建非常优雅的解决方案和代码模块。在实际应用中,有许多情况是某个东西发生变化,而应用的另一部分应该受到影响。问题是,当实例或类的属性发生变化时,你可以做任何事情,包括但不限于检查其值是否有效,当某个值发生变化时向某人发送消息,等等。选项是无限的。

在本章中,我们将涵盖以下主题:

  • 什么是键值编码?

  • NSKeyValueCoding 协议

  • NSKeyValueCoding 行为的手动子集

  • 关联对象

  • 选择器作为键

  • 最大灵活性和处理不寻常的键/值

此外,请注意,NSKeyValueCoding 协议自 Cocoa 中的 Mac OS X 10.0 以来就已经可用,它也出现在了 2008 年 7 月 11 日发布的 iOS 2.0 中。一般来说,iOS 和 Mac 的 API 通常是先在 Mac 平台上出现,然后再在 iOS 平台上出现。

什么是键值编码或 KVC?

键值编码基本上是一种间接访问对象属性的方法,而不是通过实例变量显式获取和设置这些属性。使用 KVC,我们使用字符串作为属性键,它充当一个标识符。它通过传递一个“键”,即一个字符串来获取或设置与该键相关的属性。例如,看看以下代码示例:

@interface DogClass
@property NSString *dog_name;
@property NSInteger number_legs;
@end

DogClass *mydog = [[DogClass alloc] init];
NSString *string = [myDog valueForKey:@"dog_name"];
[mydog setValue:@4 forKey:@"number_legs"];

在前面的代码中,我们创建了具有两个 NSStringNSInteger 属性的 DogClass。然后,我们使用 valueForKeysetValue 通过键值编码分别获取 dog_namenumber_legs 的值。

如果这听起来很熟悉,你可能在使用 NSDictionary 时会注意到语法上的相似性。

另有一个示例代码,你可以参考以获得更多澄清。让我们看看以下代码:

// The following line sets a property, directly.
//Example A
myObject.myProperty = myValue;

/*
  While this other line sets the same property, this time using KVC.
*/
//Example B
[myObject setValue:myValue forKey:@"myProperty"];

一些早期接触 Objective-C 的开发者不喜欢使用点操作符显式设置属性的方法,就像在 myObject.myProperty = myValue 中看到的那样,但这种方法本质上是有帮助的,因为它将设置过程中涉及的属性与设置动作本身分离开来。在这个上下文中,一个正常的设置器是适用的,但编写你自己的设置器意味着你将编写大量的样板代码,这会使你的代码更加冗长。

基本上,你的应用程序的访问器方法将实现由 KVC 确定的方法和模式签名。这些访问器方法的任务是提供一种方式来访问应用程序数据模型中的属性值。它们有两个,setget 访问器。set 访问器——也称为 setters——设置属性的值,而 get 访问器——也称为 getters——获取/返回属性的值。

想象一下,有一个NSTableViewDataSource方法来处理除了默认之外的一行编辑,而不使用 KVC。它应该看起来像以下代码:

- (void)tableView:(NSTableView *)aTableView
    setMyObjectValue:(NSString *)anObject
    forMyTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    if ([[aTableColumn identifier] isEqual:@"myName"])
    {
        [[myRecords objectAtIndex:rowIndex] setName:anObject];
    }
    else if ([[aTableColumn identifier] isEqual:@"myAddress"])
    {
        [[myRecords objectAtIndex:rowIndex] setAddress:anObject];
    }
}

然而,一旦我们可以使用 KVC,方法可以像这样:

- (void)tableView:(NSTableView *)aTableView
    setMyObjectValue:(NSString *)anObject
    forMyTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    [[myRecords objectAtIndex:rowIndex] setValue:anObject forKey:[aTableColumn identifier]];
}

这里展示了 KVC 的本质;它是一个更好的方法,因为每个属性的编辑不需要作为单独的条件来处理。另一个巨大的优点是它的效率,因为一个有数千列的表将由相同的代码处理,甚至不需要添加一行。注意,在第一个例子中,我们需要有两个if循环来处理两个不同的标识符,但使用 KVC,我们可以减少冗长的代码,使用setValue代替,并且只需一条语句就能达到相同的结果。

除了键值编码简化了你的代码之外,实现其兼容访问器是一个有效的设计原则,它有助于数据封装,并使得与键值观察(我们将在后面介绍)和其他技术(如 Cocoa 绑定、Core Data 等)一起工作变得更加容易。

NSKeyValueCoding是一个非正式协议,它提供了 KVC 的基本方法,而NSObject提供了其默认实现。键值编码可以访问三种类型的对象值;它们是属性、一对一关系和一对多关系,我们可以通过字符串间接访问属性。

我们所说的属性只是一个简单的值属性,所以它可能是一个NSStringBoolean值,以及NSNumber和其他不可变对象类型。

当一个对象具有自己的属性时,这些属性被称为属性,它们在对象和属性之间建立一对一的关系。这些属性有趣的地方在于,它们可以改变,而对象本身却没有任何改变。为了更好地理解这一点,可以想象一个NSView实例的 superview 作为一对一关系。一组相关对象构成一对多关系。我们可以在NSArrayNSSet实例中看到这一点,其中NSArrayNSSet实例与一组对象之间有一对多关系。

NSKeyValueCoding协议

我至今展示的每个示例代码中都使用了NSKeyValueCoding协议。我也一直称它为协议,但正如我之前所说的,它是一个非正式协议,一个NSObject类别。

KVC 是一种机制,允许你通过使用字符串“键”间接访问对象的属性。为了启用 KVC,你的类必须遵守NSKeyValueCoding。大多数情况下,你不需要做任何事情就能完成它,因为它是通过NSObject来遵守的。

为了使某个属性的键值编码兼容类,必须实现setValue:forKey:valueForKey:方法以正常工作。

属性和一对一关系的兼容性

如果属性仅仅是属性或一对一关系,你必须确保你的类具有以下规范;一个例子是 [myObject setValue:myValue forKey:@"myProperty"];,这是我们之前看到的:

  • 有一个名为 <key>_<key> 的实例变量,或者有一个名为 -<key> 的实现方法,这是对键值对中键的引用。一般来说,KVC 键以小写字母开头,但对于像 URL 这样的键,如果首字母大写也是可以接受的。

  • 如果属性是可变的,则还需要实现 -set<Ket>:

  • -set<Key>: 方法的实现不应包含任何验证,因为验证将由下一点提到的方实现。

  • 如果验证适用于该键,则必须在此处实现 -validate<Key>:error: 以及你的验证代码。

索引一对一关系的兼容性

使用 NSArraysNSMutableArrays 将会带你了解一对多关系的概念,对于索引多对多关系,你需要确保的关键值编码兼容性要求是:

  • 实现 -<key> 方法,返回一个数组

  • 此外,你可能还有一个名为 <key>_<key>NSArray 实例变量,或者甚至可以继续实现 -countOf<Key> 和以下一个或多个:-<key>AtIndexes:-objectIn<Key>AtIndex:

  • 为了提高性能,你也可以实现 -get<Key>:range:,但这不是必需的

否则,如果你处理的是可变的索引有序多对多关系,这些是你的要求:

  • 至少实现一个方法:-insertObject:in<Key>AtIndex:-insert<Key>:atIndexes:

  • 至少实现一个方法:-removeObjectFrom<Key>AtIndex:-remove<Key>AtIndexes:

  • 作为选项,你甚至可以实现以下方法之一:-replace<Key>AtIndexes:with-replaceObjectIn<Key>AtIndex:withObject:

无序多对多关系的兼容性

NSSets 是无序集合的一个例子,并且也有多对多关系,所以你需要确保的无序多对多关系的键值编码兼容性要求是:

  • 实现 -<key> 方法,返回一个 NSSet

  • 否则,设置一个名为 <key>_<key> 的实例变量

  • 或者实现以下方法:-enumeratorOf<Key>-countOf<Key>,和 -memberOf<Key>:

如果是可变的无序多对多关系,KVC 兼容性会要求你:

  • 实现以下方法中的至少一个:-add<Key>:-add<Key>Object:

  • 实现以下方法中的至少一个:-remove<Key>:-remove<Key>Object:

  • 为了提高性能,你可以实现 -set<Key>:-insert<Key>:

使用NSString键,你可以使用setValue:forKey:valueForKey:方法设置和获取值。这个键是一个简单的字符串,用作对象属性的标识符。一个键必须符合以下规则:以小写字母开头,不应包含空白字符,并使用 ASCII 编码。以下示例键中应用了所有这些规则:mySampleKeypageNumberoddSum

还有键路径,它们基本上是一个由点分隔的两个或多个键的字符串,如pictures.byOwner.forYear。如果你很难理解,可以将其视为如这里所示的 UNIX 目录相对路径,pictures/Vasilkoff/2014

很明显,文件夹 2014 相对于Vasilkoff,而Vasilkoff相对于pictures,反过来又相对于用户的当前目录。在键路径中,第一个键——在我们前面的代码示例中是pictures——相对于接收对象。

例如,使用地址和街道的概念,你可以从地址中推导出街道。所以,如果你使用相同的概念,address.street键路径将从接收对象中获取地址属性的值,然后你可以根据地址对象确定街道属性。

键值编码的优势

  • 大多数属性默认支持NSKeyValueCoding非正式协议。任何从NSObject继承的对象都有对NSKeyValueCoding的自动支持。所以,你的自定义类将不会支持NSKeyValueCoding,除非你明确使其继承自NSObject

  • KVC 会自动查找 setter 和 getter 方法,如果找不到,它甚至会获取或设置实例变量。

  • 在处理多个属性对象时,使用键路径的可能性非常有帮助。

  • 为了通知状态变化,KVC 可以很容易地与NSKeyValueObserving集成,以实现观察者软件模式。

  • 处理未定义键的可能性。

  • 这提供了回退机制。

键值编码的缺点

  • 属性键必须是NSStrings,这意味着编译器没有关于属性类型或其存在的任何信息。因此,无法从 ID 的返回值中检索任何类型的信息,正如你所知,ID 是一个指向 Objective-C 对象的指针。

  • 它的扩展搜索路径使其成为一个非常慢的 KVC 方法。

  • 该类必须提供一个方法或实例变量与属性名称匹配,只有这样,它才能被NSKeyValueCoding找到。如果你的键有误,你的应用程序将在运行时崩溃,而不是在编译时,所以你必须确保你的键拼写正确,以避免崩溃。

NSKeyValueCoding 行为的手动子集

NSKeyValueCoding 协议在查找方法和实例变量时表现不同。在第一种情况下,它会查找方法选择器的名称,而在最后一种情况下,它会查找实例变量的名称。

这可以通过手动完成,如下面的示例所示:

// Manual implementation of KVC setter for method.
NSString *mySetterString = [@"set" stringByAppendingString:[myKeyString capitalizedString]];
[myObject performSelector:NSSelectorFromString(mySetterString) withObject:myValue];

// Manual implementation of KVC setter for instance variable.
object_setInstanceVariable(myObject, myKeyString, myValue);

由于 KVC 可以自动查找设置器和获取器,如果您想避免 NSKeyValueCoding 查找指定的或普通的方法和实例变量,您可能只需要通过创建自己的查找路径来使用前面的方法。

创建自己的查找路径的优点

为了避免使用 NSKeyValueCoding,寻找通常由 NSKeyValueCoding 查找的方法或实例变量,并创建自己的查找路径将是您需要采取的方法。让我们先从优点开始,然后讨论其缺点:

  • 这可能比正常的 NSKeyValueCoding 路径更快。

  • 它让您对路径有更多的控制。与 NSKeyValueCoding 路径不同,它也会适用于非 NSObject 继承的类。

  • 通过手动操作,可以使用非对象值进行获取和设置。

创建自己的查找路径的缺点

  • 通常,您将花费比仅使用正常的 NSKeyValueCoding 路径更多的时间来处理它。

  • 它也提供了更少的灵活性,因为您需要编写更多的代码来覆盖任何不寻常的键/值情况,这些情况通常由自动方法覆盖。

关联对象

在 iOS 和 64 位 Mac OS X 应用程序使用的 Objective-C 2.0 运行时中,您可以从任何对象设置关联到另一个对象。在这种情况下,对象没有实例变量或方法的支撑,可以在运行时通过键设置一组额外的属性,如下所示:

objc_setAssociatedObject(myObject, myKey, myValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

如果您想从对象外部设置属性,可以使用它。如果您是一个对象,而您的 T 恤颜色是您的属性之一,那么这就像有人从您房子的外面改变它的颜色,而您甚至都没有注意到。

您应该在类似的情况下使用它,即您想在设置属性时让对象远离知道、支持或参与,而您从程序的其它部分设置属性。关联对象不应该是您头脑中想要使用的顶级方法,因为缺乏类型信息使得由于错误的类型而导致崩溃变得容易。

使用关联对象的优点

  • 键可以是任何指针。在这种情况下,可以使用 OBJC_ASSOCIATION_ASSIGN

  • 它可能是最快的键值编码方法。

  • 不需要方法或实例变量的支持。

使用关联对象的缺点

  • 它对对象本身(实例变量或方法)没有影响。对象不会知道自己的变化。

  • 在关联对象中,键不再是 NSString,而是一个指针。

选择器作为键

通常,KVC 会在找到属性键后查找属性键,并在找到后执行操作。另一种方法是在查找过程中对对象的属性进行操作。Objective-C 核心中有一个查找方法,其键用作选择器。

以下代码行是如何实现这个查找方法的:

objc_msgSend(myObject, mySetterSelector, myValue);

注意

这种方法与手动实现实例变量的设置器非常相似,但不是使用键来形成一个选择器进行查找,而是使用选择器本身作为键。

使用选择器作为键的优势

  • 可以获取和设置非对象数据。

  • 在所有处理方法的方法中,这是最快的一个。

使用选择器作为键的缺点

  • 你需要不同的选择器来获取和设置

  • 由于选择器不是对象,无法直接存储在NSArrayNSDictionary中。相反,你可以使用NSValue或 Core Foundation

最大灵活性和处理不寻常的键/值

在学习了这么多使用键值编码的方法之后,如果你在处理不寻常的键/值时寻求更多灵活性,仍然有一种非常重要的实现方式。就是自己动手做。键值编码的最终方法就是自己处理实现。

创建一个获取器和设置器方法,并在每个方法中适当地在一个对象拥有的字典上返回和设置值,可能是最简单的方法。

我们可以在以下示例代码中查看这种方法:

/*
//------------------------------
  We create the method called "setCollectionValue:forKey:"
//------------------------------
*/

- (void)setCollectionValue:(id)value forKey:(NSString *)key
{
      /*
     //------------------------------
	 Here we set the value for key in a dictionary owned by the object.
     //------------------------------
     */

      [collectionDictionary setObject:value forKey:key];
}
 /*
//------------------------------
     Then, we create the method called "getCollectionValueForKey:"Note that it's a getter method, so it must return something – (id)
//------------------------------
*/
 - (id)getCollectionValueForKey:(NSString *)key
{
    /* 
   //------------------------------
    Here, we get the object from the dictionary, for the specified key and return it.
   //------------------------------
    */

    return [collectionDictionary objectForKey:key];
}

在我们的示例代码中,我们使用了NSDictionary作为值的内部存储;然而,你可以使用自己的存储解决方案,甚至 Cocoa 键值存储结构:

  • NSMutableDictionary

  • NSMapTable

  • CFMutableDictionaryRef

自行实现的优势

  • 单个对象可以暴露多个集合

  • 在获取和设置时,可以使用相应集合支持的任何数据类型

  • 在所有实现方法中,这是最灵活的一个

自行实现的优势

  • 对于随机对象来说,这根本不起作用,只有针对目标类才有效

  • 除了这个之外,你不能使用其他NSKeyValueCoding概念

键值观察

键值观察——也称为 KVO——是一种在变量更改时得到通知的方法,但仅当它使用 KVC 更改时。我们可以从这个中突出两点:

  • 首先,你需要 KVC 才能进行 KVO

  • 其次,如果一个变量直接通过其默认的设置器和获取器方法更改,而没有使用键值编码,你将不会得到任何通知

任何键路径中的任何变量都可以由一个对象进行观察。如果你考虑使用 KVO,这很有用。由于 KVO 建立在 KVC 之上,你需要 KVC 来实现 KVO,使用 KVO 应该是你需要使用 KVC 的原因之一。

实现键值观察

实现 KVO 相对容易,正如我们将在下面的代码示例中看到的那样。在指定的键路径上添加一个观察者。之后,你可以创建一个方法,当观察者在其键路径上看到变量发生变化时,该方法将被调用。

可以使用以下方法从NSKeyCodingProtocol注册对象作为观察者:addObserver:forKeyPath:options:context:。任何修改执行时,都会调用以下方法observeValueForKeyPath:ofObject:change:context:

首先,进入你的类并添加以下方法:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
}

如您之前所见,此方法在执行任何修改时被调用。然而,该协议比这更强大;它允许您在变化发生之前和之后通过使用相应的方法:willChangeValueForKeydidChangeValueForKey来通知变化。如果您需要时间特定的通知,您可以考虑这些方法。

让我们检查以下代码,其中我们注册一个对象作为观察者:

/*
//------------------------------
   We register the object "developmentManager" as the observer of "developer". It will then notify you when any change will take place for the key path "developmentStage".
//------------------------------
*/
[developer addObserver:developmentManager forKeyPath:@"developmentStage" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

如果您仔细观察,您会注意到我们使用了选项NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld。这两个选项都在我们需要知道旧值和新值时使用。这些值将存储在我们的更改字典中。

在我们的例子中,让我们假设开发阶段由级别表示,NSInteger值从010,并且每次修改时我们都需要通知我们的进度。在这种情况下,我们将创建两个简单的方法来为我们完成这项工作:

- (void)informNoProgress
{
  NSLog(@"We had no progress today");
}

- (void)informRealProgress
{
  NSLog(@"Our today's progress is of %@ level", developer.developmentStage);
}

前面的两个方法现在已完成;一个将在开发阶段没有变化时不会通知进度——在我们的场景中,我们将考虑这是不可能减少的,另一个将在开发阶段变化时通过级别通知真实进度。然而,现在,我们想要在比较值之后调用适当的方法。记住我们使用了选项NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld;它们将在变化后保存旧值和新值。

当观察者通知修改时,旧值和新值将在调用该方法的内部进行处理,如下所示:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if([keyPath isEqualToString:@"developmentStage"])
    {
    /*
    //------------------------------
        Here we store the old and new values for further comparison.
    //------------------------------
    */
     NSInteger oldStage = [changeobjectForKey:NSKeyValueChangeOldKey];
    NSInteger newStage = [changeobjectForKey:NSKeyValueChangeNewKey];

    /*
    //------------------------------
        Then, we check whether the oldStage level is lower than the newStage level
    //------------------------------
    */
     if(oldStage < newStage)
    {
      /*
      //------------------------------
          If the value is lower, there is progress and we call the properly method to inform it
      //------------------------------
      */
       [self informRealProgress];

    } else {
      /*
      //------------------------------
	  However, if the old level is not lower, it means there was no progress, we call the method to inform it.
      //------------------------------
      */
       [self informNoProgress];
    }
      }
}

在前面的代码中,我们确保如果观察到的键是我们真正要找的,只是为了确保——在我们的例子中,键是developmentStage。然后,我们将旧值和新值存储起来以便比较。如果有积极的变化,通知进度,如果没有,调用其他方法通知坏消息。

这是一个非常实用的工具,尤其是如果它被巧妙地使用,因为它真的很强大,因为它允许我们观察或监视对象上的 KVC 键路径,并在对象值发生变化时收到通知,这在某些编程环境中可能很有用。即使在属性变化上也能有所控制,这是一个非常强大的功能,我相信您将在自己的项目中找到很多很好的用例。

性能考虑

在重写 KVC 方法实现时,你必须小心,因为默认实现会缓存 Objective-C 运行时信息,以便更有效且更少出错,不必要的重写实现可能会影响你应用程序的性能。

摘要

到目前为止,我们已经深入探讨了键值编码和其他细节,例如各种实现方法、它们的优缺点,以及键值观察——这是建立在键值编码之上的一个机制。

我们还看到了一些关于键值编码和键值观察的工作代码,并解释了为什么我们更倾向于使用键值编码而不是其他类似方法,例如使用点操作符来访问属性。

通过这些,我希望这能帮助你理解键值编码和键值观察。因此,让我们继续前进到下一章,我们将深入探讨苹果公司推出的全新语言 Swift。

第八章. Swift 简介

苹果公司在 2014 年 6 月 2 日在旧金山的 Moscone West 举办了他们的全球开发者大会WWDC),与往年一样,地点设在同一个场馆。他们宣布了一系列新的 API、用于游戏的技术如 Metal、新的 iOS(iOS 8)和 Mac(Yosemite)操作系统,以及 2014 年对 iOS 开发者来说最重要的公告,即 Swift 编程语言的发布,有些人认为 Swift 是为了取代 Objective-C,因为 Objective-C 自 1983 年推出以来,由于其历史悠久,已经开始显得有些过时。Swift 旨在成为一个易于学习和简单的编程语言,这将降低那些被 Objective-C 吓到的开发者的入门门槛。然而,Swift 是什么?它有哪些优点?它与 Objective-C 有多大的不同?最后,学习 Swift 有多容易?这些问题我们将在本章中探讨,为了开始,以下是我们将要涵盖的主题列表:

  • 欢迎来到 Swift

  • Swift 基础知识

  • 内存管理

欢迎来到 Swift

Swift 实际上并不是一门新语言,因为苹果公司早在 2010 年就开始了 Swift 的开发。随着像 Ruby、Python、Haskell、Rust 等编程语言的流行,Swift 的开发使用了这些流行语言的语言理念。正如苹果公司将 Swift 描述为“没有 C 的 Objective-C”,你可以将 Swift 视为一种语言,它是对 Objective-C 的重新构想,使用了来自 JavaScript 等语言的现代概念和语法,同时仍然保留了 Objective-C 的本质和精神。

Swift 通过使用 ARC(自动引用计数)来消除指针,并通过这种方式使内存管理对开发者来说是透明的,这样他们就可以专注于 iOS 应用程序的开发,而无需担心大多数时候的内存管理。Swift 使用 ARC 而不是 Java 中发现的 GC(垃圾回收)方法。这意味着如果你不小心使用循环强引用,Swift 仍然可能会泄漏内存。Smalltalk 是一种在 1972 年发布的编程语言,它在架构方面对 Objective-C 产生了重大影响,例如消息传递。Objective-C 中的 Smalltalk 方面,如方法调用,已被点符号和类似 Java 和 C#的命名空间系统所取代。然而,Swift 并不是完全脱离 Objective-C 的。Objective-C 的关键概念,如协议、闭包和类别,仍然存在于 Swift 中,只是语法更加清晰和简洁。

Swift 对内存管理的处理方法是使用 ARC,ARC 的一个问题是开发者可能会无意中创建一个强引用循环,其中两个不同类的实例都包含对另一个类的引用。因此,Swift 提供了弱引用和弱引用键(unowned)来防止强引用循环的发生。

对于一个有 C 或 C++背景的资深 Objective-C 程序员来说,Swift 可能看起来像一门全新的语言,因为它摒弃了一些 Objective-C 的方面,例如冗长性。我相信很多 Objective-C 开发者都经历过“方括号地狱”,其中简单的功能需要用很多方括号包裹,这使得代码难以阅读,并且也可能会将错误引入你的应用程序。Swift 的目标是让开发者能够在没有 C 的情况下利用 Objective-C 的力量。因此,Swift 确实有一些方面让开发者更容易使用,但相反,Swift 也有一些部分似乎还没有完全完善。然而,请记住,在撰写本文时,Swift 仍然处于 beta 测试阶段,Apple 在接下来的几周和几个月内可能还会引入很多变化。然而,随着 Apple 全力支持 Swift,现在是开始学习一些 Swift 基础的好时机。就像 Apple 引入的所有新技术一样,你需要 Xcode 6 beta 或更高版本来运行和构建你的 Swift 代码,因为 Xcode 5 不支持 Swift。你的 Swift 代码也可以在 iOS 7 和 Mac OS 10.9.3 上运行。所以,如果你是苹果开发者,你可以下载 Xcode 6 beta 并将其安装在你的 Mac 上,因为它将与你的 Xcode 5 并行安装,不会覆盖任何内容或破坏你的当前 Xcode 项目。那么,让我们开始吧。

Swift 基础

Swift 的语法与 Objective-C 非常不同,虽然 Objective-C 有很多依赖于 C 和 C++组件,如指针、强类型等。Swift 在简洁性和变量声明方面与流行的脚本语言(如 Python 和 Ruby)非常相似。让我们看看 Swift 的一些基础知识,以便熟悉它。

变量声明

Swift 摒弃了记住intfloatNSString等的需求,并将所有这些类型的变量合并为一种类型,即var类型。如果你熟悉 JavaScript,那么var关键字应该不会陌生。Swift 支持类型推断,根据你分配给变量的值,它会推断其类型:

var welcome
welcome = "Hello world"

这意味着变量welcome被推断为字符串类型,因为我将它赋值为文本Hello world。然而,如果你想更具体一些,你可以这样注释一个变量:

var welcome: String.

然后在 Swift 中,要将两个字符串连接起来,你可以这样做:

welcome += " Bob"

如果你之前使用的是 Objective-C,你需要输入更长的语法:

NSString *hello = @"Hello world";
str = [str stringByAppendingString:@" Bob];

Swift 还支持使用let关键字声明常量。因此,要声明一个常量,你只需输入以下语法:

let LIFE_MEANING = 42

注意,Swift 现在推断 LIFE_MEANING 是一个整数,因为你已经将它赋值为 42。要打印一行用于日志的文本,类似于 Apple 的 Cocoa 框架中的 NSLog,你可以使用 println 关键字,而使用 NSLog 时,你需要指定格式说明符,例如 %d 用于整数,%@ 用于 NSStrings,或 %f 用于 float/double。

对于 Swift 来说,没有必要这样做;你可以直接使用以下语法示例:

println("The text is \(welcome)") //print out the value of variable welcome
println("The meaning of life is \(LIFE_MEANING)")//print out the meaning of life

一件没有从 Objective-C 改变的事情是,Swift 中的注释用 // 表示单行注释,用 /**/ 表示多行注释。

分号也是可选的。有些人可能想过于严谨,会加上分号,但就我个人而言,我不喜欢比必要的多打任何多余的字符,所以我倾向于在我的 Swift 代码中省略分号 (😉。

变量声明

与所有编程语言一样,Swift 支持用于算术比较和赋值的运算符数组。

所有如 /*+ 等运算符的功能与 Objective-C 中相同,只是 + 运算符在需要连接多个字符串时还充当字符串连接运算符的双重功能。

Swift 引入了闭区间运算符的概念,它定义了一个从 xy 的范围,如果你使用它像这样 (x...y),它包括 xy 的值。例如:

for index in 1...5 { print("Value is \(index)") }

这将打印出值 12345。正如你可能已经注意到的,这可以用来替换 Objective-C 中更冗长的传统 for 循环,它由以下代码行表示:

for (int i = 0; i <= 5, i ++)
NSLog(@"Value is %d", i);

然而,如果你想要执行一些常见的 for 循环代码来遍历数组呢?那么,你需要一个半开区间运算符,它与闭区间运算符类似,只是少了一个点 (x..y):

let breeds = ["Pitbull", "Terrier", Bull dog", "Maltese"]
let count = breeds.count
for i in 0..count { println("This breed is \(breeds[i])") }

如你之前所见,Swift 也支持集合类型,如之前展示的数组以及字典,我们将在接下来的几页中介绍。要开始声明一个数组,你只需使用以下语法:

var catBreeds = ["Siamese", "Scottish"]

然后,还有一些有用的属性,例如:

  • count:这个属性返回数组中的项目数量

  • isEmpty:这是一个布尔变量,当计数属性为 0 时返回 true

  • append:这个属性将允许你向数组的末尾添加一个项目

Swift 提供了一些辅助代码来迭代数组,而不是使用 for 循环、while 循环或 do-while 循环。在 Swift 中,数组迭代更容易,因为你只需这样做即可进行迭代:

for item in catBreeds{
  println(item)
}//prints out "Siamese" and "Scottish"

你不再需要为 forwhiledo-while 循环编写任何冗长且不必要的代码,因为你可以使用项目变量来访问数组。

接下来,我们将介绍字典。在 Swift 中,字典的功能和用法与 Cocoa 中的 NSDictionary 类似。然而,有一个主要区别在于,在 NSDictionaryNSMutableDictionary 中,你可以使用任何对象作为键和值,这并不提供有关对象性质的信息。在 Swift 中,字典中键和值的类型总是通过显式类型注解或类型推断明确指定。

Swift 中字典的语法非常简单,如下所示:

var breeds = Dictionary<String, String> = ["Breed1": "Bull Dog", "Breed2": "Terrier"]

前面的代码使用了显式的类型表示法,正如你所见,键和值被明确地定义为 StringString。这与你在 JavaScript 中声明字典或在 Java 中声明 Map 的方式非常相似,只需运行以下代码即可:

var breeds = ["Breed1": "Bull Dog", "Breed2": "Terrier"]

前面的代码使用了类型推断,一旦我们将 Breed1 分配给键,将 Bull Dog 分配给值,Swift 就会自动推断我们的字典将包含两个字符串。

在 Swift 中修改字典的方式与访问数组类似,只是你使用键而不是索引,在我们的例子中键是一个字符串。因此,如果你想修改映射到 Breed1 键的值,你可以像下面这样操作:

breeds["Breed1"] = "Dalmatian"

另一方面,Swift 允许我们以另一种方式更新值,即 updateValue 方法,如下所示:

breeds.updateValue("Breed2", forKey: "Bloodhound")

这两种方式都可以让你使用键来更新值,但我更喜欢第一种方式,因为它更简洁,但同样易于阅读和理解。

在 Swift 中遍历字典与遍历数组类似,我们可以放弃旧的 Objective-C forwhiledo-while 循环方法。要在 Swift 中进行字典迭代,我们只需使用以下代码:

for (breed, breedname) in breeds{
  println("\(breed) is \(breedname)")
}//prints Breed1 is Dalmation, Breed2 is Bloodhound

在任何通用编程语言中,控制流语句都是必要的,以便控制代码和应用程序的流程。因此,尽管 Swift 与 Objective-C 相去甚远,但它仍然允许使用类似于 C++ 的 C 语言风格的控制流结构。

这里是一个 Swift 中可用的控制流结构的列表:

  • for 循环

  • for-in 循环

  • while 循环

  • do-while 循环

  • if 语句

  • switch 语句

这些控制流语句与 Objective-C 中的作用相同,但它们有一些改进,我将简要解释。

迭代语句

对于像 for 循环这样的重复执行语句,Swift 强调使用 for-in 循环进行迭代。在其他编程语言(如 Java)中也称为增强型 for 循环。这提高了代码的可读性,并使代码更加简洁。例如:

var dogs = ["Bulldog", "Terrier", "Dalmatian"]
for dog in dogs {
  println("This dog is a \(dog)");
}

然而,如果你需要 Objective-C 传统的 for 循环风格,你可以使用 Swift 来实现,如下所示:

for index = 0; index < 3; ++index {
//do something here
}

条件语句

If 语句的行为与 Objective-C 中的行为相同,除了语法上有一个小的变化,如下所示。请注意,括号是可选的,所以我们没有将它们放在条件表达式周围:

if temperatureInCelsius < 10 {
  println("It is cold here");
}

注意,在前面的例子中,我们有一个非常简单的条件,所以我们选择消除我们的括号。但是,如果你有多个条件呢?在这种情况下,Swift 将使用你熟悉的正常优先级规则,但缺少括号可能会使操作难以理解。因此,在这种情况下,我更倾向于使用括号来处理多个条件和操作,如下所示:

if (temperatureInCelsius < 10) && (temperatureinCelsius > 0)
{
  println("It is chilly here");
}

然而,在 Swift 中,Switch 语句现在通过不需要进入下一个 case 来简化了调试。因此,现在整个 switch 语句在第一个匹配的 switch case 完成后立即完成其执行。所以,下面的语句将显示以下输出:

let number = 2
switch number {
  case 1:
    println("Number is 1");
  case 2:
    println("Number is 2");
  case 3:
    println("Number is 3");
}

在 Swift 中,输出将是 "Number is 2",而不是在 Objective-C 中看到的 "Number is 1""Number is 3"

Swift 中的控制流已经得到改进,语法已经改进以提高可读性,并防止开发者创建非明显的错误,例如由于缺少break语句而导致的 Switch case fallthrough。

函数

函数是每种编程语言的基本构建块,在 Swift 中也是如此,但也有一些改进,我们现在将介绍。函数的语法已经发生了很大的变化,因此 Swift 中的函数现在具有以下语法:

func animalType(animalName: String)-> String {
  let text = "This is a " + animalName
  return text;
}

因此,你可以使用 println(animalType("Dog")) 来调用它。如果一个函数没有返回值,你可以避免添加箭头(->),如下所示:

func animalType(animalName: String) {
  println("This is a \(animalName)")
}

Swift 中的函数现在可以具有多个返回值,作为复合返回值的一部分,你可以使用元组返回类型。

元组类型只是对由逗号分隔的零个或多个类型列表的一个花哨的称呼,这些类型被括号包围。因此,为了让一个函数有多个返回值,你需要使用元组,如下所示:

func myFunc(iCount: Int) -> (intA: Int, intB: int) {
  var intX = 1, intY = 2
  intA = iCount + intX
  intB = iCount + intY
  return (intA, intB)
}

然后为了使用返回值,你需要将其分配给一个变量:

let num = myFunction(10)
println("Value is \(num.intA) and \(num.intB)")

Swift 允许在函数中使用默认值,如果函数参数没有被使用,将使用默认值:

func add(num1: Int, num2: Int = 0)
{
  var total = num1 + num2
}

因此,在前面的例子中,如果在不传递第二个参数的情况下调用 add 函数,num2将具有默认值 0,如下所示:

add(1)

Swift 函数还允许函数接受可变数量的参数,这在需要向函数传递可变数量的参数时非常有用。要使函数能够接受可变数量的参数,你只需要在你的函数中添加三个点(...),如下所示:

func getAverage(numbers: Double...) -> Double
{
  var total: Double = 0
  for num in numbers {
    total += num
  }

  total = total&/Double(num.count)
  return total
}

因此,你可以使用可变数量的参数调用getAverage函数,例如getAverage(1, 2, 3)getAverage(1, 2, 3, 4, 5)。默认情况下,Swift 将所有函数参数视为常量,以促进良好的编程实践。这是 Swift 的更多独特功能之一,你在其他过程式编程语言(如 C++、Objective-C 等)中找不到。因此,尝试修改函数参数将导致错误。但是,如果你需要在代码中修改函数参数,你只需添加var关键字来告诉 Swift 将该函数参数视为变量,而不是常量,如下所示:

//num is now a variable and can be modified inside the function myFunction
func myFunction(var num: Int)
{
}

Swift 中函数的一个重要变化,与 Objective-C 不同,是你可以在另一个函数内部创建嵌套函数。但请注意,内部函数仅对封装函数可用。要声明嵌套函数,你只需使用正常的函数调用即可:

func adder(num: Int)
{
  func addOne(number: Int) -> Int   { return number + 1 }
}

Swift 中的类和结构体

如你所知,类和结构体是通用数据结构,是应用程序代码的构建块。你可以定义属性和方法来为这些类和结构体添加功能,使用与变量、函数等相同的语法。

Swift 中的类和结构体有许多共同点,例如:

  • 定义属性以存储值

  • 定义方法以提供附加功能

  • 能够扩展以扩展其功能

  • 能够遵守协议以提供标准功能

然而,类与结构体有其他结构体不具备的差异;它们是:

  • 继承,允许子类继承另一个类的特性

  • 类型转换,允许你在运行时检查和解释类实例的类型

  • 引用计数,允许对类实例有多个引用

  • 析构器,允许类进行资源释放

  • 结构体在你代码中传递时会被复制

要声明一个类或结构体,分别使用classstruct关键字:

class myClass {
  var x = 0
  var y = 0
}

struct myStruct {
  var x = 0
  var y = 0
}

与 Objective-C 一样,要使用structclass,你需要在使用它之前创建其实例。因此,对于structclass,你需要使用()来创建classstruct的实例。

let classA = myClass()//creating an instance of a class
let structB = myStruc()//creating an instance of a struct

要访问classstruct中的属性,你可以使用"."运算符来访问它,如下所示:

var myX = classA.x
var myY = structB.y

对于类,你可以在你的类文件中定义自己的自定义初始化器和析构器。你的结构体成员的初始化器会自动为你创建,你可以使用,如下所示:

let yourStruct = myStruct(x: 50, y: 80)

通常,你不需要手动清理你分配的实例,因为 Swift 会为你使用 ARC 来完成。然而,如果你使用了一些自定义资源,那么你可能需要自己进行额外的清理。一个用例是当你有一个类打开一个文本文件并向其中写入或追加数据时。所以,在这种情况下,你可能需要在你的实例被销毁之前关闭文件。一个初始化器语法的例子如下:

deinit{
//Your deinitialization code here which could be closing an open file etc
}

Swift 类和 Swift 结构体之间最重要的区别之一是 Swift 类是通过引用传递的,这意味着当你将它赋给另一个实例时,会创建现有类实例的引用,任何对新的实例的改变都会影响原始实例。这与按值传递形成对比,按值传递会将值的副本传递给变量,因此新变量中发生的事情不会影响原始变量。

因此,根据这种差异,在某些情况下使用类可能更有用,而在其他情况下使用结构体会更好。这完全取决于你的程序或应用程序的上下文。所以,让我们用一些代码来帮助我们更好地理解这一点:

classA = myClass()
classA.x = 80
classB = classA
classB.x = 100

如果你使用以下代码,你会注意到classA成员x也会被设置为100的值,因为当你运行classB = classA代码时,会传递一个classA的引用。所以,任何影响classB的也会影响classA

闭包

Swift 中的闭包在 Objective-C 中被称为 blocks。两者都有创建自包含代码块的概念,这些代码块可以被传递和使用。

闭包使用{}分别表示开始和结束。所以,创建一个闭包并调用的一个非常简单的例子可能是这样的:

  var name = "Gibson"
      var greet = {  println("Hello \(name)")   }
      greet()

你可以在你的调试控制台中看到输出Hello Gibson

当然,你需要向闭包传递参数并从闭包获取返回值。你还需要使用()来包围你的参数,并使用->来表示你的返回值,就像你在这里看到的那样:

  var s1: String = "Howdy"
      var name: String = "Gibby"
      var holler = { (s1: String, name: String) -> String in
            return s1 + " " + name
        }
        var ret = holler(s1, name)
        println(ret)

如果你运行代码,你会得到输出Howdy Gibby,因为我传递了两个字符串变量s1name,分别对应于你从(s1: String, name: String)行中看到的,而我要求返回一个字符串类型的值,使用-> String

接下来,让我们继续讨论 Swift 中的内存管理,这你仍然需要注意,因为自动引用计数(ARC)让你免于许多内存管理技术,但你仍然需要在 Swift 中注意一些内存管理技术,因为如果你不小心,Swift 仍然可能会泄漏内存。

Swift 中的内存管理

Swift 的创建是为了避免 C 语言的一些缺点,其中之一就是内存管理。注意,在本章中,我并没有提到任何关于指针、内存分配、释放等内容。这是因为 Swift 中的内存管理被设计得尽可能简单,以便你,作为开发者,可以更多地关注应用程序开发而不是调试内存泄漏。每当创建一个新的类实例时,ARC 会分配一块内存用于存储该实例的信息。这块内存包含有关实例类型(字符串、整数等)以及与该实例相关联的属性值等信息。当该实例不再需要或被引用时,ARC 会释放该实例使用的内存。这是为了避免实例在不再使用或需要时仍然占用宝贵的内存空间的情况。

然而,如果你在 ARC 释放实例之后尝试访问实例的属性或方法,那么你很可能会看到崩溃的结果。所以,为了确保这种情况不会发生在你身上,ARC 会跟踪当前有多少属性、变量和常量正在引用一个类实例,并且只要代码中的某个地方至少有一个活动引用指向该实例,ARC 就不会允许释放。当你将类实例赋给属性、常量或变量时,就会创建一个强引用,这个强引用会紧紧地保持对那个类实例的控制,并且只要这个强引用存在,ARC 就不会进行释放调用。让我们通过一些代码来进一步说明这一点:

  1. 让我们声明一个名为 Dog 的类:

    class Dog {
      let type: String
      init(_type: String) {
        self.type = _type
        println("Init done")
      }
      deinit {
        println("Deinit done")
      } 
    }
    
  2. 然后,我们将创建两个对 Dog 类的引用:

    var dog1: Dog?
    Var dog2: Dog?
    

    注意

    注意到存在一个 ? 关键字,这意味着 dog1 是一个可选类型,这意味着它可能是 nil。在 Swift 中,任何带有 ? 关键字的变量都意味着它有可能包含一个 nil 值。由于我们声明了 dog1dog2 为可选类型,这意味着 dog1dog2 被初始化为 nil 值,并且没有对 Dog 类的引用。

  3. 接下来,我们将创建一个实例,并将两个变量分别赋值为 dog1dog2

    dog1 = Dog(type: "Bulldog")
    dog2 = dog1//1 strong reference to dog1 is created
    

    现在,有两个对 Dog 实例的强引用。一个是通过 dog1,另一个是从 dog2dog1

    关于 Swift,有一点需要注意,类型推断仅适用于初始赋值;将另一个类型赋给同一变量将引发错误。这与 JavaScript 等其他语言截然不同,后者不会抛出错误。让我用一个例子来说明我的意思:

    var hello = "Hello World"//this means hello is inferred to have a type of string 
    hello = 42 
    //This will throw an error as now you are assigning an integer to a string variable
    
  4. 因此,你可以尝试将 dog1 赋值为 nil,如下所示:

    dog1 = nil
    

    ARC 会看到 dog2 仍然持有强引用,因此不会释放 Dog 实例。Dog 实例将被释放的唯一情况是当 dog2 被设置为 nil,如下所示:

    dog2 = nil //this will let ARC deallocate the Dog instance
    

相反,Swift 也支持弱引用,其中引用没有对它引用的实例的强引用。因此,即使有弱引用,ARC 也会销毁实例。要创建弱引用,你需要使用 weak 关键字,如下所示:

weak var cat: String?

注意,? 关键字被添加到 String 关键字末尾,因为弱引用可以允许有 nil 的值,所以所有弱引用都必须使用 ? 关键字声明为可选。注意,它也被声明为变量,因为弱引用的值在代码运行时可能会改变。因此,弱引用不能声明为常量,因为弱引用对其引用的实例没有强引用。所以,当这个实例即将被释放而弱引用仍然指向它时,ARC 会在这种情况下将弱引用设置为 nil,你可以检查弱引用的值以查看该对象是否已被释放。这样你可以避免出现最终得到一个指向已由 ARC 释放的无效实例的引用的情况。

在强引用和弱引用之间,还有一种引用类型,它对其引用的实例保持弱引用,但不能设置为 nil,因此它总是假定有一个值。这被称为非拥有引用。它可以作为弱引用的替代品,以及在某些用例中,正如我们将在这里看到的:

class Country {
  let name: String
  let capital: City!
  init(name: String, capital: String) {
    self.name = name
    self.capital = City(name: name, country: self)
  }

class City {
  let name: String
  unowned let country: Country
  init(name: String, country: Country) {
    self.name = name
    self.country = country
  }
}

注意,我们在 Country 的初始化方法中初始化了 City,但我们还需要在 City 的初始化方法中初始化 Country,这本身就是一个难题,因为 Country 依赖于 City 的初始化器,而 City 依赖于 Country 的初始化器。为了解决这个问题,你可以将 Countrycapital 变量声明为一个隐式解包的可选属性,这可以通过使用 ! 来表示。这意味着 capital 属性将有一个默认值 nil

! 关键字还充当一个解包函数,你可以获取属性值而不必将该属性分配给局部变量。如前所述,带有可选符号(即 ? 关键字)的变量可以包含一个值或者什么都没有。因此,当你测试这个类型为可选的变量时,你需要知道是否有值,而不必直接访问其底层值。! 关键字意味着你可以解包变量以获取访问值。

然而,这并不能免除你检查该属性是否为 nil 的责任,因为你仍然需要在代码中检查 nil

因此,现在的情况是 capital 具有默认的 nil 值,一旦 Country 实例在其初始化方法中设置了其 name 属性,它就被视为完全初始化。这意味着 Country 初始化方法可以在设置 name 属性后立即开始引用和传递其 self 属性。因此,现在 Country 初始化方法可以在设置自己的 city 属性时,将 self 属性作为参数之一传递给 City 初始化方法。

摘要

如您所见,Swift 在语法、风格和范式方面与 Objective-C 有很大不同。Swift 是为了摆脱编程中的 C 范式,在那里我们需要理解内存管理、分配和释放。我们回顾了 Swift 的一些基本特性,并指出 Swift 具有更简洁的代码,内存管理更加简单,因为 ARC 在 Swift 中为我们处理内存管理。然而,Swift 在撰写本文时仍处于测试阶段,因此在其走向 alpha 版本和发布状态的过程中,其功能仍可能发生变化。因此,您可以预期在此期间某些功能可能会被添加或删除。但是,Swift 的基础将不会发生重大变化,我希望这一章能帮助您更好地理解 Swift,并为您在不久的将来使用 Swift 进行编程做好准备。

要了解更多关于 Swift 的信息,最佳参考资料是苹果公司 Swift 编程网站 developer.apple.com/swift/blog/,因为自 Swift 公布以来,该网站的内容一直持续更新。

在下一章中,我们将探讨使用 Xcode 中的一些优秀工具(如静态分析器)进行内存管理的技术,我们还将更详细地介绍各种技术,以便您知道在不同情况下哪种调试工具是最好的工具。

第九章:内存管理和调试

回到 iOS 3 和之前版本的美好时光,计算机内存的管理是一项繁重的工作,因为每个指针和内存分配都需要精确跟踪,以免你遇到可怕的内存泄漏情况,这可能是由于你的代码中缺少release键码等原因。然而,随着 iOS 4 及更高版本的发布,苹果引入了 ARC,全世界的开发者都欢欣鼓舞,因为他们认为内存管理的日子已经结束了。然而,遗憾的是,情况并非如此,因为 Objective-C 不像 Java 或 C#等其他编程语言那样,有一个垃圾回收器会为你进行内存管理和垃圾回收。ARC 只是一种使内存管理简化的工具,这样我们就不需要显式地调用release方法,例如[myArray release],因为 ARC 会为我们处理这些。所以,尽管在开发 iOS 应用时我们需要的用于内存管理的脑细胞减少了,但我们仍然需要在引入 ARC 后进行一些基本的内存管理,而这一章将帮助你在这个过程中。因此,为了开始,以下是本章我们将要涵盖的主题:

  • 内存泄漏

  • 强/弱引用

  • 保留周期

  • 内存过度使用

  • 使用调试器和断点

  • 收集 AppPlumbing 泄漏的数据

  • 使用 LLVM / Clang 静态分析器

  • 使用 NSZombie

内存泄漏

如果你习惯在alloc/init方法或retain语句之后调用release方法,ARC 允许你省去这些步骤,因为即使你仍然可以调用你的alloc/init方法或retain语句,也不需要添加release语句,因为 ARC 会为你处理这一切。这引入了简洁性,并使你的代码更加简洁。以下是一个示例:

在 ARC 之前:

Class1 *obj1 = [[Class1 alloc] init];
Class1 *obj2 = [obj1 retain];
[obj2 release];
[obj1 release];

在 ARC 之后:

Class1 *obj1 = [[Class1 alloc] init];
Class1 *obj2 = obj1;

如果你编写的代码没有调用如“在 ARC 之后”中看到的release方法,你将有两个内存泄漏出现在你的代码中,这是由于你忘记添加两个release方法。你会注意到行数已经减少,代码更容易理解,因为不需要调用任何release语句。所以,有了 ARC,人们可能会误以为他们的内存管理问题已经解决,但实际上,即使有 ARC,内存泄漏仍然可能发生,我将向你展示如何处理这种情况。

ARC 有助于自动化添加retain/release/autorelease语句到你的代码中,但即使有 ARC,内存泄漏仍然可能发生。这并不明显,因为人们认为有了 ARC,就不会有任何内存泄漏。然而,情况并非如此,即使有 ARC,内存泄漏仍然可能发生,但你可以使用一些方法来查找内存泄漏。然而,首先,让我们通过一些术语来了解一下。

强/弱引用

强引用与retain属性同义,即通过将对象的引用计数增加 1。在 ARC 的世界里,retainassign属性不再使用,分别被strongweak所取代。

强引用是对象的默认属性,因为它意味着你想要获得对象的拥有权,而弱引用意味着另一个对象正在持有你想要的对象的拥有权,然后你不能阻止它被释放,因为拥有权不属于你。

强引用弱引用分别由 Objective-C 中的strongweak关键字表示。即使在自动引用计数(ARC)的情况下,仍然可能出现内存泄漏,并且使用 ARC 的一些内存泄漏原因包括:

  • 保留周期

  • 创建二级线程而不为其提供自己的自动释放池

  • 使用具有非 ARC 代码的框架

  • 在块内部引用自身,这会创建一个强引用

保留周期

当两个对象,例如父对象和子对象,相互具有强引用时,就会发生保留周期。一个简单的例子如下代码:

@interface MyParent : NSObject
@property (strong) MyChild *myChild;
@end

@interface MyChild : NSObject
@property (strong) MyParent *myParent;
@end

你可以使用以下代码创建MyParent类型的对象:

MyParent *myParent = [[MyParent alloc] init];

保留周期是由前一行代码创建的,其外观如下:

保留周期

在前面的图中,你可以快速看到所谓的保留周期,因为myParentmyChild有强引用,而myChildmyParent也有强引用。这是一种内存泄漏的形式,如果一个对象试图释放第一个对象的一个实例,由于第二个对象对第一个对象有强引用,因此无法释放,这样就创建了保留周期。请注意,ARC 不会为你修复所有的内存泄漏,所以你必须,作为开发者,使用一些工具来修复这种类型的内存泄漏,我们将在后面介绍。由于这种类型的内存泄漏不是很明显,修复它需要更多的努力和思考,但幸运的是,苹果提供了一些工具,将极大地帮助我们。

避免保留周期发生的一般规则是记住这一点——如果对象 A 想要无限期地保留对象 B,那么对象 A 必须在层次结构树中位于对象 B 之上,对象 A 必须对对象 B 有强引用。如果你有在层次结构树中处于同一级别的对象,那么你应该使用弱引用来避免保留周期。所以,在前面的图中,为了避免保留周期,mySecondObject不应该对myFirstObject有强引用。然而,如果你确实需要让mySecondObjectmyFirstObject有引用,那么应该将其设置为弱引用而不是强引用。树形层次结构是安全的,并且请记住,放置弱引用可以避免保留周期和内存泄漏。

内存过度使用

如果你使用了足够的 iOS 应用,你将注意到一些应用在你无意识地点击按钮或执行某些操作后,会强制关闭自己。这是 iOS 处理内存问题的方法,它基本上就是这么说,“这个应用有内存泄漏,你没有足够的内存来处理它,所以这个应用必须关闭。”

总共有三个 iOS 内存警告级别。当内存不足时,级别 1 和 2 将在你的 Xcode 控制台中显示,如下所示。级别 3 发生在你的应用程序崩溃并返回 Springboard 时,这是指 iOS 主屏幕的术语:

内存过度使用

使用调试器和断点

使用 IDE,如 Xcode,进行调试的最基本概念之一是断点概念,你可以通过断点在特定时间点停止你的运行程序。使用断点非常简单;你只需打开你的 Xcode 项目,点击窗口左侧的代码位置,就会出现一个蓝色指示器,如下所示:

使用调试器和断点

接下来,当你运行你的应用程序,当程序在while(true)语句的行号26处遇到时,程序将停止,你可以在行号26之前将光标移到任何变量上,Xcode 将显示该变量在该时间点包含的值。断点在调试内存泄漏时非常有用,其中你有一个关于泄漏出现位置的概念,并想查看该变量的值或内存地址。你可以设置多个断点,并使用“单步执行”命令来逐行执行代码,以查看程序的执行情况。以下是在使用断点调试时可能会遇到的图标列表:

使用调试器和断点

在前面的屏幕截图中,从左到右的四个图标,你可以用于断点调试,如下所示:

图标 描述
继续程序执行 这将使你的程序继续执行,直到遇到下一个断点或程序结束
单步执行 这将使你的程序在当前作用域中执行下一行代码
步入 这将使你的程序跟随方法进入其自身的代码并查看方法的代码
跳出 这将带你从当前上下文退出,并调用程序堆栈中一个步骤向上的方法

断点在检查程序由于断点而停止时变量在特定时间点的值时非常有用。当内存不足时,Xcode 控制台将显示四个用于断点调试的图标,以帮助你调试与内存相关的和其他逻辑错误。

收集你的应用数据

请注意,内存警告级别并不一定意味着你的应用程序正在泄漏内存。可能存在这样的情况,即你的应用程序正在加载或对大型资源(如数据文件、图像、视频等)执行操作,这将触发内存警告。ARC 将在稍后处理清理工作。然而,如果你看到内存警告级别为 2,那么你应该开始检查你的代码,因为下一个内存警告级别将是实际的应用程序崩溃。

调试崩溃和内存泄漏就像捉迷藏或扮演侦探的游戏。周围会有很多线索,这些线索将引导你找到代码中导致令人烦恼的崩溃或内存泄漏的罪魁祸首。苹果为我们提供了许多工具和日志,这些工具和日志将对我们调试代码非常有用。我们将在这里介绍一些常用的方法,以便你能够尽快开始修复这些问题。

其中一种较简单的方法是通过你的电缆将你的设备连接到你的机器,启动 Xcode,它将自动检测你连接的设备,然后按Shift + Command + C激活你的调试控制台,这是一个位于 Xcode 屏幕右下角的黑色屏幕。或者,你可以从 Xcode 菜单中选择视图 | 调试区域 | 激活控制台,如下所示:

收集应用程序数据

这将显示当你通过电缆连接到 Xcode 运行应用程序时所有的 NSLog 和崩溃输出。

然而,在测试你的应用程序时,有时它并未连接到你的 Xcode,而在那一刻崩溃。在这种情况下,前面提到的方法不起作用,那么你该怎么办呢?不要担心,一旦你回到你的桌子前,将你的 iOS 设备连接到你的机器并启动 Xcode,你就有另一种方法获取崩溃日志。

一旦你启动了 Xcode 并将你的设备连接到应用程序崩溃的地方,Xcode 实际上能够访问设备上的崩溃日志。为此,你只需要点击窗口并从 Xcode 菜单中选择组织者,如下面的截图所示:

收集应用程序数据

这将打开你的组织者,它实际上是一个所有连接到 Xcode 的设备的仓库,显示了有关应用程序的配置文件和截图等信息。然而,我们真正感兴趣的是崩溃日志。

因此,点击顶部的设备按钮,你将看到迄今为止连接到你的设备的所有设备开发者信息。

点击你当前连接的设备,它由一个绿色圆圈表示。然后,选择设备日志选项,这将打开一个包含已崩溃应用程序的另一个列表。在那里,你可以按进程(应用程序名称)、类型日期/时间对结果进行排序。点击一个项目将在屏幕右侧显示崩溃日志。在那里,你可以看到回溯,它实际上是所有在崩溃前被调用的方法的列表。导致你崩溃的代码的最后一部分将在回溯的顶部,你应该从底部开始查看,以了解你的应用程序是如何工作的,以及它穿越的所有函数和方法:

收集应用程序数据

管道内存泄漏

接下来,我们将查看 Xcode 中一个特殊的工具,在应用程序运行时获取有关应用程序的详细信息。这个特殊工具实际上是一套工具,可以执行以下功能:

  • 检查和监控一个或多个进程

  • 记录一系列用户操作并回放它们,就像录像机一样

  • 保存用户界面录制并从 Xcode 中访问它们

  • 这套工具统称为 Instruments,它们在追踪难以复现的 bug(如随机崩溃和调试内存泄漏)时比 NSLogs 更有用。

  • 分析你的应用程序性能

  • 对你的应用程序进行压力测试

  • 更好地了解你的应用程序是如何工作的

在本节中,我将教你 Instruments 的基础知识以及如何使用它来调试一些代码。所以,为了开始,你只需要遵循这三个简单的步骤:

  1. 点击 Xcode IDE 左上角的Xcode菜单。

  2. 从出现的列表中选择打开开发者工具

  3. 将会弹出一个子菜单,其中包含Instruments项,你应该点击它:管道内存泄漏

  4. 然后,你应该会看到一个弹出窗口,其中包含以下选项:管道内存泄漏

    有如泄漏分配时间分析器等选项,它们显示了所有各种工具。

  5. 为了进行一个小测试运行,打开Instruments.xcodeproj文件,它有非常泄漏的代码,我们将看到如何使用 Xcode 调试工具Instruments来理解代码运行时内存分配峰值。所以,为了开始,让我们使用 Xcode 性能分析工具通过点击产品 | 分析菜单选项来查看我们的内存峰值,如图所示:管道内存泄漏

  6. 然后,Xcode 将显示仪表窗口,然后您需要选择分配选项并点击分析按钮。一旦点击了分析按钮,泄漏的应用程序将开始执行,您将看到以下屏幕。注意您将看到的快速上升的图表以及所有堆分配行,它将显示内存消耗以非常快的速度增加:管道内存泄漏

因此,为了重申步骤,我们需要做以下几件事:

  1. 打开 Xcode。

  2. 点击产品 | 配置文件

  3. 点击分配 | 配置文件

  4. 查看所有堆分配部分。

  5. 查看图上的内存分配。

  6. 检查块内部是否存在自身保留周期或使用,这可能会提示或创建一个保留周期。

使用 LLVM / Clang 静态分析器

Instruments 工具套件旨在在您的应用程序运行时使用。然而,正如俗话所说,“预防胜于治疗”。因此,在您拉起 Instruments 以在运行时调试应用程序之前,有一个很好的步骤您应该遵循,那就是对您的代码库执行静态分析。

静态分析是一种机制,其中使用一系列算法和技术来分析您的源代码以查找错误。这听起来可能像是您在编译阶段所做的事情,但这里有一个重要的区别。编译您的代码将告诉 Xcode 检查您的代码库中的语法错误,并标记出它检测到的任何错误或警告。静态分析更进一步,因为它分析您的代码以找到在运行时可能出现的潜在错误。静态分析允许程序计算程序的所有可能的执行情况,并对代码进行质量、安全性和安全性分析,以便您能够被提醒有关溢出、除以零、指针错误等问题。因此,将静态分析视为运行时测试,但是在您的代码开始执行之前。

随着静态分析深入到您的代码,Xcode 进行静态分析所需的时间将会更长。因此,仅将静态分析用于调试难以修复的错误,或在将应用程序提交到 iTunes App Store 之前作为最终步骤。要激活应用程序的静态分析,请点击产品 | 分析,让 Xcode 开始对您的代码进行静态分析:

使用 LLVM / Clang 静态分析器

根据您的代码库的大小,静态分析可能需要几秒钟到几分钟,因为它深入到您的代码中,以挖掘出任何潜在的问题。只有几行代码的项目进行静态分析可能只需几秒钟,而拥有数千行代码的大型项目可能需要几分钟或更长时间,具体取决于项目的大小。然后,点击 Xcode 屏幕左侧,如图所示,以查看 Xcode 通过静态分析发现的潜在问题。

默认情况下,静态分析会深入到代码库的每个角落。这会在你的机器上消耗大量资源,如果你有一个大的代码库或者一个慢速的机器,静态分析所用的时间可能会相当长。因此,如果你不想进行深入分析,你可以调整 Xcode 使用的静态分析级别,这样可能不会发现像深度静态分析那样多的问题,但仍然可以帮助暴露一些问题。静态分析很有用,因为它可以暴露出编译器无法检测到的错误,如溢出、除以零等。要更改静态分析的级别,请点击左侧的项目,然后选择 构建设置,然后查找 '分析' 的分析模式选项,并将其设置为 浅层(更快),如图所示:

使用 LLVM / Clang 静态分析器

使用 NSZombie

最后但同样重要的是,让我向你介绍 NSZombie 的概念。NSZombie 是一种内存调试辅助工具,可以帮助你在调试内存泄漏时。正如你所知,当一个对象的保留计数为 0 时,该对象将被释放并不再存在。然而,如果你启用了 NSZombie,保留计数为 0 的对象将变成 NSZombie 实例。然后,当这个 NSZombie 从你的代码的另一个地方收到消息时,它将显示警告而不是使你的应用崩溃或表现出不可预测的行为。

NSZombie 对于调试微妙的过度释放或 autorelease 错误很有用,因为这些类型的错误往往会表现为崩溃或奇怪的行为。NSZombie 将将这些崩溃和奇怪的行为显示为警告,这有助于你的调试。

NSZombies 处于一种奇怪的半存活/半死亡状态,因为当保留计数为 0 时,它们不会被释放,但它们也不是完全活着的。因此,NSZombie 是一个合适的术语,用来描述这些半活/半死的对象。

然而,一个需要注意的重要点是,一旦你完成调试,就应该禁用 NSZombies。NSZombies 消耗内存,就像保留计数为 0 的任何对象一样,它们被转换成 NSZombie,仍然占用内存而不是被释放。所以,如果你不禁用 NSZombie,它将占用更多的内存。为了利用 NSZombie 的力量,使其记录警告而不是使你的应用崩溃或表现出不可预测的行为,只需遵循以下简单步骤来激活 NSZombie:

  1. 在你的 Xcode IDE 中点击 产品 菜单。

  2. 选择 方案 菜单项。

  3. 继续点击 编辑方案... 以打开弹出窗口启用 NSZombie:使用 NSZombie

  4. 然后,你会看到一个带有 启用僵尸对象 选项的弹出窗口出现。点击此复选框,NSZombie 将被启用。

  5. 最后,运行你的项目,你将看到 NSZombie 在行动中:使用 NSZombie

最后,这里有一个表格概述了在哪种情况下应该使用哪种调试工具,以便你可以根据情况使用正确的工具:

调试工具 适当的上下文
Xcode Instruments 这用于在运行时找到导致崩溃的内存泄漏
静态分析器 这用于在代码执行前分析代码库中的问题,例如除以 0、内存问题等
NSZombie 这用于显示警告而不是因为内存泄漏而崩溃

摘要

我们讨论了内存管理理论的一些方面,例如保留循环和强/弱引用。然后,我们转向内存泄漏的后果和不同的警告级别。在此之后,我们发现了如何获取崩溃日志以帮助您获取有关您的应用程序和代码的信息。然后,我们查看了一系列 Xcode 拥有的各种工具列表,例如 Instruments 和 NSZombies,这些工具将帮助我们调试由多种原因引起的内存泄漏,例如未释放对象或过早释放对象。最后,我们以静态分析、启用 NSZombies 及其用途的描述作为结束。有了所有这些工具和信息,我希望调试内存泄漏和错误对您来说已经变得更加容易,因为您拥有了完成这项任务所需的所有工具,使这个过程不再那么痛苦。

在下一章中,我们将介绍一些关于内存管理的开发者技巧,例如获取器、设置器和其他技巧。那么,让我们进入下一章。

第十章. 内存管理的技巧和窍门

内存管理是每个处理 Objective-C 的程序员都会遇到的问题,尽管苹果引入了许多工具来帮助查找与内存相关的问题,如 Instruments 和 NSZombies。在本章中,我们将探讨一些更微妙的技术和工具,有些是显而易见的,有些将为你提供关于 Objective-C 的新视角。我们还将涵盖一些重要主题,例如:

  • 使用 @property 关键字

  • 使用获取器/设置器方法

  • 理解 Objective-C 中的属性属性

  • 何时避免使用 KVC 和 KVO

因此,让我们开始吧!

Objective-C、C 和内存管理

Objective-C 和 C 编程语言密切相关,因为 Objective-C 是 C 的一个真正的超集,这意味着在 C 中工作的一切都将与 Objective-C 一起工作。因此,本质上这也意味着你在 C 或 C++ 中熟悉的内存管理方法和协议也将适用于 Objective-C。然而,Objective-C 的一个好处是编译器在幕后为你做了大量的内存管理工作。这意味着与 C 或 C++ 相比,在 Objective-C 中处理内存管理不需要编写太多的代码。

然而,请注意,尽管你可以混合使用 C++ 和 Objective-C,但 Objective-C 并不是 C++ 的超集。这并不意味着你可以完全放手内存管理,因为 Objective-C 没有像 Java 那样的垃圾回收器。

随着 Xcode 4.2 和 iOS 4 及 5 及以后版本的 自动引用计数(ARC) 支持的发布,世界各地的开发者都认为他们繁琐的内存管理日子已经结束,但不要误解,你需要记住 ARC 是一个编译时内存管理机制,编译器将检查源代码,然后在编译后的代码中添加 retainrelease 消息。ARC 不是 Java 和 C# 程序员熟悉的传统垃圾回收机制,垃圾回收是在运行时由垃圾回收器完成的。

因此,ARC 的引入意味着作为开发者的你甚至需要更少的输入,因为你不需要在代码中显式地输入 retainrelease 消息,这使得你的代码更加冗长。然而,正如我们在前几章中看到的,当我们介绍了保留周期和其他类型的内存泄漏时,使用 ARC 意味着你仍然需要了解内存管理原则,这正是 Objective-C 和 Xcode 与其 C 编程语言对应物相比的优势所在。其内置机制帮助程序员通过一系列良好的实践避免内存泄漏。因此,让我们在本章中详细探讨这些良好实践。

获取器和设置器

如果您已经进行了一些 Java 和 C# 编程,并且来自 Java 或 C# 背景,您应该熟悉获取器和设置器方法,或者您可能也知道它们分别被称为访问器和修改器。它们是良好编程的基本支柱。获取器/设置器或访问器/修改器,也被称为用于保持封装原则的方法,其中成员变量被设置为私有以保护它们免受可能有害的其他代码的影响,而获取器/设置器充当私有成员变量和其他代码之间的守门人或中介。看看以下代码行:

public int getAge()
{
  return Age;
}
public void setAge(int _age)
{
  Age = _age
}

从 Java 或 C# 的角度来看,前面的两种方法应该不会让您感到陌生。如果使用不当,获取器和设置器可能会被认为是糟糕的。将变量设置为公共的,同时编写获取器和设置器方法是一个很好的例子,因为这违反了封装的概念。现在,获取器和设置器方法是推荐编程实践的良好基础,因为它们提供了以下好处以及更多:

  • 隐藏对象的内部状态

  • 设置不同的访问级别,如只读、只写等

  • 创建一个公共接口将使您在需要更改实现层时更容易进行代码更改,这在您需要跨多个文件进行更改时将变得明显

  • 允许您通过这些获取器和设置器方法对您的对象执行的操作进行严格的规则约束

获取器和设置器方法通常以 get 和 set 前缀开头。这可能会让您感到惊讶,但 Objective-C 对获取器和设置器方法提供了非常强的支持。然而,您可能会问:“Objective-C 中的获取器和设置器方法在哪里?我不记得设置过任何以 getset 开头的方法或编写过任何代码?”然而,实际上,它们确实存在,并且已经存在于您的代码中,但您还没有意识到这一点,因为 Objective-C 为您提供了一个抽象层,这样您就不需要花费太多时间编写获取器和设置器方法。这个抽象层允许我们稍后看到,您可以通过代码中的 @property 关键字定义获取器和设置器方法以及各种属性,如 readonlyreadwrite 等。获取器和设置器与内存管理密切相关,因为您可以在这些方法中编写代码来清理内存。

Objective-C 中的属性属性

如果您已经进行了一些 Objective-C 编程,您可能会遇到以下语法:

@property (nonatomic, readonly) UIView *rearView;
or
@property (nonatomic, retain) UIActivityIndicatorView *loadingView;

现在,我敢打赌,当你将这些属性如nonatomic等分配给对象时,你对诸如nonatomicretain之类的术语的含义可能只有一个模糊的概念。这些关键字,如nonatomicreadonly,实际上定义了您的对象的属性,这些属性在 Xcode 为您自动创建的 getter 和 setter 方法中使用。这些术语是与内存管理和访问控制相关的编码关键字,并不是为了迷惑您或给您增加额外的输入(至少不会像输入 getter 和 setter 方法那样多)。无论如何,让我们来看看这些术语的含义,以便您更好地理解这些关键字与 getter 和 setter 的关系:

属性名称 描述
nonatomic 这个属性不是线程安全的,但比atomic更快。
atomic 这个属性用于完整性,将不允许在您的代码中某个时刻有其他线程尝试访问此对象时发生不良情况。然而,由于需要额外的簿记开销,它比nonatomic慢。
strong 这与 ARC 一起使用,通过在您完成对象后自动释放对象,帮助您不必担心对象的 retain 计数。在不支持 ARC 的代码中,它是 retain 属性的同义词。
weak 这意味着引用计数不会增加 1,它不会成为对象的所有者,但它确实持有对它的引用。这是非 ARC 代码中unsafe_unretained的另一个术语。
assign 这个属性将生成一个 setter 方法,该方法将分配值给对象而不是复制或保留它。
copy 当对象是可变时使用,此时您会创建对象的副本。请注意,copy 不能与 retain 一起使用,因为对象的副本其 retain 计数已经增加 1。
readonly 这个属性将使对象为只读,代码的@implementation部分将不会创建 setter 方法。
readwrite 这意味着readwrite属性属性是可用的,并且会自动为您创建 getter 和 setter 方法。

因此,@property(nonatomic, retain) NSString *text将告诉编译器,“我有一个名为 text 的 NSString 类型的成员变量,所以我将需要一个 getter/setter 方法对,它们将使用 retain/release 过程。”

既然你已经定义了成员变量的属性,比如哪个是readonly,哪个是readwrite等等,接下来是什么?

接下来,您将使用@synthesize关键字。@synthesize关键字将告诉编译器,“既然我已经为我的NSString *text对象声明了nonatomicretain属性,请现在为我的NSString *text对象创建 getter 和 setter 方法对。”

因此,仅用这两行代码,我们就可以告诉 Objective-C 为我们创建获取器和设置器方法,并为我们的对象或变量分配只读、只写等属性。这比在 Java 或 C# 中输入冗长的获取器和设置器代码要好得多。

注意

请注意,从 Xcode 4.4 开始,@synthesize 关键字默认由 Xcode 自动提供,但可能存在需要您显式添加 @synthesize 关键字的情况,我们将在后面进行说明。

现在您知道 @synthesize 为什么会这样做。@property@synthesize 有助于自动化获取器和设置器方法的创建,以及通过仅几行代码轻松创建访问规则和控制。

看看我的属性声明:

@property (nonatomic, readwrite) int myInt;

@synthesize myInt;

在您的实现文件中,您会发现以下代码可以完美编译,这表明 _myInt 正在直接访问变量:

int yourInt = _myInt;

一旦变量被合成,就会自动创建一个实例变量(或简称为 iVar),并在其前面加上下划线。变量名中下划线的存在是一种命名约定,表示这是一个 iVar,Objective-C 会为您自动完成这项工作。

因此,当您以 _myInt 的形式调用 _myInt 时,不会出现编译错误,因为当您告诉编译器 myInt 将执行哪些属性时,编译器会自动为您创建 _myInt

@synthesize 还会创建验证规则,您可以使用 @property 关键字将这些规则分配给变量。例如,readonly 验证规则意味着当您尝试为变量赋值时,您将收到编译错误“只读属性不能重新赋值”,这正是您自动创建的设置器(修改器)方法的工作原理,而无需编写冗长的代码。

让我们看看一些代码,好吗?让我们创建一个名为 UserObject 的对象,并将其变量命名为 Age

那么,让我们开始吧!

  1. 我们首先点击 文件 | 新建 或按键盘上的 Command + N,然后选择 Cocoa TouchObjective-C 类,如图所示:Objective-C 中的属性属性

  2. 接下来,我们输入类的名称,即 UserObject,并将其留为 NSObject 的子类:Objective-C 中的属性属性

  3. 然后,点击 下一步,然后点击 创建,您的 UserObject 类将为您创建。然后,您应该在您的 UserObject.h 文件中得到以下内容。在 UserObject 头文件中添加一个名为 age 的整数,并将其 nonatomicreadonly 属性分配给它,如图所示:

    #import <Foundation/Foundation.h>
    
    @interface UserObject : NSObject
    {
        int age;
    }
    @property (readwrite, nonatomic) int age;
    @end
    
  4. 现在如果你尝试构建你的代码,你会得到一个警告,自动生成的属性 'age' 将使用自动生成的实例变量 '_age',而不是现有的实例变量 'age',因为你没有在UserObject类的.m实现文件中显式添加@synthesize age代码。

    这个警告只是一个友好的提醒,因为你没有添加@synthesize age代码,Xcode 将为你的所有 setter 和 getter 方法创建一个名为_age的实例变量。这是一个无害的警告,但对我来说,我更喜欢尽可能让我的代码没有警告,所以我会在UserObject.m实现文件中添加@synthesize age;代码行,得到如下所示的内容:

    @implementation UserObject
    
    @synthesize age;//This is to remove the warning
    
    @end
    
  5. 接下来,我们在UserObject类中添加NSString *name,并分配readwritenonatomic属性,这样我们的代码现在看起来如下。如前所述的readwrite属性将告诉编译器我们希望自动为我们创建 getter 和 setter 方法,而nonatomic属性意味着我们接受age变量不是线程安全的:

    @interface UserObject : NSObject
    {
        int _age;
        NSString *name;
    }
    
    @property (readwrite, nonatomic) int age;
    @property (readwrite, nonatomic) NSString *name;
    @end
    
    while our .m implementation file will look like this
    
    #import "UserObject.h"
    
    @implementation UserObject
    
    @synthesize age, name;
    
    @end
    
  6. 我们现在可以使用UserObject *user = [[UserObject alloc] init]创建UserObject类的一个实例。

  7. 接下来,我们可以看到 Xcode 的神奇之处,我们放入以下代码:

    [user setName:"Joe"];
    

    注意,我们没有为NSString *name创建 getter 或 setter 方法,但 Xcode 足够聪明,一旦我们将属性分配给NSString *name,就会为我们创建它。

  8. 然而,在某些特定情况下,你可能想覆盖 Xcode 提供的默认 getter 和 setter 方法。这样做非常简单,以我们的int age为例,我们只需在我们的UserObject.h头文件中创建以下方法:

    -(void) setAge:(int)aAge;
    -(int) getAge;
    
  9. 我们在.mUserObject实现文件中放入自定义的 getter 和 setter 方法,如下所示:

    -(void) setAge:(int)aAge;
    {
        int MIN_AGE = 20;//add in our validation logic for our setter here
        if (aAge < MIN_AGE)
            age = 20;
        else
            age = aAge;
    }
    
    -(int) getAge
    {
        return age;
    }
    

因此,现在当你显式地使用如[self setAge];这样的语法调用setAge方法时,代码将调用你的自定义 setter 方法,因为你已经添加了自己的 getter 和 setter 代码来覆盖 Xcode 为你创建的默认 getter 和 setter 代码。这为你提供了效率和灵活性,因为 Xcode 会假设你希望变量的默认 getter 和 setter 方法,而你又有权在需要时覆盖它们,这可能会在特殊情况下发生。

性能指南

尽管与早期的诺基亚手机相比,iOS 设备如 iPhone 和 iPad 拥有更多的内存,但这并不意味着你可以对内存管理粗心大意。iOS 内存模型和其他移动操作系统不包括计算机操作系统上存在的磁盘交换空间,在计算机操作系统上,持久存储空间被用作内存空间的扩展,以便持久存储可以作为低内存情况下的一种 RAM 使用。因此,为 iOS 设备开发的 app 在可访问的内存量上更加有限。

使用大量内存会导致系统性能严重下降并触发三个内存警告级别,最后一个警告级别将导致您的应用程序崩溃。此外,在多任务模式下运行的应用程序将与所有其他具有更高优先级的应用程序(如短信应用程序和电话应用程序)共享系统内存。因此,在任何情况下,您都无法获得 100%的手机内存供您的应用程序使用,即使是全新的 iOS 设备也会有后台进程在运行。因此,减少 iOS 应用程序使用的内存量应该是一个高优先级任务,而不是应该归档为低优先级标签的任务。

如果您的设备中可用的空闲内存较少,这意味着系统将有更高的概率无法满足未来的内存请求。如果出现这种情况,系统将移除被怀疑的应用程序和非易失性资源。然而,这并不是一个好的解决方案,因为这只是临时的,那些挂起的应用程序和非易失性资源可能很快又会被需要。

iOS SDK 中的UIKit中的UIViewController类提供了有用的方法来帮助您在控制台中接收内存警告,这在之前的章节中已经看到。我列出了三种实现内存警告通知的方法:

  • 您应该实现applicationDidReceiveMemoryWarning代理方法,因为当您的应用程序有一些内存警告时,它将被触发。

  • 要在调试控制台中获取更细粒度的内存警告,例如Received memory warning. Level=1Received memory warning. Level=2,特别是针对您的UIViewController,您可以实现您自定义的UIViewController子类的didReceiveMemoryWarning方法。

  • 要达到类级别,您可以通过addObserver方法注册您的对象以接收UIApplicationDidReceiveMemoryWarningNotification通知,以便在内存不足时调用特定方法,如下所示:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(seeMemoryWarning:) name: UIApplicationDidReceiveMemoryWarningNotification object:nil];
    - (void) seeMemoryWarning:(NSNotification *)notification
    {
      NSLog(@"Low memory");
    }
    

一旦您在代码中看到任何这些警告被触发,您应该立即响应,查看如何编写代码来释放任何不想要的内存。以下是一些实现此目的的方法:

  • 通过调用removeFromSuperview方法(例如[myView removeFromSuperview];)移除对用户不可见但仍然被加载到内存中的任何视图。

  • 通过将它们设置为nil释放不在屏幕上的任何图像。

  • 通过调用release方法清除当前代码未使用的任何数据结构。

想象一下,你的应用程序中存在内存泄漏,导致崩溃的泄漏只有在应用程序使用 2 小时后才会出现。因此,如果你想在代码中复现内存泄漏并触发崩溃,你需要每次运行应用程序 2 小时以看到崩溃。这可能会是一个耗时的任务,因为你需要让你的应用程序持续运行,但幸运的是,Xcode 提供了一种在不实际产生内存泄漏的情况下触发内存警告的方法,这个功能归功于 iOS 模拟器。你可以点击硬件 | 模拟内存警告来触发内存警告,这样你就可以在相关的内存警告方法处理程序下编写和测试你的内存清理代码。

以下图表显示了你需要点击的位置来触发内存警告:

性能指南

这样做将允许你在低内存条件下测试你的 iOS 应用程序,然后编写相关代码以减少内存使用。

不要过度思考内存管理

内存管理并不是一件过于复杂或难以理解的事情。因此,为了进一步帮助你进行内存管理,这里有一些实用的技巧:

  • 你可以尝试将你的资源文件,如音频、图像和属性列表,尽可能地缩小。为了减少属性列表文件占用的空间,你可以在使用NSPropertyListSerialization类的同时,使用名为 Pngcrush 的免费开源命令行工具来压缩 PNG 文件,这样你可以节省 20%或更多的空间,具体取决于你的 PNG 文件。

  • Core Data 不仅仅是一个持久化存储框架。Core Data 提供了一种内存高效的方式来管理大型数据集,如果你操作大量结构化数据,使用 Core Data 持久存储或 SQLite 数据库作为数据存储,而不是使用 NSData 或 NSUserDefault,将确保你可以利用 Apple 自己的 Core Data 框架提供的有效内存使用。

  • 资源应该在需要时加载,例如,当你只需要在设备屏幕上看到它时。这被称为懒加载,我们在上一章中已经看到过。你可能会在真正使用之前提前加载所有资源。然而,这实际上意味着你的资源在当前时刻实际上没有被使用时却在占用内存。因此,为了优化内存使用,始终实践懒加载。

  • 最后,这是一个鲜为人知的技巧,你可以在你的 构建设置 中使用:你可以添加 -mthumb 编译器标志,通过使用 16 位指令而不是 32 位指令来帮助减小代码的大小,这会占用更少的空间,并且这可能导致高达 35% 的节省。然而,有一个注意事项是,如果你的 iOS 应用程序包含浮点运算密集型代码,并且你的应用程序需要支持 ARMv6,例如旧一代的 iPod Touch 和旧款 iPhone,那么不应该为你的应用程序使用 -mthumb 选项。然而,如果你的代码是为 ARMv7 编写的,那么你可以在你的 Xcode 项目中启用 -mthumb 选项,这是默认启用的。

何时避免使用 KVC 和 KVO

我们之前在 第七章 中讨论的 KVC 和 KVO,键值编程方法,看起来像是一个非常细粒度的通知机制,但如果使用不当,KVO 是可能出现错误的。如果你不是该键路径的观察者,removeObserver 方法会导致崩溃,因此精确跟踪你正在观察的属性是必须的。

KVO 只有一个回调方法。如果你有多个通知,你需要在同一个回调方法中处理它们,这使得你的代码显得不够优雅,就像这样:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  if ([keyPath isEqualToString:@"mySize"])
  {
            //Do something else
      }
   else if ([keyPath isEqualToString:@"anotherSize"])
   {
    //Do something else
  }
}

随着更多通知的出现,你将编写大量的 if-else 语句,你将能够看到代码将多么难以控制,并且会出现许多不良情况,如崩溃、错误等,这需要更多的调试时间。

如果多次注册 KVO,可能会使你的应用程序崩溃。如果你有一个父类正在观察同一对象上的相同参数,removeObserver 方法将被调用两次,这会导致第二次调用时崩溃。

KVO 以一种奇妙而神奇的方式工作,就像回调一样。使用回调的代码可能难以调试。所以,如果你对 KVO 有足够的经验,我建议从小型项目开始使用 KVO,因为 API 文档很少,如果你不熟悉 KVO,这可能会导致未来的调试问题。

摘要

最后,我们到达了本章的结尾。本章涵盖了 Objective-C 的一些细节,例如属性属性,这些属性你可能一直在输入,但并没有一个清晰的概念。我们还讨论了内存管理指南,我在其中概述了一些可以添加到你对内存管理和调试代码中内存相关问题的知识的技巧和窍门。本章仅涵盖了内存管理的一小部分,我希望你已经深入研究了前面的章节,其中更深入地讨论了各种内存管理技术。最后,还有一个章节在前面,我们将探讨 Xcode 6 的一些新工具和功能,你可以在你的项目中使用。那么,我们继续吧,好吗?

第十一章:Xcode 6 的功能

在这个最后的章节中,我们将深入探讨苹果提供的实际集成开发环境IDE),开发者使用它来创建 iOS 和 Mac OS 应用程序。

在本章中,我们将涵盖以下主题:

  • Xcode 6 简介

  • Storyboard 中的新特性?

  • Xcode 6 中的调试

  • Xcode 6 中的 Interface Builder

  • 探索游乐场

Xcode 6 介绍

Xcode 6 于 2014 年 6 月 2 日在全球开发者大会WWDC)上由苹果公司宣布,并于 2014 年 9 月 17 日正式发布。Xcode 6 在 iOS 和 Mac 开发者的功能和工具方面有了很大的改进,因为它支持苹果在 2014 年创建并宣布的新 Swift 编程语言。

Xcode 6 还包括了新的特性,如 Interface Builder 中的实时渲染,你的手写 UI 代码在 UI 画布中显示,并且当你输入代码时,任何更改也会立即反映出来。它还有一个新的视图调试工具,你可以使用它来帮助你以 3D 可视化方式查看你的 UI 层,这样你可以理解界面的组成,并查看和识别任何被裁剪或重叠的视图。

随着苹果每年发布具有不同屏幕尺寸的新设备,支持多个屏幕设备并不是一件容易的事情。然而,Xcode 6 现在有一些旨在减少为多个屏幕开发 iOS 应用程序的繁琐任务的新特性。因此,让我们深入本章,看看苹果为我们提供的新工具。

在本章的整个内容中,我们将用以下段落来检查 Xcode 6 内部的和新特性。

Storyboard 中的新特性

Xcode 6 在 Storyboard 和 Interface Builder 方面引入了一些新特性。自从 Xcode 4 引入以来,Storyboard 允许你通过可视化界面链接屏幕,描述不同屏幕之间的转换,并对所有屏幕有一个良好的概念性概述,因为它们都被放置在一个单独的文件中。Storyboard 一直是程序员、开发人员和设计师创建界面并使用 GUI 轻松链接的重要工具。这对于设计师来说尤其有用,因为它允许他们克服编写代码的恐惧,Storyboard 也允许他们轻松创建直观的界面。

同样,随着 Xcode 6 的引入,Storyboard 也增加了新的功能和变化。其中一些新的功能包括:

  • 允许 Storyboard 或 NIB 文件替换启动图像

  • 通用 Storyboard

现在,让我们更深入地探讨我之前提到的 Xcode 6 的两个新特性。

允许 Storyboard 或 NIB 文件替换启动图像

当 iOS 应用程序忙于加载其初始的第一个屏幕时,iOS(操作系统)将显示一个静态图像,这是应用程序开发者插入的。为了达到最佳效果,启动图像应该类似于应用程序的用户界面。有关启动图像的更多信息,可以在 Apple 的网站上找到,网址为developer.apple.com/library/ios/documentation/userexperience/conceptual/mobilehig/LaunchImages.html,其中提到以下几点:

"启动文件或图像提供了一个简单的占位符图像,iOS 在应用程序启动时显示。占位符图像给用户留下应用程序运行速度快、响应灵敏的印象,因为它立即出现,并迅速被应用程序的第一个屏幕所取代。每个应用程序都必须提供启动文件或至少一个静态图像。

在 iOS 8 及以后版本中,你可以创建一个 XIB 或故事板文件,而不是静态的启动图像。当你在 Interface Builder 中创建启动文件时,你使用尺寸类别来定义不同显示环境的不同布局,并使用 Auto Layout 进行细微调整。使用尺寸类别和 Auto Layout 意味着你可以创建一个适用于所有设备和显示环境的单个启动文件。"

在 iOS 8 和 Xcode 6 之前,开发者必须为每种屏幕尺寸提供启动图像,这些图像可以覆盖 iPad、iPhone 4S、iPhone 5S 等设备。如果你的应用程序是通用应用程序,这意味着你需要提供多个版本的启动图像以支持各种设备。但现在,随着使用故事板作为启动图像的引入,你可以使用 Auto Layout 创建单个启动故事板,这个启动故事板可以用作你支持的所有设备的启动图像。这是一个非常方便的节省时间的方法,因为这意味着你不再需要为各种屏幕尺寸创建多个启动图像。

因此,在接下来的几页中,我们将简要介绍如何使用故事板作为启动图像,你将欣赏到 Xcode 6 为所有开发者添加的这项附加功能。请注意,此功能仅在 iOS 8 上工作,不在 iOS 7 上工作。因此,如果你针对使用 iOS 7 的设备,那么使用故事板作为启动图像将不起作用,你需要回退到使用静态图像的旧方法。然而,考虑到这一点,大多数 iOS 用户在发布新版本时都会升级他们的操作系统。从现在开始,你应该经常使用故事板作为启动图像。但是,请记住,如果你需要支持 iOS 7,你可以使用启动图像作为运行 iOS 7 设备的后备,同时使用启动故事板为运行 iOS 8 的设备服务。那么,让我们开始吧,我将带你了解 Xcode 6 中这个新巧的功能。

从你的应用程序启动图像

对于本节,我们将创建一个简单的应用来加载故事板作为启动图像。那么,让我们开始吧,好吗?

  1. 首先,我们将创建我们的项目。在本教程中,我们将在选择 文件 | 新建项目 后使用单视图应用:从您的应用启动图像

    您还会看到以下屏幕:

    从您的应用启动图像

    然后,我们需要创建一个故事板,这将是在用户启动应用时看到的第一个图像,因此我们需要创建一个新的故事板,并将其命名为 launch.storyboard。请注意,我们需要向我们的启动故事板添加一个视图控制器,并可以添加其他控件,例如 UILabels:

    从您的应用启动图像

  2. 接下来,我们需要点击我们的项目,其名称为 LaunchApplication,然后设置 启动屏幕文件 选项为 launch.storyboard,这是我们刚刚创建的故事板:从您的应用启动图像

  3. 为了验证,我们可以前往 info.plist 并查找此键:启动屏幕界面文件。如果此键存在,则表示映射到该键的值是我们用于启动图像的故事板或 NIB 文件的名称:从您的应用启动图像

  4. 然后,我们需要前往我们的 launch.storyboard 文件,然后选择 launch.storyboard 文件的 视图控制器,然后点击属性检查器图标,确保已勾选 是否为初始视图控制器从您的应用启动图像

  5. 最后,我们需要构建项目并运行它,以查看启动图像现在显示的是 launch.storyboard 文件,其中包含文本 欢迎使用启动故事板从您的应用启动图像

只需几个步骤,我们就能使用故事板或 NIB 文件来替换我们的启动图像、PNG 图像,借助自动布局,只需几个步骤就可以轻松地用单个故事板或 NIB 替换多个启动图像。

只需这些简单的步骤,我们就可以使用启动故事板。

通用故事板

现在,让我们继续探讨 Xcode 6 的下一个酷炫功能,那就是通用故事板。通用故事板意味着你的故事板将能够以正确的位置显示 UI 元素,如UITextfieldsUIButtons,无论它是用 iPad、iPhone 6+等查看。因此,你可以创建一个故事板,并用于 iPad、iPhone 和其他设备。那些可以一次性下载并在 iPhone 和 iPad 上同样良好运行的通用应用程序,现在已成为 iTunes App Store 中的常态。过去,存在为 iPhone、iPad、视网膜设备和非视网膜设备生成不同布局的问题。然而,随着自动布局的引入,它让全球的开发者生活变得更加容易,而 Xcode 6 通过添加通用故事板使其变得更加容易。使用通用故事板,Xcode 6 现在允许我们在使用自动布局创建用户界面布局后,轻松地看到我们的布局在不同分辨率的设备上的外观。

要激活通用故事板,我们只需要几个简单的步骤,并使用我们创建的 Xcode 项目启动图像来展示我们需要执行的简单步骤。请注意,此功能仅在 iOS 8 上有效。

首先,我们需要在Main.storyboard中选择我们的视图控制器,然后点击屏幕右侧的文件检查器图标,并确保使用大小类被勾选:

通用故事板

接下来,你会在你的故事板底部注意到一个可以点击的图标,你可以拖动并调整其大小来模拟你的自动布局用户界面在不同屏幕布局下的外观,例如 iPad 纵向、iPhone 横向等。因此,请随意点击并移动它,看看你的布局将如何呈现,然后根据你的偏好和规格进行调整:

通用故事板

Xcode 6 中的调试

如你所见,Xcode 6 为我们开发者增加了一些实用的新工具,以帮助我们工作。然而,不仅如此,调试现在也变得更加容易,因为一些额外的功能现在已成为 Xcode 6 的一部分。以下是一些提供的调试好工具:

  • 视图层次结构调试器

  • 调试仪表盘

  • 增强的队列调试

在额外的调试好工具列表中,视图层次结构调试器是其中最有用的一个。在 Xcode 6 之前,如果你想查看应用程序的视图层次结构,你必须使用插件,如 Spark Inspector、Reveal 等。然而,随着 Xcode 6 的发布,视图层次结构可视化现在得到了官方支持,你将在 Xcode 6 中获得视图层次结构的全部功能。

要在 Xcode 6 中使用视图层次结构调试器,您需要确保您的应用程序当前正在运行,然后您需要点击 Xcode 底部的调试视图层次结构图标,如图所示,当您移过图标时,按钮将显示鼠标悬停文本调试视图层次结构

Xcode 6 中的调试

当您点击该图标时,您将看到一个旋转的UIActivityIndicator图标出现几秒钟,然后您的当前视图的图像出现。然后您只需向上、向下、向左和向右拖动,就可以沿着 3D 轴旋转您的视图,以查看图像是否对齐,如图所示:

Xcode 6 中的调试

视图层次结构调试器将有一些选项,如图所示,您可以使用这些选项来帮助您的调试:

Xcode 6 中的调试

从左到右,以下表格显示了各个按钮的功能:

图标 按钮名称 功能
Xcode 6 中的调试 显示裁剪内容 这将隐藏或显示被裁剪的内容
Xcode 6 中的调试 显示约束 这显示了自动布局约束
Xcode 6 中的调试 重置查看区域 这将视图重置为默认状态
Xcode 6 中的调试 调整视图模式 这显示了带有内容的线框视图
Xcode 6 中的调试 缩放、实际大小和缩放 这将设置视图的缩放比例

调试仪表盘

调试仪表盘已经增加了两个新的仪表盘,它们是:

  • 网络活动仪表盘

  • 磁盘活动仪表盘

网络活动仪表盘将显示发送和接收的数据量,以及一个打开端口的列表和诸如 IP 地址等详细信息,如下面的截图所示。您将使用此网络活动仪表盘的典型场景是当您需要跟踪发送和接收的数据量以进行网络优化时,同时查看远程 IP 地址和端口号,以便您了解设备连接的位置。

这些功能将有助于您最小化发送和使用的网络流量,使用网络活动仪表盘将是您首先应该查看的地方:

调试仪表盘

磁盘活动调试仪表盘将显示应用程序对磁盘进行的所有读取和写入操作的真实时间数据。它还提供了所有打开的文件信息,以及磁盘 I/O 活动日志,供您查看,您可以在下面的屏幕截图中看到。如果您正在开发对磁盘进行大量读取和写入操作的应用程序,并且遇到不规则的磁盘读取和写入故障,那么这个磁盘活动调试仪表盘将为您带来巨大的帮助,因为它会告诉您读取和写入操作的大小。这是一个非常适合您的工具,您可以使用这些数据来跟踪您实际读取和写入到磁盘的数据量,这反过来又可以帮助您更好地了解情况,以便修复问题:

调试仪表盘

Interface Builder 的新功能

在 Xcode 6 中,Interface Builder 新增的功能很少,它们是:

  • 实时渲染

  • 大小类

  • 预览助手

实时渲染功能正如其名所暗示的那样工作。因此,实时渲染所做的就是在 Xcode IDE 中显示和渲染自定义对象,如自定义按钮、字体等,而无需编写一行代码。这意味着当您更新自定义对象的代码时,Interface Builder 设计画布将自动更新为编辑器中输入的新外观,而无需您构建和运行项目来在模拟器或设备上查看。以前,您必须运行应用程序才能看到对自定义对象所做的更改,这些对象是您在故事板中创建或通过编程创建的,并且具有自定义的外观。然而,现在,苹果通过在 Xcode 6 中引入实时渲染来简化了我们的工作,因为它在开发过程中节省了我们的时间,使我们不必浪费时间去构建和运行代码以查看自定义对象。您可以预期构建和运行代码成千上万次,甚至数百万次,因此节省的每一秒都会在后续的开发中节省您数小时的时间。

Xcode 6 中新增的最后一项功能是大小类概念;在我介绍通用故事板概念时,我们简要地讨论了大小类。为了更详细地解释,iOS 8 的大小类允许开发者创建和设计一个通用的故事板,并为 iPad 和 iPhone 提供定制的布局。随着大小类的引入,您可以一次性定义常见的视图和约束,然后为每个支持的设备屏幕和形态添加您自己的自定义变体。

最后,Xcode 6 中还有一个令人兴奋的新功能,即预览助手。预览助手允许您预览并查看您的布局在不同设备/目标上并排时的外观。因此,您可以看到您的布局在 iPad 或 iPhone 4S 上并排时的样子。要激活预览助手,您需要点击右上角的显示助手编辑器按钮以激活助手编辑器,然后当助手编辑器出现时,点击带有两个相互连接图标的图标,这将显示一个名为预览的菜单项。点击预览项并选择您希望预览的故事板,如图所示:

Interface Builder 中的新功能

接下来,您可以在左下角看到一个+图标。点击它,您将看到一个包含不同屏幕尺寸的 iOS 设备列表,例如 iPhone 4 英寸、iPhone 4.7 英寸等。

Interface Builder 中的新功能

这些对应于您希望预览的各种 iOS 屏幕设备。因此,点击一个设备,将出现一个显示该屏幕大小的画布,在该画布中,您可以看到您选定的故事板在该屏幕尺寸下的外观。因此,无需构建和选择您的目标模拟器,Xcode 6 允许您预览布局的外观,而无需浪费几秒钟的构建时间。这个功能对故事板和 XIB 文件也很有用。总之,使用预览助手的步骤如下:

  1. 点击右上角的显示助手编辑器以激活助手编辑器。

  2. 点击带有两个相互连接图标的图标以显示名为预览的菜单。

  3. 点击预览项并选择您希望预览的故事板。

  4. 点击左下角的+图标以选择一个 iOS 设备列表,以查看您的故事板在该选定设备屏幕上的外观。

Swift 的 Playground

苹果公司在 2014 年的 WWDC 上宣布了 Swift 编程语言,与此相应,Xcode 6 带来了一个名为 Playground 的新功能,您可以在其中拥有一个交互式工作区来编写 Swift 代码并在 Xcode 中获得实时反馈。这使得编写 Swift 代码变得简单且有趣,因为您可以在一行代码中输入并立即看到结果。如果您的代码通过循环迭代,您可以通过时间线助手查看其运动。时间线助手还会以图表形式显示您的变量,并在视图组合时绘制每个步骤。为了更好地了解 Playground,让我们通过一个简单的项目来尝试一下:

  1. 首先,我们需要通过选择文件|新建|Playground菜单来创建一个新的 Playground,并像您在这里看到的那样为我们的 Playground 命名。对于这个项目,让我们将我们的 Playground 项目命名为 MyPlaygroundSwift 的 Playground

  2. 接下来,将出现一个屏幕,您可以在其中输入 Swift 代码,结果将立即显示在右侧。为了测试它,请尝试输入以下代码:

    import UIKit
    
    var str = "Hello, playground"
    var name = "Gib"
    
    var sum = 0
    for i in 0...10
    {
        sum += i
    }
    
    sum
    
  3. 接下来,你将在屏幕右侧看到你的 Swift 输入的结果,你可以在这里看到:Swift 游乐场

    现在,这看起来相当酷,因为实时反馈会告诉你输出结果,并提供代码验证。这对于想要测试算法而不必构建代码的开发者来说很有用,或者你可以显示绘图代码并立即看到它。

虽然游乐场听起来不错,但有一些限制需要注意,特别是关于游乐场的限制。以下是不能使用游乐场完成的限制列表:

  • 它不能用于用户交互

  • 游乐场只能在模拟器上运行,而不能在设备上运行

  • 客户端库和框架不能导入,因为只能使用系统库和框架

摘要

正如你所见,随着新工具如视图层次结构调试器、预览编辑器和新增功能(如允许将故事板和 NIBs 用作应用的启动图像,而不是仅仅静态图像)的引入,Xcode 为开发者向前迈出了重要的一步。有了所有这些可以玩的新组件,苹果使得开发者能够轻松地创建和编码酷炫的项目,并减少了完成这些任务的努力。因此,我将你留给你的编码任务,并希望你在阅读这本书并获得一些有用的提示时有一个愉快的时光。就此,我向你道别,祝你编码愉快。

P.S. 如果你希望深入了解 Xcode 6,这里有一个链接到官方的 Xcode 6 Apple 文档:developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/WhatsNewXcode/Articles/xcode_6_0.html.

posted @ 2025-10-23 15:11  绝不原创的飞龙  阅读(18)  评论(0)    收藏  举报