拷贝

265 阅读12分钟

1 、概述

拷贝分为两种:浅拷贝和深拷贝。

浅拷贝:浅拷贝是对内存地址(指针)的拷贝,使目标对象和原对象的指针指向同一块内存。原对象的引用计数增加1。

深拷贝:是新开辟了一块内存你空间并对原对象的内容(原对象的这块内存)进行复制,放入这块开辟的新的内存空间中。

2、使用方法

拷贝的使用方法时-(id)copy/-(id)mutableCopy
- (id)copy:会生成一个不可变对象
- (id)mutableCopy:会生成一个可变对象

在开发过程中,我们用到的拷贝可大致分为两类:对象(自定义对象和系统对象)的拷贝和容器类的拷贝,对象和容器类又可分为是否可变。

2.1 非容器对象的拷贝

OC中非容器对象一般有如 NSString,有不可变的非容器类对象比如NSString对象和可变的非容器NSMutableString对象。下面我拿NSString和NSMutableString举例。

2.1.1 不可变非容器对象的 浅/深 拷贝

NSString *originStr = @"origin";
NSString *originStrCopy = [originStr copy]; //浅拷贝
NSString *originStrMultiCopy = [originStr mutableCopy];//深拷贝
NSMutableString *multiOriginStrCopy = [originStr copy];//浅拷贝且返回不可变对象
NSMutableString *multiOriginStrMultiCopy = [originStr mutableCopy];//深拷贝且返回可变对象
[multiOriginStrCopy appendString:@"error"];//crash
[multiOriginStrMultiCopy appendString:@"string"];

当我们用 %p 查看每个对象的内存地址的时候,可以看到下图结果:

originStrCopy 的地址和 originStr 的地址是一样的,originStrMultiCopy 的地址和 originStr 的地址不一样,则 originStrCopy是由 copy浅拷贝生成的对象,指针指向了originStrCopy,originStrMultiCopy则是利用 mutableCopy 对originStr进行了深拷贝。

通过看五个对象的类型可以得出:copy得到的对象是一个不可变类型,而mutableCopy得到的对象是一个可变类型,此时我如果对multiOriginStrCopy 对象执行字符串拼接等操作,就会 crash,因为copy得到的对象是不可变的,也就是实际上multiOriginStrCopy是一个不可变的对象,那它为什么可以利用NSMutableString 的方法 appendString:呢?只是因为我在定义的时候,把它定义成了一个 NSMutableString 而已。

对于不可变的非容器类对象,copy方法为浅拷贝,原始对象的引用计数+1,产生的目标对象为不可变类型;mutableCopy方法为深拷贝,会开辟新一份内存放入原始对象的值,产生的目标对象为可变类型。

2.1.2 可变非容器对象的 浅/深 拷贝

NSMutableString *originStr = [NSMutableString stringWithString:@"origin"];
NSString *originStrCopy = [originStr copy];
NSString *originStrMultiCopy = [originStr mutableCopy];
NSMutableString *multiOriginStrCopy = [originStr copy];
NSMutableString *multiOriginStrMultiCopy = [originStr mutableCopy];

同上,我们用 %p来查看这五个对象的具体内存地址:

先看前三个对象:发现无论是 copy 生 成originStrCopy 还是 mutableCopy 生成的 originStrMultiCopy,与 originStr的内存均不同,这说明什么?

对于可变的非容器类对象无论是执行 copy 还是 mutableCopy,产生的目标对象都是新开辟了内存的对象,也就是均为深拷贝后产生的对象。copy产生的目标对象依然为不可变类型,mutableCopy产生的目标对象依然为可变类型。也就是我对multiOriginStrCopy 使用 append:这些方法的话,就会crash,道理同上。

2.2 容器的拷贝

OC中容器对象一般有如 NSArray,有不可变的容器类对象比如 NSArray 对象和可变的容器 NSMutableArray 对象。下面我拿 NSArray 和 NSMutableArray 举例。

2.2.1 不可变容器的 浅/深 拷贝

