iOS开发 图文并茂理解深拷贝与浅拷贝

深拷贝和浅拷贝(Shallow copy 和 Deep copy)

一.概念定义

对象复制有两种:浅拷贝和深拷贝。 普通副本是浅拷贝,它生成一个新集合,该集合与原始对象共享对象的所有权。 深层副本从原始文件创建新对象,并将其添加到新集合中。

1.浅拷贝

浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。指针拷贝,即修改A,B也会跟这改变

iOS开发 图文并茂理解深拷贝与浅拷贝_第1张图片
浅拷贝.png

2.深拷贝

深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。分配了新内存,即A和B没有任何关系,改变A的值B也不会跟着变

iOS开发 图文并茂理解深拷贝与浅拷贝_第2张图片
深拷贝.png

3.总结:

深拷贝就是内容拷贝,浅拷贝就是指针拷贝。
本质区别在于:

  • 是否开启新的内存地址(内存地址是否一致)
  • 是否影响内存地址的引用计数

二.示例分析 -- 实践是检验真理的唯一标准

在iOS中深拷贝与浅拷贝要更加的复杂,涉及到容器与非容器、可变与不可变对象的copy与mutableCopy。下面用示例逐一分析:

2.1 非容器类对象的深拷贝、浅拷贝

这里指的是NSString,NSNumber等等一类的对象

2.1.1 不可变对象的copy与mutableCopy
// 非容器类 不可变对象
- (void)immutableObject {
    // 1.创建一个string字符串。
    NSString *string = @"Jason Mraz";
    NSString *stringB = string;
    NSString *stringCopy = [string copy];
    NSString *stringMutableCopy = [string mutableCopy];
    
    // 2.输出指针指向的内存地址。
    NSLog(@"string = %p",string);
    NSLog(@"stringB = %p",stringB);
    NSLog(@"stringCopy = %p",stringCopy);
    NSLog(@"stringMutableCopy = %p",stringMutableCopy);
}
//打印结果
string = 0x1085da078
stringB = 0x1085da078
stringCopy = 0x1085da078
stringMutableCopy = 0x60400005b4b0

可以看到,string、stringB和stringCopy内存地址一致,即指向的是同一块内存区域,进行了浅复制操作。而stringMutableCopy与另外三个变量内存地址不同,系统为其分配了新内存,即进行了深复制操作。
即在<非容器类 不可变对象> copy实现了浅拷贝,mutableCopy实现了深拷贝

2.1.2 可变对象的copy与mutableCopy
// 2.非容器类 可变对象
- (void)mutableObject {
    // 1.创建一个可变字符串。
    NSMutableString *mString = [NSMutableString stringWithString:@"Coca Cola"];
    NSString *mStringCopy = [mString copy];
    NSMutableString *mutablemString = [mString copy];
    NSMutableString *mStringMutableCopy = [mString mutableCopy];
    
    // 2.在可变字符串后添加字符串。
    [mString appendString:@"AA"];
    [mutablemString appendString:@"BB"];  // 运行时,这一行会报错。
    [mStringMutableCopy appendString:@"CC"];
    
    // 3.输出指针指向的内存地址。
    NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
}

在上面代码中,注释2部分为可变字符串拼接字符串,运行到为mutablemString拼接字符串这一行代码时,程序会崩溃,因为通过copy方法获得的字符串是不可变字符串。所以在运行前要注释掉这一行。

//打印结果 
mString = 0x608000050470,
mstringCopy = 0xa1b0ce20f6c30889,
mutablemString = 0xa1b0ce20f6c30889,
mStringMutableCopy = 0x608000050770

可以看到使用copy的对象内存地址是相同的,但所有数据都有原数据不同。所以,这里的copy和mutableCopy执行的均为深复制。

综合上面两个例子,我们可以得出这样结论:

  • 对不可变对象执行copy操作,是指针复制,执行mutableCopy操作是内容复制。
  • 对可变对象执行copy操作和mutableCopy操作都是内容复制。
  • copy返回不可变对象,mutableCopy返回可变对象

用代码表示如下:

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

2.2 容器类对象的深拷贝、浅拷贝

容器类对象指NSArray、NSDictionary等。容器类对象的深复制、浅复制如下图所示:


iOS开发 图文并茂理解深拷贝与浅拷贝_第3张图片
CopyingCollections.png

对于容器类,除了容器本身内存地址是否发生了变化外,也需要探讨的是复制后容器内元素的变化。

// 3.浅复制容器类对象
- (void)shallowCopyCollections {
    // 1.创建一个不可变数组,数组内元素为可变字符串。
    NSMutableString *red = [NSMutableString stringWithString:@"Red"];
    NSMutableString *green = [NSMutableString stringWithString:@"Green"];
    NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
    NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
    
    // 2.进行浅复制。
    NSArray *myArray2 = [myArray1 copy];
    NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
    NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1];
    
    // 3.修改myArray2的第一个元素。
    NSMutableString *tempString = myArray2.firstObject;
    [tempString appendString:@"Color"];  //第一个元素添加Color
    
    myMutableArray3[0] = @"no good";
    [myMutableArray3 addObject:@"11111"];
    [myMutableArray3 removeObjectAtIndex:1];
    
    // 4.输出四个数组内存地址及四个数组内容。
    NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
    NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}

