【iOS】归档

数组和字典都可以保存对象,如果把包含自定义对象的数组以写入到本地沙盒能保存成功么?

场景:两个按钮,分别是读写操作。数组中存放自定义对象,然后把数组存到沙盒。

自定义类

1
2
3
4
5
6
7
8
9
10
@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

@implementation DBPerson

@end

写数据

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)writeAction {
DBPerson *person = [[DBPerson alloc] init];
person.name = @"idbeny";
person.age = 10;

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *dataPath = [documentPath stringByAppendingPathComponent:@"person.data"];
NSArray *dataArray = @[@"idbeny", @30, person];
BOOL isSuccess = [dataArray writeToFile:dataPath atomically:YES];
NSLog(@"写入%@", isSuccess ? @"成功" : @"失败");
}

输出:写入失败

因为自定义对象写入到本地沙盒需要使用归档(其实就是编码)才可以实现。

一、NSCoding

任何对象保存到本地都需要遵守NSCoding协议,并且协议内容是必须实现的。

1
2
3
4
5
6
@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)coder;
- (nullable instancetype)initWithCoder:(NSCoder *)coder; // NS_DESIGNATED_INITIALIZER

@end
  • encodeWithCoder: 归档(编码),指定key值保存属性value
  • initWithCoder: 解档(解码),根据key值取对应的属性value

1.1. 遵守协议

1
2
3
4
5
6
@interface DBPerson : NSObject<NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

1.2. 实现协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation DBPerson

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}

@end

二、归档(编码)

对象归档使用的NSKeyedArchiver里面的archiveRootObject:类方法。

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)writeAction {
DBPerson *person = [[DBPerson alloc] init];
person.name = @"idbeny";
person.age = 10;

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *dataPath = [documentPath stringByAppendingPathComponent:@"person.data"];
NSArray *dataArray = @[@"idbeny", @30, person];
BOOL isSuccess = [NSKeyedArchiver archiveRootObject:dataArray toFile:dataPath];
NSLog(@"写入%@", isSuccess ? @"成功" : @"失败");
}

编译运行,崩溃了,DBPerson中没有encodeWithCoder:这个方法.

KeyedArchiver[79403:937363] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[DBPerson encodeWithCoder:]: unrecognized selector sent to instance 0x6000022088c0’

原因:当执行NSKeyedArchiverarchiveRootObject:时,会自动调用对象的encodeWithCoder:方法,该方法的目的就是告诉他保存对象的哪些属性。要想实现encodeWithCoder:方法,必须遵守NSCoding协议。

遵守协议后,我们再运行试下。输出:写入成功。

查看保存的路径:

三、解档(解码)

当执行NSKeyedUnarchiverunarchiveObjectWithFile:时,会自动调用对象的initWithCoder:方法,该方法的目的就是告诉他读取对象的哪些属性。要想实现initWithCoder:方法,同样须遵守NSCoding协议。

1
2
3
4
5
6
7
8
- (IBAction)readAction {
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *dataPath = [documentPath stringByAppendingPathComponent:@"person.data"];
NSArray *dataArray = [NSKeyedUnarchiver unarchiveObjectWithFile:dataPath];
NSLog(@"%@", dataArray);
DBPerson *person = dataArray.lastObject;
NSLog(@"name=%@,age=%ld", person.name, person.age);
}

编译运行,输出结果:

1
2
3
4
5
6
7
(
idbeny,
30,
"<DBPerson: 0x6000037e97c0>"
)

name=idbeny,age=10

四、扩展

4.1. 协议不遵守也可以,本质是给对象发送encodeWithCoder:这个消息,如果该方法没有实现才会崩溃。但为了写代码的时候能够有提示,遵守协议还是很有必要的。

4.2. 在上面的DBPerson案例中,initWithCoder:方法中为什么不能调用父类的initWithCoder:方法?

1
2
3
4
5
6
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {

}
return self;
}

但是继承自UIView的自定义View就可以调用父类的initWithCoder:方法

1
2
3
4
5
6
- (instancetype)initWithCoder:(NSCoder *)aCoder {
if (self = [super initWithCoder:aCoder]) {

}
return self;
}

这是因为UIView实现了NSCoding协议,NSObject没有实现该协议。只要一个类遵守了一个协议,他的子类都会遵守这个协议。

initWithCoder:调用时机:

  • 加载IB,开始解析对应类时调用(IB解析完成后会调用awakeFromNib);
  • 解档时会调用。