NSArray *originArray = [NSArray arrayWithObjects:@"a",@"b",@"c",nil];
NSArray *originArrayCopy = [originArray copy];
NSArray *originArrayMultCopy = [originArray mutableCopy];
NSMutableArray *multiOriginArrayCopy = [originArray copy];
NSMutableArray *multiOriginArrayMultCopy = [originArray mutableCopy];

[multiOriginArrayCopy addObject:@"d"];
[multiOriginArrayMultCopy addObject:@"d"];

同样的,我们查看各个对象的详细类型和地址信息:

详细看上图的originArray 和 originArrayCopy 及 originArrayMultCopy 三个对象的地址,originArray地址0x0000600003b8c270,originArrayCopy地址0x0000600003b8c270,originArrayMultCopy地址0x0000600003b8c2a0

前两者的地址是相同的,说明对于不可变的容器类执行copy方法时,是浅拷贝,执行mutableCopy时,是深拷贝操作,会为目标对象新开辟一块内存。而且,对一个可变对象的容器类执行copy操作得到的目标对象类型是不可变的对象,对一个可变对象的容器类执行mutableCopy操作得到的目标对象是可变类型的。

其次我们再看前三者数组中,包含的三个对象a,b,c的地址发现数组里面对象的地址都是相同的,说明什么呢?

对于不可变容器类对象即使是 mutableCopy操作,得到的目标对象都仅仅是对原对象执行了一次单层深拷贝,里面的对象还是浅拷贝拷贝了指针过来存入已开辟好的内存中。

同样,我们考虑一下如果我们对 multiOriginArrayCopy 执行 数组的增删改方法会怎样呢?

会crash,道理同上面一样,对于copy产生的目标对象是不可变的,之所以可以调用不可变数组的增删改方法是因为它被定义时是一个不可变类型。

2.2.2 可变容器的 浅/深 拷贝

NSMutableArray *originArray = [NSMutableArray arrayWithObjects:@"a",@"b",@"c", nil];
NSArray *originArrayCopy = [originArray copy];
NSArray *originArrayMultCopy = [originArray mutableCopy];
NSMutableArray *multiOriginArrayCopy = [originArray copy];
NSMutableArray *multiOriginArrayMultCopy = [originArray mutableCopy];

[multiOriginArrayCopy addObject:@"d"];
[multiOriginArrayMultCopy addObject:@"d"];

同样的,我们查看各个对象的详细类型和地址信息:

老样子,我们还是先看前三者各自的地址和类型:

originArray 和 originArrayCopy 及 originArrayMultCopy 三个对象的地址,originArray地址0x60000057e010,originArrayCopy地址0x60000057e100,originArrayMultCopy地址0x60000057e250

三者地址均不相同,说明对于一个可变的容器类对象,无论是执行copy还是mutableCopy,得到的都是一个全新的目标对象,由此可得:

对一个可变的容器类对象,执行copy是深拷贝得到一个不可变类型的目标对象,执行mutableCopy是也是深拷贝得到一个可变类型的目标对象。

其次我们再看各数组对象存储的对象的地址,发现每个index位置相同的对象(a,b,c)地址全都是相同的,说明了:

对于可变容器类对象即使是 mutableCopy操作,得到的目标对象都仅仅是对原对象执行了一次单层深拷贝,里面的对象元素还是浅拷贝拷贝了指针过来存入已开辟好的内存中,和不可变容器类的结论一致。

2.3 容器类单层深拷贝(OneLevelDeepCopy)/完全深拷贝(TrueDeepCopy)

前面说到,在容器类的拷贝中,容器里盛放的元素始终是指针复制,也就是虽然新建了一个容器,但是里面的元素还是指针拷贝存放的,也就是说copy 和metableCopy 对容器(Array或者Dictionary)不论是深拷贝或者浅拷贝,对于内部元素是指针拷贝。Apple文档将这个称为集合的单层深拷贝

如果想要实现元素的对象完全深拷贝就需要实现 trueDeepCopyArray。

