0%

Objective-C 回忆录

注:这篇基本上是重新翻看《Objective-C 基础教程》时的一些阅读笔记,内容偏入门级。熟悉 ObjC 的就不需要翻啦(ー`´ー)。

历史

早在 20 世纪 80 年代初,Brad Cox 为了融合流行的、可移植的 C 语言和优雅的 Smalltalk 语言的优势,设计出了 Objective-C 语言,它是 C 语言的一个扩展集。1985年,Steve Jobs 创立了 NeXT 公司,他们使用 Objective-C 语言基于 Unix 开发了 NeXTSTEP 操作系统。而在 Apple 收购了 NeXT 之后,从 NeXTSTEP 和 OPENSTEP 编程环境演化出来了著名的 Cocoa 编程工具箱,从此 Cocoa 和 Objective-C 就成了 Apple 公司 OS X 和 iOS 操作系统的核心。

Objective-C 小知识点

  • Xcode 通过 .m 扩展名来表示文件使用的时 Objective-C 代码,应由 Objective-C 编译器处理。而 C 编译器处理 .c 文件,C++ 编译器处理 .cpp 文件。所有这些编译工作默认由 LLVM 处理。(扩展名 .m 表示 message)
  • 通过#import导入的头文件使用预编译头文件(压缩的、摘要形式的头文件)的方式来加快读取速度。
  • 导入头文件使头文件和源文件之间建立了一种紧密的依赖关系。如果头文件有任何变化,那么所以依赖 它的文件都得重新编译。
  • 头文件中的@class指令用于创建一个前向引用,在编译器只需要知道这是一个类,后面只会通过指针去引用它时提供了一个缩短编译时间的好方法,此外,还可以有效解决两个类之间循环依赖的问题。但是在诸如继承时则不能使用,因为编译器需要知道所有超类的信息才能成功为其子类编译@interface部分。
  • @selector()返回一个指向有特定名称的选择器的 SEL 指针。什么是选择器呢?选择器只是一个方法名称,但它以 Objective-C 运行时使用的特殊方式编码,以快速的执行查询,可以使用@selector()编译指令圆括号中的方法名称来指定选择器。
  • @protocol()返回一个指向有特定名称的协议的 Protocol * 指针。
  • Objective-C 运行时生成一个类的时候,会创建一个代表该类的类对象。类对象包含了指向超类、类名和类方法列表的指针,还包含一个 long 型的数据,为新创建的实例对象指定大小。用来创建新对象的类方法称为工厂方法。

SEL-Methods-IMP

布尔类型

在早期的 32 位系统下,BOOL实际上是一种对带符号的字符类型(signed char)的typedef,它使用 8 位的存储空间,通过#define指令把YES定义为 1,NO定义为 0。编译器只将BOOL认作 8 位二进制数,所以将大于 1 字节的整型值赋给一个BOOL变量,那么只有低位字节会被用作BOOL值。
Objective-C中的真值类型和数值图:BOOL / bool / Boolean / NSCFBoolean
目前在64位 iOS, tvOS, watchOS 系统中 BOOL 其实是 bool 的 typedef,也就是说 BOOL 只有0(NO),1(YES)两个值。

1
2
3
4
// iOS, tvOS, watchOS:
typedef bool BOOL;
// macOS
typedef signed char BOOL;

OOP

OOP 是一种编程架构,可构建由多个对象组成的软件。软件就好比存在于计算机中的小零件,它们通过互相传递信息来完成工作。
过程式编程建立在函数之上,数据为函数服务,而面向对象编程则以程序的数据为中心,函数为数据服务。数据可以通过间接方式引用代码,代码可以对数据进行操作。
对象到底是什么呢?对象是一种包含值和指向其类的隐藏指针的结构体。类是一种能够实例化成对象的结构体,类含有一个指针用于指向实现某个功能的代码。(类对象有什么用呢?让每个对象直接指向各自的代码不是更简单嘛?确实是更简单一些,而且某些 OOP 系统也是这样做的。但是拥有类对象会具备极大的优势,如果在运行时改变某个类,则该类的所有对象都会自动继承这些变化。)
在 Objective-C 中调用方法时,一个名为 self 的秘密隐藏参数将被传递给接收对象,而这个参数引用的就是该接收对象,例如,在代码[circle setFillColor:kRedColor]中,方法将 circle 作为 self 参数进行传递。由此方法可以使用此隐藏的 self 参数查找并操作对象的数据。

继承

方法调度

对象在收到消息时,如何知道要执行哪个方法呢?当代码发送消息时,Objective-C 的方法调度机制将在当前类中搜索相应的方法,如果无法在接受消息的对象的类文件中找到相应的方法,它就会在该对象的超类中进行查找。

支持继承程序中的方法调度

实例变量

在创建一个新类时,其对象首先会从它的超类继承实例变量,然后根据自身情况添加自己的实例变量。

1
2
3
4
5
@interface RoundedRectangle : Shape
{
int radius;
}
@end

下图展示了RoundedRectangle对象的内存布局。
对象中实例变量的布局
最上面是 NSObject 对象声明的名为 isa 的实例变量,它保存着指向对象当前类的指针,接下来是由 Shape 类声明的两个实例变量 fillColor 和 bounds,最后是由 RoundedRectangle 类声明的实例变量 radius。
每个方法调用都获得了一个名为 self 的隐藏参数,它是一个指向接收消息的对象的指针,self 指向继承链中第一个类的第一个实例变量,如上图所示也就是 isa 变量。因为编译器已经看到了所有这些类的 @interface 声明,也就知道了对象中的实例变量的布局,根据这个基地址再加上偏移地址,编译器就可以查找其他实例变量的位置了。
脆弱的基类问题:在 Snow Leopard 和 iOS4.0 系统中引入 64 位的 Objective-C 运行时之前,即使苹果工程师想在 NSObject 中添加其他的实例变量也是无法做到的,因为在编译器生成的程序中,那些偏移位置是通过硬编码实现的。在引入运行时之后它使用间接寻址方式确定了变量的位置(把实例变量当做一种存储偏移量所用的特殊变量,交由类对象管理,偏移量会在运行时查找,如果类的定义变了,那么存储的偏移量也就变了。因此任何时候都能访问到实例变量正确的偏移量,甚至可以在运行时向类中新增实例变量,这就是稳固的 ABI 机制,通过这个机制我们可以在类扩展或实现文件中定义实例变量),从而解决了这个问题。

super

为了调用继承的方法在父类中的实现,需要使用 super 作为方法调用的目标。super 既不是参数也不是实例变量。当你向 super 发送消息时,实际上是在请求 Objective-C 向该类的超类发送消息。 如果超类中没有定义该消息,Objective-C 会和平常一样继续在继承链上一级中查找。
调用超类的方法

存取方法

如果要对其他对象中的属性进行操作,应该尽量使用对象提供的存取方法,绝对不能直接改变对象里面的值,例如:main()函数不应该直接访问 Car 类的 engine 实例变量(通过car->engine的方法)来改变 engine 的属性,而应该使用 setter 方法进行更改。
在 Objective-C 中所有对象间的交互都是通过指针实现的。

Foundation

为什么诸如 CGRect, CGPoint, CGSize 等数据类型是 C 的 struct 而不是对象呢?原因在于性能!GUI 程序通常会使用许多临时的坐标、大小和矩形区域来完成工作。但是所有的 Objective-C对象都是动态分配的,而动态分配是一个代价较大的操作,它会消耗大量的时间。

NSString

C 字符串是将字符串作为简单的字符数组进行处理,并且在数组最后添加尾部的零字节作为结束标志。
NSString 的 length 实例方法能够精确无误的处理各种语言的字符串。因为一个字符占用的可能多余一个字节。这样在 C 语言的strlen()函数只能计算字节数,就会返回错误的数值。

NSArray

NSArray 是用来存储对象的有序列表,你可以在 NSArray 中放入任意类型的对象,但是它只能存储 Objective-C 对象,而不能存储原始的 C 语言基础数据类型,如 int, float, enum, struct 和 NSArray 中的随机指针。此外,它还不能存储 nil。
没有创建 NSMutableArray 和 NSMutableDictionary 的字面量语法。
对可变数组进行枚举操作时,需要注意不能通过添加或删除这类方式来改变数组的容量
NSArray 中添加了通过代码块来枚举对象的方法:

1
2
3
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

为什么有了快速枚举还要代码块枚举呢?因为通过代码块可以让循环操作并发执行,而通过快速枚举,执行操作要一项项地线性完成。

NSDictionary

为什么不用数组存储而要用字典呢? 因为字典(也被称为散列表)使用的是键查询的优化方式,可以立即找出要查询的数据而不需要遍历整个数组。
尽量不要创建 NSString, NSArray, NSDictionary 的子类,因为它们都是以类簇的方式实现的。

NSValue

NSValue 可以封装任意值。

NSPredicate (谓词)

NSPredicate用于指定数据被过滤的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface Engine: NSObject
@property (nonatomic, assign) NSInteger horsepower;
@end

@implementation Engine
@end

@interface Car : NSObject
@property (nonatomic, strong) Engine * engine;
@end

@implementation Car
@end

Engine *engine = [[Engine alloc] init];
engine.horsepower = 300;
Car *car = [[Car alloc]init];
car.engine = engine;

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"engine.horsepower >= 120"];
BOOL match = [predicate evaluateWithObject:car];

-evaluateWithObject:计算指定对象 car 是否满足谓词 predicate 中的条件。本类的谓词中使用 engine.horsepower 作为键路径,对 car 对象应用valueForKeyPath:方法获取引擎的马力。然后比较其是否大于等于 120。
NSPredicate 一般用于对集合类中数据的过滤,使用方法可以参考 NSHipster 的这篇文章:NSPredicate。另外,在谓词字符串中可以使用 LIKE 如:"name LIKE '???er*'" 将会匹配 er 前有3个字符,后面还有一些字符的 name 字符串变量。 也可以是使用 MATCHES 运算符类赋给它一个正则表达式,从而来选择匹配的值。

内存管理

如果一个对象内有指针指向其他对象的实例变量,则称该对象拥有这些对象。如果一个函数创建了一个对象,则称该函数拥有这个对象。“拥有一个对象”意味着该实体要负责确保对其所有的对象进行清理。
NSObject 类提供了一个-(id)autorelease;的方法,当给一个对象发送 autorelease 消息时,实际上是将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中的所有对象发送 release 消息。例子如下:

1
2
3
4
- (NSString *)description {
NSString *description = [NSString alloc] initWithFormat:@"I am %d years old",4]];
return [description autorelease];
}

内存管理规则:

  1. 使用 new 、 alloc 和 copy 方法创建一个对象时,该对象的引用计数值为 1,当不再使用该对象时,你应该想该对象发送一条 release 或 autorelease 消息。这样对象将在使用寿命结束时被销毁。
  2. 当使用其他方法获得一个对象时,则假设该对象的保留计数器值为 1,而且已经被设置为自动释放了。
  3. 如果你保留了某个对象,就需要释放或者自动释放该对象,必须保持 retain 方法和 release 方法的使用次数相等。

自动释放池的释放时间是完全确定的:要么是在代码中自己手动销毁,要么是使用 AppKit 时在时间循环结束时销毁。自动释放池以栈的形式实现:当你创建了一个新的自动释放池时,它就被添加到栈顶。接收 autorelease 消息的对象将被放入最顶端的自动释放池中。
Objective-C 的垃圾回收器是新型的垃圾回收器,它定期检查变量和对象并且跟踪它们之间的指针,当发现没有任何变量指向某个对象时,就将该对象视为应该丢弃的垃圾。与自动释放池一样,垃圾回收也是在时间循环结束时触发的。

ARC

iOS 无法使用垃圾回收,垃圾回收期在运行时工作,通过返回的代码来定期检查对象。ARC 是在编译时进行工作的。它在代码中插入了合适的 retain 和 release 语句。
ARC 只对可保留的对象指针有效,主要有三种:

  • 代码块指针
  • Objective-C 对象指针
  • 通过 _attribute((NSObject)) 类型定义的指针

声明变量时使用 __weak 关键字或对属性使用 weak 特性的归零弱引用会在指向的对象释放之后,将这些弱引用设置为零(nil)。
使用 ARC 时要注意:

  • 属性名称不能以 new 开头。
  • @property 声明的对象其内存管理特性默认为 assign。

拥有者权限

ARC 中的可保留对象指针可以与非可保留对象指针通过桥接转换的 C 语言技术来进行转换并对其指针的所有权进行管理。

  • __bridge type 操作符:可以使 void *id 对象指针相互转换,这个类型转换会传递指针但是不会传递它的所有权。
1
2
3
NSString *nsString = @"aString";
CFStringRef cfString = (__bridge CFStringRef)nsString;
//cfString接收了指针,但指针的所有权仍然由 nsString 保留,使用完 cfString 变量之后不需要调用 CFRelease 函数去释放它。
  • __bridge_retained CF type 操作符:这个类型转换会使要转换赋值的变量也持有所赋值的对象。会给对象的保留计数器加 1。__bridge_retained 转换与 retain 类似。
1
2
3
4
NSString *nsString = @"aString";
CFStringRef cfString = (__bridge_retained CFStringRef)nsString;
CFRelease(cfString);
//cfString 和 nsString 同时持有对象。使用完后 nsString 由 ARC 负责释放,但是 cfString 需要你调用 CFRelease() 函数释放。
  • __bridge_transfer Objective-C type 操作符,与上一个执行相反的操作,被转换的变量所持有的对象在该变量被赋值给转换目标后随之释放。__bridge_transfer 转换与 release 相似。
1
2
3
4
5
const char *cString = "cString";
CFStringRef cfString = CFStringCreateWithCString(NULL, cString, kCFStringEncodingASCII);
NSString *nsString = (__bridge_transfer NSString *)cfString;
NSLog(@"%@",nsString);
//nsString 持有该对象,使用完后 nsString 由 ARC 负责释放, cfString 在转换完成后释放。

在 struct 和 union 中是不能使用保留对象的。可以通过使用 void* 和桥接转换来解决这个问题。

记录一些疑惑:

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSString *ocString = @"aString";
CFStringRef cfString = (__bridge_retained CFStringRef)ocString;
CFShow(cfString);
}
return 0;
}

以上代码按照上面的理解,cfString 和 ocString 都是持有了对象的,但是用 Xcode9 的 analyze 来分析并没有曝出内存泄露问题? 不太能理解,希望看到的大神讲解一下。求教育!

对象初始化

分配对象

向某个类发送 alloc 消息就是从操作系统获得一块内存,并将其指定为存放对象的实例变量的位置。alloc 方法还顺便将这块内存区域全部初始化为 0,如 BOOL 类型变量初始化为 NO, float类型变量初始化为 0.0,指针初始化为 nil。刚分配的对象不能立即使用,需要先初始化,不然会出现奇怪的行为。

初始化

为什么要嵌套调用 alloc 和 init 方法?

1
Car *car = [[Car alloc] init];

而不是这样:

1
2
Car *car = [Car alloc];
[car init];

因为初始化方法返回的对象可能与分配的对象不同。像 NSString 和 NSArray 这样的类事件上是以类簇的方式实现的,所以 init 方法可以检查它的参数,并决定返回另一个类的对象更合适。
我们经常这样写初始化方法:

1
2
3
4
5
6
- (instancetype)init{
if (self = [super init]) {
//自定义初始化行为
}
return self;
}

代码中调用了[super init],其作用是让超类完成自身的初始化工作。由于 self 参数是通过固定的距离来寻找实例变量所在的内存位置的,如果从 init 方法返回一个新对象,则需要更新 self,以便其后的实例变量的引用可以被映射到正确的内存位置。而且这个赋值操作只影响该 init 方法中 self 的值,而不影响该方法范围以外的任何内容。如果在初始化一个对象时出现问题,则 init 方法可能会返回 nil。

指定初始化函数

类中某个初始化函数被指派为初始化函数,该类的所有初始化方法都使用指定初始化函数执行初始化操作,而子类使用其超类的指定初始化函数进行超类的初始化,通常接受参数最多的初始化方法是最终的指定初始化方法。
如果创建了一个指定初始化函数,则一定要在你自己的指定初始化函数中调用超类的指定初始化函数。

属性

  • @property 预编译指令的作用是自动声明属性的 setter 和 getter 方法。
  • @synthesize 预编译指令的作用是实现该属性的访问方法。所有属性都是基于变量的,当在 synthesize getter 和 setter 方法时,编译器会自动创建适当类型的实例变量,并且在属性名前加下划线,作为实例变量的名字。如果你没有声明这些变量,编译器也会声明的。注:Xcode 4.5 之后,可以不必使用 synthesize 了。
  • @dynamic 预编译指令告诉编译器不要自动生成任何代码或创建相应的实例变量。我们可以自己去写实现方法。

实例变量的声明可以放在头文件和实现文件中,区别在于若有一个子类,并且要从子类直接通过属性访问变量,那么变量就必须声明在头文件中。
在使用属性时,同时可以指定其各种特性,如:

1
@property (nonatomic, readwrite, assign) CGRect size;

展示了属性的默认的一些特性,其中比较重要的是这些内存管理语义的:

  • assign “设置方法”只会执行针对 scalar type 的简单复制操作,如: CGFloat, NSInter
  • strong 定义了一种”拥有关系“,设置方法会先保留新值,并释放旧值,然后将新值设置上去。
  • weak 定义了一种”非拥有关系,设置方法与 assign 类似,但是在属性所指的对象释放时,属性值也会被设置为 nil。
  • unsafe_unretained 语义与 assign 相同,但它适用于 object type ,表示“非拥有关系”,而且在目标对象释放时,属性值也不会被设置为 nil,所以是 unsafe 的。
  • copy 所属关系与 strong 类似,然而设置方法并不保留新值,而是拷贝它。通常用于 NSString, NSArray, NSDictionaty 及其子类。当源字符串是 NSString 时, copy 操作只是做了次浅拷贝,当源字符串是 NSMutableString 时, copy 操作是深拷贝,属性值指向拷贝生成的新对象。

在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部既可以使用“点语法”通过存取方法来访问实例变量,也可以直接访问实例变量。这两种方法有以下区别:

  1. 直接访问实例变量不经过 Objective-C 的方法派发,因此速度比较快。
  2. 直接访问实例变量,不会调用其“设置方法”,因此绕过了相关属性所定义的“内存管理语义”。
  3. 直接访问实例变量,不会触发“键值观察”。

因此在对象内部写入实例变量时,应该通过其“设置方法”来做,而在读取实例变量时,直接访问它。例外情况是在初始化方法及 dealloc 方法中应该总是直接访问实例变量。因为子类可能会 override 设置方法。这时在基类中通过设置方法来访问实例变量时将会调用子类的设置方法。(但是若使用了惰性初始化技术,则必须通过存取方法来访问属性)。

类别(Category)

利用 Objective-C 的动态运行时分配机制,可以为现有的类添加新方法。可以在类别中添加属性(必须是 @dynamic 类型的),但是不能添加实例变量,类别没有空间容纳实例变量,添加属性的好处在于可以通过点语法调用 setter 和 getter 方法。
使用类别时要注意避免命名冲突,当发生命名冲突时,类别具有更高的优先级,类别方法将完全取代初始方法。
类别主要有三个用途:

  1. 将类的实现代码分散到多个不同的文件或框架中(使用分类中方法时要引入分类的头文件。有时编写程序库时,将分类的头文件不随程序库一起公开,从而使用者就不知道库里还有这些私有方法)。
  2. 创建对私有方法的前向引用(在类别中声明该私有方法,然后将该类别置于实现文件的最前端,编译器就知道该方法已经存在,不会发出警告了。主要用于不方便在类的 @interface 部分列出方法或者使用的是尚未发布的私有方法。)。
  3. 向对象添加非正式协议,用于实现委托(创建一个 NSObject 的类别,然后在你的类中实现想要实现的方法。这也意味着只要对象实现了委托方法,任何类的对象都可以成为委托对象。)

类扩展是唯一能声明实例变量的分类,也可以改变属性的读写权限等,类扩展必须定义在其所接续的那个类的实现文件里,而且它没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,它没有名字。
为什么能在类扩展中定义方法和实例变量呢?因为有“稳固的 ABI 机制”,使得我们无需知道对象大小即可使用它,由于类的使用者无需知道实例变量的内存布局,所以他们就不必须定义在公共接口中了。
实例变量也可以定义在“实现块”里,如下所示:

1
2
3
@implementation EOCPerson {
int _anInstansceVariable;
}

从语法上来说,这与直接添加到类扩展中等效。

协议

Objective-C 不支持多重继承,但是我们可以通过协议这种方式描述接口,让类遵循协议,然后实现协议中的方法来扩展类的功能。协议最常见的用途是实现委托模式,不过也有其他用法。

委托模式

“委托模式”是一种实现对象间通信的编程设计模式,该模式的主旨是:定义一套接口,某对象若想要接受另一个对象的委托,则需遵从此接口,以便称为其“委托对象”(delegate)。而“另一个对象“则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。
有了协议之后,类就可以用一个属性来存放其委托对象了:

1
@property (nonatomic, weak) id<XXXDelegate> delegate;

需要注意的是这个属性一般都定义为 weak, 因为通常情况下扮演 delegate 的那个对象也要持有本对象,因此为了避免 retain cycle,存放委托对象的那个属性就得定义为weak 或者 unsafe_unretained。
在调用 delegate 对象的方法时,总是应该把发起委托的实例也一并传入方法中(通过协议方法的声明),这样, delegate 对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。
有时候需要优化委托对象是否能响应某个协议方法时(调用if([delegate respondsToSelector:@selector(xxx)])),可以将此信息缓存在某个结构体实例变量中。

匿名对象

有时候对象类型并不重要,重要的是对象有没有实现某些方法,在这种情况下可以用”匿名对象“来表达这一概念。如:id<XXXDelegate>,不需要知道此对象所属的类型,只有遵循 XXXDelegate 协议就好了。

数据持久化

数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。数据模型可以是任何数据结构或对象模型,存储模型可以是关系模型、XML、二进制流等。iOS 开发中常用的数据持久化技术有:plist 文件,NSKeyedArchiver,SQLite3,NSUserDefaults,CoreData 等。

plist 文件

plist 文件可以存储 NSArray, NSDictionary, NSString, NSNumber, NSData, NSDate类及其可变类的对象。一般有两种方式进行读写操作:

  • NSArray, NSDictionary, NSData及其子类可以直接调用writeToFile:atomically:方法将对象写入 plist 文件。
1
2
3
4
5
6
7
8
//写入
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"test.plist"];
NSArray *array = @[@1,@"string"];
[array writeToFile:filePath atomically:YES];
//... 读取
NSArray *otherArray = [NSArray arrayWithContentsOfFile:filePath];
NSLog(@"%@",otherArray);
  • NSPropertyListSerialization 类可以为存储和加载 plist 的行为提供很多设定项(比如修改数据的为可变类型的),它可以将 plist 的数据内容以二进制的形式写入文件,因此其提供的其实是 NSArray 和 NSDictionary 与 NSData 之间的转换功能。
1
2
3
4
5
6
7
8
9
//写入
NSDictionary *dic =@{@"one":@"1",@"two":@2};
NSError *error;
NSData *serializedData = [NSPropertyListSerialization dataWithPropertyList:dic format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
if (serializedData) {
[serializedData writeToFile:filePath atomically:YES];
}
//...读取
NSMutableDictionary *otherDic = [NSPropertyListSerialization propertyListWithData:serializedData options:NSPropertyListMutableContainersAndLeaves format:NULL error:&error];

NSKeyedArchiver 和 NSKeyedUnarchiver

遵循 NSCoding 协议并实现了其方法的对象都可以将它的实例变量和其他数据编码为数据块,然后保存在磁盘中,需要的时候再读会内存中创建新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ZAThing : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger number;
@property (nonatomic, strong) NSMutableArray *subThings;
@end

@implementation ZAThing
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
_number = [aDecoder decodeIntegerForKey:@"number"];
_subThings = [aDecoder decodeObjectForKey:@"subThings"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.number forKey:@"number"];
[aCoder encodeObject:self.subThings forKey:@"subThings"];
}
@end
1
2
3
4
5
6
7
ZAThing *thing = [[ZAThing alloc] init];
thing.name = @"111";
thing.number = 222;
NSArray *things = @[@1,@2];
thing.subThings = [things mutableCopy];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:thing];
ZAThing *otherThing = [NSKeyedUnarchiver unarchiveObjectWithData:data];

值得注意的是,在对象中还有嵌套的对象时,如上面的 subThings,在归档和反归档时会递归调用嵌套对象的 encode 和 decode 方法。(注:如果 subThings 中包含 thing 对象,这样循环包含的话,Cocoa 的归档和反归档也可以对其进行处理,但是不要用试图 NSLog 来打印, NSLog 不够智能不能处理这种情况)。