iOS开发快速进阶与实战
上QQ阅读APP看书,第一时间看更新

2.3 拷贝

这里的拷贝是对于在Objective-C中对象的拷贝。其实说到拷贝,在iOS中,或者更确切地说,在Objective-C中我们对拷贝关注得比较多一些,并且会联想到深拷贝和浅拷贝的概念。然而Swift中却很少有涉及拷贝的问题,即使Swift下的NSObject对象仍然有copy和mutableCopy方法,但究其原因,创建单纯的Swift类并不需要继承NSObject,而是使用Swift的类。另外,很多牵涉拷贝问题的数组和字典,在Swift中对应于Array和Dictionary,不同于NSArray和NSDictionary,Array和Dictionary是值类型,赋值后并不需要担心其拷贝问题。本节要介绍的是在Objective-C中的深拷贝和浅拷贝问题。

如果现在问你,什么是深拷贝?什么是浅拷贝?你或许知道,深拷贝是深度拷贝,是拷贝一个实例对象到一个新的内存地址,而浅拷贝只是简单拷贝一个实例对象的指针。在苹果的官方文档中提供了这样一个图示,如图2-2所示,用于解释深拷贝和浅拷贝的不同。

图2-2 深拷贝和浅拷贝

从图2-2中可以看到,集合的浅拷贝(Shallow Copy)后的数组Array 2与之前的数组Array 1指向同一段内存区域,而深拷贝(Deep Copy)下Array 2与Array 1分别指向不同的内存区域,从这一点来看我们刚刚所说的是正确的。

集合浅拷贝的方式有很多。当创建一个浅拷贝时,之前集合里面的每个对象都会收到retain的消息,使其引用计数加1,并且将其指针拷贝到新集合中。

     NSArray * shallowCopyArray = [someArray copyWithZone:nil];
     NSDictionary * shallowCopyDict = [[NSDictionary alloc]initWithDictionary: someDictionary
     copyItems:NO];

上面两句代码分别展示了创建数组和字典的浅拷贝对象,这是官方文档中的例子,如果你在工程中运行,并分别打印地址,代码如下。

     NSArray * someArray = @[@"2222"];
     NSArray * shallowCopyArray = [someArray copyWithZone:nil];
     NSLog(@"someArray address: %p", someArray);
     NSLog(@"shallowCopyArray address: %p", shallowCopyArray);
NSDictionary * someDictionary = @{@"11": @"22"}; NSDictionary * shallowCopyDict = [[NSDictionary alloc] initWithDictionary: someDictionary copyItems:NO]; NSLog(@"someDictionary address: %p", someDictionary); NSLog(@"shallowCopyDict address: %p", shallowCopyDict);

运行之后可以发现打印地址为:

     someArray address: 0x618000000910
     shallowCopyArray address: 0x618000000910
     someDictionary address: 0x6180000227c0
     shallowCopyDict address: 0x6180000228a0

可以发现浅拷贝前后的数组所指向的内存地址是一样的,而字典所指向的内存地址发生了变化,为何同样是浅拷贝,拷贝前后内存地址却发生了改变呢?这是因为对于数组我们只是调用了它的copyWithZone方法,但由于是不可变数组,返回了自身,所以浅拷贝前后数组的内存地址不变。而对于字典来说,shallowCopyDict是通过alloc、init创建的,因此在内存中开辟了一段新的内存空间,但对于之前字典中的对象,只是拷贝其内存地址,所以浅拷贝前后字典的内存地址发生了变化,其实内部元素的地址是不变的。引用此例是为了说明,在集合对象的浅拷贝中,并非是对于自身的浅拷贝,而是对于其内部元素的浅拷贝,接下来会详细分析。

刚刚介绍了集合类型的浅拷贝,对于集合类型的深拷贝,将刚刚介绍的第二个方法的第二个参数改为YES,就是深拷贝了。

     NSArray * deepCopyArray = [[NSArray alloc]initWithArray:someArray copyItems:YES];

通过将第二个参数设置为YES,我们实现了对数组的深拷贝。在深拷贝中,系统会向集合中的每一个元素对象发送一个copyWithZone:消息,该消息是来自于NSCopying协议,如果有对象没有实现该协议方法,那么就会导致崩溃,如果实现了该方法,那么会根据该方法的具体实现,实现具体的深拷贝。看一下下面这一段代码:

     NSString * str = @"2222";
     NSArray * someArray = @[str];
     NSArray * shallowCopyArray = [someArray copyWithZone:nil];
     NSArray * deepCopyArray = [[NSArray alloc]initWithArray:someArray copyItems:YES];