运行demo,可以看到控制台输出如下:

myArray1 = 0x6000002402a0, 
myArray2 0x6000002402a0, 
myMutableArray3 0x600000240300, 
myArray4 0x600000240210
Contents of 
 myArray1 (
    RedColor,
    Green,
    Blue
), 
 myArray2 (
    RedColor,
    Green,
    Blue
), 
 myMutableArray3 (
    "no good",
    Blue,
    11111
), 
 myArray4 (
    RedColor,
    Green,
    Blue
)

可以看到myArray1和myArray2数组内存地址相同,myMutableArray3和myArray4与其它数组内存地址各不相同。这是因为mutableCopy的对象会被分配新的内存,alloc会为对象分配新的内存空间。

观察数组内元素,发现修改myArray2数组内第一个元素,四个数组第一个元素都发生了改变,所以这里对于数组内元素只进行了浅复制。但通过对myMutableArray3数组内元素的增删改查发现改变时并未影响其余数组,内存地址也不与其他数组一致,即对于容器类对象进行复制操作时,深拷贝也只是单层拷贝,可以把深拷贝理解为单层深拷贝,容器内元素还是浅拷贝(指针拷贝)

2.3 自定义对象的深拷贝、浅拷贝

自定义的类需要我们自己实现NSCopying、NSMutableCopying协议,这样才可以调用copy和mutableCopy方法。如果没有遵循,拷贝时会直接Crash。

@interface Person : NSObject 
@property (nonatomic,copy) NSString *name;

-(id)copyWithZone:(NSZone *)zone;
@end

NSCopying协议只有一个必须实现的copyWithZone: 方法。进入Person.m,实现copyWithZone: 方法。

-(id)copyWithZone:(NSZone *)zone{
    Person *copy = [[[self class] allocWithZone:zone] init];
    copy->_name = [_name copy];
    //copy.name = self.name;
    return copy;
}

测试代码如下:

// 4.自定义对象
- (void)personCopy {
    //model可变对象, 测试copy的意义:会生成新的地址,如果用=就是指向相同的地址
    Person *person = [Person new];
    person.name = @"qqq";
    Person *newPerson = [Person new];
    
    newPerson = [person copy];
    
    NSLog(@"person.name=%p",person.name);
    NSLog(@"newPerson.name=%p",newPerson.name);
    
    newPerson.name = @"PPP";
    
    NSLog(@"person.name=%p",person.name);
    NSLog(@"newPerson.name=%p",newPerson.name);
}

打印结果如下:

person.name=0x10921c288
newPerson.name=0x10921c288

person.name=0x10921c288
newPerson.name=0x10921c2e8

断点结果如下:


iOS开发 图文并茂理解深拷贝与浅拷贝_第4张图片
image.png

结合打印数据和断点图片可以看到当自定义类person使用copy方法后,person.name和newPerson.name依然是相同地址,即指针拷贝,但person和newPerson地址不同,结论与可变数组一致,都为单层深拷贝
至于为什么修改了修改newPerson.name的值person.name地址一样确没有跟着改变,后面会单独一个模块讲述

三.完全深拷贝的实现

我们之前测试看到即使是深拷贝也是单层深拷贝,下面我们介绍实现完全深拷贝的方法
数组存放model(最常见的模型)

①第一种方案
// 5.数组存放自定义对象
- (void)arrayPersonCopy {
    //copy两块内存地址不一样  深拷贝
    Person *person = [Person new];
    person.name = @"qqq";
    
    Person *newPerson = [Person new];
    newPerson.name = @"www";
    
    //单层深拷贝   内部自定义变量还是指向同一地址  会根据内容改变
    NSArray *listArr = @[person,newPerson];
    NSLog(@"listArr == %@",listArr);
    
    Person *eee = listArr[0];
    eee.name = @"111";
    
    //循环取出内部元素逐个复制
    NSArray *arr = [NSArray array];
    NSMutableArray *tempArr = [NSMutableArray array];
    for (Person *tempP in listArr) {
        Person *newPerson = [tempP copy];
        [tempArr addObject:newPerson];
    }
    arr = tempArr;
    
    Person *ddd = listArr[0];
    ddd.name = @"asjdkasjkdakjas";
    
    NSLog(@"%@",arr);
}

打印结果如下:

listArr == (
    "",
    ""
)

arr == (
    "",
    ""
)

可以看到数组本身和内部元素内存地址都不相同,实现了深拷贝,但还是需要注意层级问题,如果model里还有容器类对象依然存在无法完全复制的情况

