提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
在前面学习NSString的三种实现方式的过程中,遇到了深浅拷贝的问题,特此撰写博客总结知识
浅复制是指创建一个新对象,该对象与原始对象共享内部数据的引用。换句话说,浅复制只复制对象本身,而不复制对象所引用的数据。因此,原始对象和副本对象指向同一块内存,对其中一个对象的修改会影响另一个对象。
通俗的讲,浅复制就是仅仅创建一个存放与复制对象相同地址的指针变量,也可以称为指针拷贝
深复制是指创建一个新对象,并复制原始对象及其引用的所有数据。深复制会递归地复制对象的所有内容,包括对象所引用的其他对象。这样,原始对象和副本对象拥有独立的内存空间,彼此之间的修改互不影响。
通俗的讲,深复制就是创建一个与被复制的对象的值完全相同的对象,但是他们的地址是不同的,因此深复制也可以称为内容拷贝
我们以我们的代码来解释我们的拷贝原理:
NSString:
NSString *string1 = @"helloworld";
NSString *string2 = [string1 copy];
NSString *string3 = [string1 mutableCopy];
NSMutableString *string4 = [string1 copy];
NSMutableString *string5 = [string1 mutableCopy];
NSLog(@"string1 = %p",string1);
NSLog(@"string2 = %p",string2);
NSLog(@"string3 = %p",string3);
NSLog(@"string4 = %p",string4);
NSLog(@"string5 = %p",string5);
NSMutableString:
NSMutableString *string1 = [NSMutableString stringWithString:@"helloworld"];
NSString *string2 = [string1 copy];
NSString *string3 = [string1 mutableCopy];
NSMutableString *string4 = [string1 copy];
NSMutableString *string5 = [string1 mutableCopy];
NSLog(@"string1 = %p",string1);
NSLog(@"string2 = %p",string2);
NSLog(@"string3 = %p",string3);
NSLog(@"string4 = %p",string4);
NSLog(@"string5 = %p",string5);
结论:
从我们上述的结果可以看出:
对于不可变非容器类对象(NSString),它的copy是浅拷贝,mutablecopy是深拷贝
对于可变容器类对象(NSMutableString),它的copy与mutablecopy都是深拷贝
引用:
这里引用一下其他博主的结论,可以知道我们复制后的对象的类型只与深还是浅复制有关,而与原来的数据类型无关
NSArray:
NSArray *array = [NSArray arrayWithObjects:@"a",@"b",@"c", nil];
NSArray *arrayCopy = [array copy];
NSArray *arrayMutableCopy = [array mutableCopy];
NSArray *tempArray = array[0];
NSArray *tempArrayCopy = arrayCopy[0];
NSArray *tempArrayMutableCopy = arrayMutableCopy[0];
NSLog(@"不可变数组 copy 和 mutableCopy的区别");
NSLog(@" 对象地址 对象指针地址 firstObject地址 firstObject指针地址");
NSLog(@" array: %p , %p , %p , %p", array, &array, tempArray, &tempArray);
NSLog(@" copy: %p , %p , %p , %p", arrayCopy, &arrayCopy, tempArrayCopy, &tempArrayCopy);
NSLog(@"mutalbeCopy: %p , %p , %p , %p", arrayMutableCopy, &arrayMutableCopy, tempArrayMutableCopy, &tempArrayMutableCopy);
NSMutableArray:
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"a",@"b",@"c", nil];
NSMutableArray *mutableArrayCopy = [mutableArray copy];
NSMutableArray *mutableArrayMutableCopy = [mutableArray mutableCopy];
NSMutableArray *tempMutableArray = mutableArray.firstObject;
NSMutableArray *tempMutableArrayCopy = mutableArrayCopy.firstObject;
NSMutableArray *tempMutableArrayMutableCopy = mutableArrayMutableCopy.firstObject;
NSLog(@"可变数组 copy 和 mutableCopy的区别");
NSLog(@" 对象地址 对象指针地址 firstObject地址 firstObject指针地址");
NSLog(@"mutableArray: %p , %p , %p , %p", mutableArray, &mutableArray, tempMutableArray, &tempMutableArray);
NSLog(@" copy: %p , %p , %p , %p", mutableArrayCopy, &mutableArrayCopy, tempMutableArrayCopy, &tempMutableArrayCopy);
NSLog(@" mutalbeCopy: %p , %p , %p , %p", mutableArrayMutableCopy, &mutableArrayMutableCopy, tempMutableArrayMutableCopy,
&tempMutableArrayMutableCopy);
编译结果:
结论:
1、copy和mutableCopy之后对象地址的变化与非集合对象变化相同。不可变类copy是浅拷贝,mutablecopy是深拷贝,可变类全都是深拷贝
2、无论是copy还是mutableCopy,内部元素的地址都没有变化,只是指针地址发生了变化。
由此我们总结一下,我们的容器类与非容器类对深浅拷贝不尽相同:
对于容器而言,元素对象始终是指针复制。 也就是说数组中的元素的拷贝始终是浅拷贝。
这里值得注意的是,即使是可变数组的深拷贝,里面的元素的地址还是原来元素的地址,只是在我们的内存中开辟了一块新空间存储新对象,也可以称其为浅层的深拷贝
Tip:
这里解释一下为何数组中的元素始终是浅拷贝:
在这个过程中,我们知道数组的元素实际上是对象的引用。所以,当我们访问数组的第一个元素时,不管是从原始数组 array、拷贝的不可变数组 arrayCopy 还是拷贝的可变数组 arrayMutableCopy 中访问,都会得到同一个字符串对象的引用。
因此,这三个变量 tempArray、tempArrayCopy 和 tempArrayMutableCopy 的地址是相同的,因为它们指向的是同一个字符串对象。这里的地址是字符串对象的内存地址。
我们再仔细来理解一下这句话:数组的元素实际上是对象的引用
这句话的意思是,在数组中存储的元素实际上是指向对象的引用,而不是对象本身。当我们将一个对象添加到数组中时,实际上是将该对象的引用存储在数组的对应位置上。
我们通过以下示例来理解这句话
NSString *string1 = @"Hello";
NSString *string2 = @"World";
NSArray *array = @[string1, string2];
在这个例子中,array 是一个包含两个元素的数组。每个元素都是一个 NSString 对象。但是,数组中存储的并不是字符串对象本身,而是指向这些字符串对象的引用。
这意味着当我们修改原始的字符串对象时,数组中对应的元素也会随之改变,因为它们引用同一个对象。例如:
string1 = @"Hello World";
NSLog(@"%@", array[0]); // 输出结果为 "Hello World"
在这个示例中,我们修改了 string1 的值,将其指向了新的字符串对象 “Hello World”。由于数组 array 中的第一个元素实际上是指向 string1 的引用,所以输出结果变为了 “Hello World”。
这就是所说的数组的元素实际上是对象的引用,而不是对象本身。这种引用机制使得多个变量可以引用同一个对象,从而实现对象之间的共享和交互。同时,需要注意当对象被修改时,所有引用该对象的地方都会受到影响。
我们的自定义类中没有实现我们的NSCopy等协议,我们需要手动实现它
因此可以知道,我们自定义类的copy与mutablecopy都是深拷贝
我们分strong与copy对其属性进行讨论
strong:
编译结果:
因为我们创建的string是可变的,我们对其t使用appendstring方法时,用strong修饰的可以被修改,用copy修饰的不能被修改
因此我们可以知道,copy修饰的属性在进行赋值时实际上是进行了深拷贝,创建了一个新的对象,所以没有被修改
strong修饰的属性在复制时则是进行了浅拷贝,所以能被修改
使用copy修饰不会发生改变的具体原因是:copy修饰下,我们创建新的字符串时属于深拷贝,会拷贝出一个新的对象,当我们改变t的值的时候,改变的只是string这个对象自己,t这个新对象并不会受到影响,所以值并不会改变
使用strong修饰会发生改变的具体原因是:strong修饰下,我们的t会持有原来的对象,使原来的对象引用计数+1,属于浅拷贝,这时候我们进行操作时,sting和t的值都会发送改变
所以我们得出结论:
strong的复制是多个指针指向同一个地址(浅拷贝),而copy的复制是每次会在内存中复制一份新的对象(深拷贝),其中对象的值和原对象相同而地址不同,一般来说对于不可变对象我们使用copy修饰,而可变的对象使用strong修饰
注意的点:在程序中,只有用点语法才能使让属性关键字发挥作用
我们在上文讲述容器类对象的深浅拷贝的时候,提到了我们的容器类对象的深拷贝实际上是一种浅层深拷贝。
它的意思是我们的深拷贝仅仅创建了一个新的容器类对象,里面的元素仅仅只是进行了指针拷贝。
通俗的讲,就是我们容器类对象的深拷贝仅仅只是创建的新容器对象地址不同,里面的元素地址仍然是相同的
为此我们可以试着去实现完全深拷贝,也就是让其里面元素也进行内容复制
下面介绍实现深拷贝的两种方法:
1、copyItems
我们先来给出它的定义:
copyItems: 方法是 NSArray 和 NSMutableArray 的一个方法,用于创建一个新的数组,并对数组中的元素进行拷贝。该方法会遍历原始数组的每个元素,并对每个元素执行 copy 操作,然后将拷贝后的元素添加到新的数组中。
可以看到定义中的关键是遍历数组中的每个元素对其进行copy,这里需要注意的是如果集合中的对象有自定义类,那么使用这个方法前自定义类必须实现NSCopying协议,否则会报错
接下来我们给出代码:
Test *t = [[Test alloc] init];
t.name = @"牢大";
NSArray *p =[NSArray arrayWithObject:t];
NSArray *tt = [[NSArray alloc] initWithArray:p copyItems:YES];
NSLog(@"%@", p);
NSLog(@"%@", tt);
NSLog(@"%@", p[0]);
NSLog(@"%@", tt[0]);
这里可以看到我们创建了一个p数组,里面存放的是自定义类对象,我们创建一个新数组tt对其使用copyItems:方法进行初始化,可以看到tt与p的地址不同,同时他们内部元素的地址也不同,这就说明:如果容器类对象中的元素是非容器类,那么我们可以用copyItems:方法对其进行完全深复制
这里为什么笔者要强调是容器类对象中的元素是非容器类,因为这个方法实际上有着一定的局限性:
当容器类对象中的元素是容器类对象时,那么就无法实现完全深拷贝
我们给出例子:
Test *t = [[Test alloc] init];
t.name = @"牢大";
NSArray *p =[NSArray arrayWithObject:t];
NSArray *k = [NSArray arrayWithObject:p];
NSArray *tt = [[NSArray alloc] initWithArray:k copyItems:YES];
NSLog(@"%@", k);
NSLog(@"%@", tt);
NSLog(@"%@", p[0]);
NSLog(@"%@", k[0]);
NSLog(@"%@", tt[0]);
这里我们可以看到,当我们容器类对象中的元素是容器类对象时,我们对其使用copyItems:方法,两个对象的地址相同,仅仅实现了浅拷贝。
因此我们可以得出结论:
使用 copyItems:
方法进行深复制仅适用于数组的直接元素,即数组中的对象。如果数组的元素还是容器对象(如数组、字典等),那么仅对容器对象本身进行拷贝(而且还是浅拷贝),并不会对容器内部的元素进行深度拷贝。这意味着容器对象内部的元素仍然是共享的,修改其中一个数组的元素可能会影响到另一个数组。
2、递归复制属性
那么为了解决这个问题,我们可以对我们重写的copyWithZone:方法中的属性进行递归复制
由此我们实现了完全的深复制。
3、解档与归档
这个方法笔者并没有尝试,这里给出讲解:
归档和解档(Archiving and Unarchiving):
使用归档和解档的方式可以将对象及其属性序列化为二进制数据,然后通过解档重新创建一个全新的对象。
这种方式要求被拷贝的对象及其属性必须遵循 NSCoding 协议,实现 initWithCoder: 和 encodeWithCoder: 方法。
实现深拷贝的步骤如下:
使用 NSKeyedArchiver 的 archivedDataWithRootObject: 方法将原始对象归档为二进制数据。
使用 NSKeyedUnarchiver 的 unarchiveObjectWithData: 方法将二进制数据解档为全新的对象。
示例代码:
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:originalObject]; id deepCopyObject = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
1、浅复制是指针拷贝,深复制是内容拷贝
2、非容器类对象的深浅复制:
不可变对象的copy是浅复制,mutablecopy是深复制
可变对象的copy与mutablecopy都是深复制
3、容器类对象的深浅复制
不可变对象的copy是浅复制,mutablecopy是深复制
可变对象的copy与mutablecopy都是深复制
容器中的元素的复制全都是浅复制
需要注意的一点是对原对象添加元素时,浅复制的对象的内容也会随之改变,而深复制不会
4、自定义类对象的深浅复制:
必须实现NSCopying协议,且全都是深拷贝,但是是浅层的深拷贝
5、属性关键词的深浅复制:
strong修饰的属性一般是可变对象,copy修饰的属性一般用于不可变对象,strong实际上是对对象的属性进行了一次浅拷贝,对象的内容修改时strong修饰的属性也会随之变化,而copy则相当于进行了一次深拷贝,内容不会随之变化
6、容器类对象的完全深拷贝