NSLog(@"someArray address: %p", someArray); NSLog(@"shallowCopyArray address: %p", shallowCopyArray); NSLog(@"deepCopyArray address: %p", deepCopyArray);
NSLog(@"someArray[0] address: %p", someArray[0]); NSLog(@"shallowCopyArray[0] address: %p", shallowCopyArray[0]); NSLog(@"deepCopyArray[0] address: %p", deepCopyArray[0]);

在运行打印之前,我们不妨猜猜看打印结果如何。鉴于之前介绍的浅拷贝的示例代码,我们有可能会认为上面代码中的shallowCopyArray和deepCopyArray是不一样的,这可以理解,因为deepCopyArray是通过alloc、init创建的,但最起码这两个数组的首元素地址是不一样的。然而,运行后发现打印结果为:

     someArray address: 0x618000018840
     shallowCopyArray address: 0x618000018840
     deepCopyArray address: 0x618000018850
     someArray[0] address: 0x108f61098
     shallowCopyArray[0] address: 0x108f61098
     deepCopyArray[0] address: 0x108f61098

打印的前三行结果与我们的猜测一致,只是后面三行打印的是相同的内存地址,让我们觉得有些意外,明明采用的是浅拷贝和深拷贝,结果却出现相同的内存地址,着实有些摸不着头脑。

实际上原因是这样的,前面说到,集合类型的深拷贝会对每一个元素调用copyWithZone:方法,这就意味着刚刚后面三行打印是取决于该方法,在深拷贝时对于第一个元素,调用了NSString的copyWithZone:方法,但由于NSString是不可变的,对于其深拷贝创建一个新内存是无意义的,所以我们可以猜测在NSString的copyWithZone:方法中也是直接返回self的,所以浅拷贝时是直接拷贝元素地址,而深拷贝是通过copyWithZone:方法来获取元素地址,两个结果是一样的。

如果将str的类型改动一下,将其改为NSMutableString类型:

     NSM utableString * str = [[NSMutableString alloc] initWithString:@"2222"];

就可以看到打印的元素地址发生了变化:

     address: 0x608000017160
     shallowCopyArray address: 0x608000017160
     deepCopyArray address: 0x608000017170
     someArray[0] address: 0x608000263300
     shallowCopyArray[0] address: 0x608000263300
     deepCopyArray[0] address: 0xa000000323232324