②第二种方案
NSArray *arr = [[NSArray alloc] initWithArray:listArr copyItems:YES];

可以实现多层深拷贝,但必须保证容器的内部元素都可以实现了NSCopying协议,也就是实现了copyWithZone方法,不然会发生崩溃

③第三种方案
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: listArr]];

需要在model.m中实现归档编/解码方法

// 归档需要实现的方法
// 1.编码方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:@"PersonName"];
}

// 2.解码方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"PersonName"];
    }
    return self;
}

可以使用归档功能实现深复制,可以将对象归档到一个缓冲区,然后把它从缓冲区解归档,这样就实现了深复制。

四.修改指针指向

// 6.更改指针指向地址
- (void)pointToAnotherMemoryAddress {
    // 1.指针a、b同时指向字符串pro
    NSString *a = @"gaoyu";
    NSString *b = a;
    NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
    // 断点1位置
    
    // 2.指针a指向字符串pro648
    a = @"https://www.jianshu.com/u/9b0fa2e3ac62";
    NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
    // 断点2位置
}
断点1截图

指针a指向字符串gaoyu内存地址,b = a表示b是a的浅复制,指针b 也指向字符串gaoyu内存地址。也可以看到a和b的值是一样的


iOS开发 图文并茂理解深拷贝与浅拷贝_第5张图片
断点2截图

可以看到,a、b指针指向不同内存地址,a指向字符串https,b指向字符串gaoyu。

这是因为字符串是存在于常量区的内存数据,"="号的赋值已经更改了a元素的内存地址,但b还是指向之前的内存地址,所以a和b的内存地址已经不一样了,对应的值也不一样,自然不会因为a的改变而改变b(常量区每个字符串的内存地址都是固定的)

五.属性中copy与strong特性的区别

首先要搞清楚的就是对NSString类型的成员变量用copy修饰和用strong修饰的区别。如果使用了copy修饰符,那么在给成员变量赋值的时候就会对被赋值的对象进行copy操作,然后再赋值给成员变量。如果使用的是strong修饰符,则不会执行copy操作,直接将被赋值的变量赋值给成员变量。
假设有一个NSString类型的成员变量string,对其进行赋值:

NSString *testString = @"test";
self.string = testString;

如果该成员变量是用copy修饰的,则等价于:

self.string = [testString copy];

如果是用strong修饰的,则没有copy操作:

self.string = testString;   //指针拷贝

知道了使用copy和strong的区别后,我们再来分析为什么要使用copy修饰符。先看一段代码:

NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
    self.string = mutableString;
    NSLog(@"%@", self.string);
    [mutableString appendString:@"addstring"];
    NSLog(@"%@", self.string);

如果这里成员变量string是用strong修饰的话,打印结果就是:

test
testaddstring

很显然,当mutableString的值发生了改变后,string的值也随之发生改变,因为self.string = mutableString;这行代码实际上是执行了一次指针拷贝。string的值随mutableString的值的发生改变这显然不是我们想要的结果。
如果成员变量string是用copy修饰,打印结果就是:

test
test

这是因为使用copy修饰符后,self.string = mutableString;就等价于self.string = [mutableString copy];,也就是进行了一次深拷贝,所以mutableString的值再发生变化就不会影响到string的值。

结论:

NSString类型的成员变量使用copy修饰而不是strong修饰是因为有时候赋给该成员变量的值是NSMutableString类型的,这时候如果修饰符是strong,那成员变量的值就会随着被赋值对象的值的变化而变化。若是用copy修饰,则对NSMutableString类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。

当然,最后附送简单易懂对照表


iOS开发 图文并茂理解深拷贝与浅拷贝_第6张图片
葵花宝典
本文知识点汇总:

①深/浅拷贝的定义及理解
②copy和mutableCopy的区别与实现的拷贝效果
③完全深拷贝的实现方法
④NSString在赋值时修改了指针指向
⑤属性中copy与strong特性的区别

面试相关:

深/浅拷贝这块的知识也是一道经典面试题,如果被问到拷贝相关的知识,多半是在考验你对于内存分配,内存地址相关的内容,Good Luck

总结

No1:可变对象的copy和mutableCopy方法都是深拷贝(区别完全深拷贝与单层深拷贝) 。
No2:不可变对象的copy方法是浅拷贝,mutableCopy方法是深拷贝。
No3:copy方法返回的对象都是不可变对象。
No4:"="等号是浅拷贝,即指针拷贝
Demo下载地址:

https://github.com/gaoyuGood/copy-mutableCopy

参考文献:

深复制、浅复制、copy、mutableCopy
从源码看iOS中的深拷贝和浅拷贝
OC之数组的copy方法
iOS 图文并茂的带你了解深拷贝与浅拷贝

你可能感兴趣的:(iOS开发 图文并茂理解深拷贝与浅拷贝)