NSArray *originArray = [NSArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c", nil];
NSArray *deepCopyArray=[[NSArray alloc] initWithArray: originArray copyItems: YES];
NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: originArray]];//归档将A数组序列化为一个Data,再将Data解归档为一个新的数组。此时的数组就是一个全新的数组

看下结果:

可以看到两个现象:

①在 deepCopyArray 中 b/c元素的内存地址还是和 originArray 中b/c的地址相同,但a元素不同

②在 trueDeepCopyArray 中各个元素内存地址完全不同,说明 trueDeepCopyArray 通过将集合对象进行归档(archive)然后解归档(unarchive)完全深拷贝得到。

initWithArray: copyItems 方法最后的参数 cotyItems 设置为 YES,如果用这种方法进行复制时,集合里的每个对象都会收到 copyWithZone: 消息。如果集合里的对象遵循 NSCopying 协议,那么集合就会被 copyWithZone 到新的集合里(copy,不是mutableCopy,上面示例里元素 a 是可变对象,调用 copy会生成新的不可变对象,所以地址改变。)。 如果对象没有遵循 NSCopying 协议,尝试用这种方法进行深复制时,将会在运行时出错。

将集合进行归档(archive),然后解档(unarchive)可以实现完全的深拷贝。 对于mutable对象会进行mutableCopy,而immutable对象则会新建一份内存来复制对象。

2.4 自定义对象的拷贝

先创建一个 Person 类,其中有两个属性 name 和 age。

@interface Person : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) NSInteger age;
@end

2.4.1 自定义对象的 copy

此时运行下列代码:

Person *person = [[Person alloc] init];
person.age = 20;
person.name = @"Box";
Person *personCopy = [person copy];

会发现此时发生了崩溃:[Person copyWithZone:] : unrecognized selector send to instance... 显然在调用 copyWithZone: 方法时,没有找到这个方法。没这个方法,那就添加呗。

so,对 Person 添加 NSCopying 协议方法 copyWithZone:** ①**

- (id)copyWithZone:(NSZone *)zone {
    Person *person = [[[self class] alloc] init];
    person.name = self.name;
    person.age = self.age;
    return person;
}

再次运行,可以发现 person 地址和personCopy 地址不同,说明对于[person copy] 生成了一份全新的对象。

2.4.2 对象属性用copy/strong修饰的不同之处

继续上代码:

Person *person = [[Person alloc] init];
person.age = 20;
person.name = @"Box";
NSLog(@"person.name=%@",person.name);
NSMutableString* otherName = [[NSMutableString alloc]initWithString:@"Jack"];
person.name = otherName;
NSLog(@"person.name=%@",person.name);
[otherName appendString:@" and Mary"];
NSLog(@"person.name=%@",person.name);

结果得到什么呢?

可变字符串 otherName 的改变(拼接/截取字符串操作)已经影响到 person.name 了。

这。。。。似乎有点“安全”隐患,万一不小心就被篡改了呢?

将 Person 的 name 属性改用 copy 修饰呢?

同样的执行代码结果是:

可以看到 otherName 无论怎么改变,已经影响不到 person.name 了,因为一个 mutable的字符串在经过 copy 以后已经不可变了,因为在对 name 赋值时是执行了下列 setter 方法:

- (void)setName:(NSString *)name
{
    _name = [name copy];
}

对于属性用 copy 还是用 strong, 应该具体场景具体对待,有一点需要记住:copy可以避免意外的数据操作。

2.5 NSCopying/NSMutableCopying协议是什么

若想要某对象具备拷贝功能,那么就要遵循 NSCopying 协议,我们自定义的非系统类必须手动实现,系统类的对象系统已经帮我们实现好了。

2.5.1 copyWithZone:

NSCopying 协议。该协议只有一个方法: - (id)copyWithZone:(NSZone *)zone

具体实现在上面①处已经看到,但是,方法①有些瑕疵,因为在此方法中,我们再次调用alloc,导致重新初始化空间,但这方法已给你分配了zone,自己就无需再次alloc内存空间了,避免空间浪费,更合理的方法应该是:

- (id)copyWithZone:(NSZone *)zone {
    Person *person = [[[self class] allocWithZone:zone] init];
    person.name = self.name;
    person.age = self.age;
    return person;
}

2.5.2 mutableCopyWithZone:

NSMutableCopying 协议。该协议只有一个方法: - (id)mutableCopyWithZone:(NSZone *)zone

该协议不会经常被用到,因为我们项目中也很少需要去操作一个可变的对象。

- (id)mutableCopyWithZone:(NSZone *)zone
{
    Person *person = [[[self class] allocWithZone:zone] init];
    person.name = self.name;
    person.age = self.age;
    return person;
}

3、归档和解档 —— 对象 和 NSData 的相互转换

NSCoding 协议

上面的 trueDeepCopy中用到了 NSKeyedArchiver (归档)NSKeyedUnarchiver(解档),在 OC 中,自定义的NSObject 对象需要遵守 NSCoding 协议来将对象归解档。

NSKeyedArchiver将自定义的类转换成NSData实例,类里面每一个值对应一个Key;NSKeyedUnarchiver将 NSData 实例根据 key 值还原成自定义的类。

NSCoder是一个抽象类

NSCoding协议有两个方法:

- (void)encodeWithCoder:(NSCoder *)coder

- (nullable instancetype)initWithCoder:(NSCoder *)coder

如果需要归档的对象是某个类的子类时,在归档和解档时,需要先调用 [super encodeWithCoder:aCoder][super initWithCoder:aDecoder]来执行父类的归解档方法。

NSSecureCoding 协议

iOS6之后,苹果基于NSCoding 引入了比 NSCoding 更安全的 NSSecureCoding协议,在解码方法中,要用 NSSecureCoding 的方法。

如果要遵守 NSSecureCoding 协议,需要实现的方法为:

+ (BOOL)supportsSecureCoding 功能正如方法名,返回YES即为支持加密编码。

解码方法示例:

initWithCoder 内使用 decodeObjectOfClass: forKey 而非 decodeObjectForKey:

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

编码方法示例:

encodeWithCoder:方法没有变化

如果是Object,就用 encodeObject: forKey: 如果是基本数据类型,就用 encodeInteger: forKey


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

4、小结

源对象类型 拷贝方法 副本对象类型 是否产生了新对象 拷贝类型
NSString copy NSString NO 浅拷贝
mutableCopy NSMutableString YES 深拷贝
NSMutableString copy NSString YES 深拷贝
mutableCopy NSMutableString YES 深拷贝
NSArray copy NSArray NO 浅拷贝
mutableCopy NSMutableArray YES 深拷贝
NSMutableArray copy NSArray YES 深拷贝
mutableCopy NSMutableArray YES 深拷贝

你用 copy 还是 strong 来修饰?

对于不可变NSString对象,若确定用NSString赋值,用strong;若确定用可NSMutableString赋值,用copy

如果使用copy修饰,在进行赋值时会先做一个类型判断,如果赋的值是一个不可变的字符串,则走strong 的策略,进行的是浅拷贝;如果是可变的字符串,则进行深拷贝创建一个新的对象,所以如果我们确定要赋的值是一个不可变的值,就不要用copy再去多一步类型判断,因为会增加无谓的开销,消耗性能。但是如果将一个可变字符串A 赋值给用 copy 修饰的属性B时,A再次拼接改变的时候, B的值不会变,因为通过copy出来一块新的内存区域; 而用strong修饰的时候A改变时B也会跟着改变,因为strong强行引用指针,是同一块内存区域,不可变的数组同理。所以如果确定要赋的值是可变字符串,就用copy。当然了,还有特殊情况,也就是如果是想让 B 跟着 A改变而改变,就用 strong,不想让 B 跟着 A 改变而改变的,就用 copy。

对于可变对象,倾向于用strong修饰:

用copy修饰的时候其实拷贝出来的一块内存区域是不可变, 用一个可变字符串给他赋值, 然后拼接这个属性时就会崩溃, 用strong修饰的时候就不会出现这种情况, 因为strong修饰后是指针强引用, 始终指向的同一块内存区域。