除了浅拷贝和深拷贝,还有一个完全深拷贝的概念。什么是完全深拷贝?就是对对象的每一层都是重新创建的实例变量,不存在指针拷贝。举个例子,在对数组的归档解档时,其实就是完全深拷贝。

     NSArray * trueDeepCopyArray = [NSKeyedUnarchiver
     unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

但完全深拷贝不仅是在归档解档中存在,在其他的情景下也有实现。

对于深拷贝来说,自身如何拷贝取决于实例方法中copyWithZone:如何实现,对于下一级一般还是采用浅拷贝的方式,这称为集合类型的单层深拷贝。

对于以上介绍的三种拷贝,可以总结为以下几个方面。

(1)浅拷贝:在浅拷贝操作时,对于被拷贝对象的每一层都是指针拷贝。

(2)深拷贝:在深拷贝操作时,对于被拷贝对象,至少有一层是深拷贝。

(3)完全拷贝:在完全拷贝操作时,对于被拷贝对象的每一层都是深拷贝。

介绍完了集合类型的拷贝问题,下面看一下非集合类型的拷贝问题。

首先看一下关于不可变对象,在调用copy方法和mutableCopy方法时有什么区别。

     NSString * string = @"123";
     NSString * stringCopy = [string copy];
     NSMutableString * stringMCopy = [string mutableCopy];
     NSLog(@"string address: %p", string);
     NSLog(@"stringCopy address: %p", stringCopy);
     NSLog(@"stringMCopy address: %p", stringMCopy);

我们采用的实验对象是NSString,NSString是不可变的对象,所以分别对其进行copy和mutableCopy时会看到控制台打印结果为:

     string address: 0x109816088
     stringCopy address: 0x109816088
     stringMCopy address: 0x60800007fa80

可以看到,对NSString进行copy只是对其指针的拷贝,而进行mutableCopy是真正重新创建一份新的NSString对象。之前介绍过,写定的字符串是存在于内存的常量区,因此可以看到两处地址的位置相差甚远。并且前面也说到,copy方法是与NSCoping协议相关的,而mutableCopy是与NSMutableCoping协议相关的,对于NSString这样不可变的系统类来说,copy后返回自身是比较好理解的,因为NSString是不可变的,对其copy也仍然是相同的内容,因此copy后仍然是同样的内存地址,而mutableCopy表明你或许真的需要一份新的可变对象,因此对NSString进行mutableCopy后会返回一个NSMutableString对象。

如何对于一个NSMutableString来调用copy和mutableCopy方法呢?

     NSMutableString * mString = [[NSMutableString alloc] initWithString:@"123"];
     NSString * copyMString = [mString copy];
     NSString * mCopyMString = [mString mutableCopy];
     NSLog(@"mString address: %p", mString);
     NSLog(@"copyMString address: %p", copyMString);
     NSLog(@"mCopyMString address: %p", mCopyMString);
     NSLog(@"copyMString is mutable? %@", [copyMString isKindOfClass:NSMutableString.class]?
     @"YES" : @"NO");
     NSLog(@"mCopyMString is mutable? %@", [mCopyMString isKindOfClass:NSMutableString.class]
     ? @"YES" : @"NO");

我们对NSMutableString对象分别进行了copy和mutableCopy,打印结果如下。

     mString address: 0x61000006e340
     copyMString address: 0xa000000003332313
     mCopyMString address: 0x61000006e600
     copyMString is mutable? NO
     mCopyMString is mutable? YES

从打印结果来看,对于NSMutableString来说,其copy的返回值是一个不可变字符串,而mutableCopy的返回值才是一个可变字符串,即使这三者是不一样的内存地址,即为三个对象。在Foundation和UIKit框架中,类似于NSString和NSMutableString这样的非集合对象且分为可变与不可变的并不多,但对于copy和mutableCopy方法的实现来说原理都是一样的,即对可变类型的对象copy结果为不可变,mutableCopy为可变。

结果也可以被看作:

  • [immutableObject copy]//浅复制
  • [immutableObject mutableCopy]//深复制
  • [mutableObject copy]//深复制
  • [mutableObject mutableCopy]//深复制

例如刚刚提到的NSString和NSMutableString,如果一个类分为可变和不可变两种时,应当同时实现NSCoping和NSMutableCoping协议。而对于我们日常开发中常见的类,却并没有可变和不可变之分,所以也就不用实现NSMutableCoping协议,如果需要实现浅拷贝和深拷贝,只要实现NSCoping协议即可,如果自定义的类需要实现浅拷贝,则在实现copyWithZone:方法时返回自身,而需要实现深拷贝时,在copyWithZone:方法中创建一个新实例对象返回即可。对于所谓的深拷贝,其实应当取决于每一层对象本身,如果需要达到完全深拷贝,则每一层对象都应当在copyWithZone:方法中创建好新的对象,如果每一层都为深拷贝做好准备,那么对最外层拷贝就是完全深拷贝。

最后一个关于拷贝的说明,是在类中声明属性时有个copy的修饰符,一般用于修饰字符串和block以及一些字典和数组。那么为何要声明成copy,而不声明成strong呢?这有什么区别吗?看以下的代码:

     @property(nonatomic, copy)NSMutableString * oneString;
self.oneString = [[NSMutableString alloc] initWithString:@"123"]; NSString * copyOneString = self.oneString; NSLog(@"copyOneString is mutable? % @", [copyOneString isKindOfClass: NSMutableString. class] ? @"YES" : @"NO");

其打印结果是NO,而当我们把copy换成strong时,则打印结果就为YES了。

这其实并不复杂,在使用copy时,会对属性的setter方法进行判断,对属性进行copy,根据属性是否为可变,则与上面说到的逻辑相同,如果为可变,则返回一个新的不可变对象,即为不可变字符串,而对于不可变则直接返回self,即为可变字符串。如果修饰符为strong,则是直接对其引用,并没有执行copy方法,所以区别在这里。如果这里的属性换成数组或者字典,则原理是一样的。只是block稍微有些不同,因为在MRC中,block需要显式地copy到堆中,而ARC中如果引用外部变量赋值时便会自动拷贝到内存中,所以block在ARC下使用copy和strong无异。对于NSString来说,作为不可变对象来说,修饰符为copy时,执行copy方法仍然返回自身,strong修饰也是返回自身,所以对于NSString这样的不可变对象来说,使用strong和copy也是一样的。

本节小结

(1)了解浅拷贝是指针拷贝,深拷贝是至少有一层对象拷贝,而完全拷贝是真正意义上的完全深拷贝;

(2)不可变对象的copy操作是指针拷贝,mutableCopy是对象拷贝,而可变对象因为实现了NSCoping协议,因此不管copy操作还是mutableCopy操作都是对象拷贝;

(3)了解属性修饰符copy和strong的区别以及对字符串、block和可变集合类型的影响。