通过源码分析iOS中的深拷贝与浅拷贝

时间:2022-09-19 16:48:18

前言

关于ios中对象的深拷贝浅拷贝的文章有很多,但是大部分都是基于打印内存地址来推导结果,这篇文章是从源码的角度来分析深拷贝和浅拷贝。

深拷贝和浅拷贝的概念

拷贝的方式有两种:深拷贝和浅拷贝。

  • 浅拷贝又叫指针拷贝,比如说有一个指针,这个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址,那么此时对这个字符串进行指针拷贝的意思就是又创建了一个指针变量,这个指针变量的值是这个字符串的地址,也就是这个字符串的引用计数+1。
  • 深拷贝又叫内容拷贝,比如有一个指针,这个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址值,那么此时对这个字符串进行内容拷贝,就会创建一个新的指针,在一个新的地址区域创建一个字符串,这个字符串的值和原字符串的值相同,新的指针指向这个新创建的字符串。这时原字符串的引用计数没有+1。

浅拷贝就是拷贝后,并没有进行真正的复制,而是复制的对象和原对象都指向同一个地址

深拷贝是真正的复制了一份,复制的对象指向了新的地址

通过源码分析iOS中的深拷贝与浅拷贝

从上图可以看出,浅拷贝a指针改变了所指向的内容b指针也指向被修改后的内容。如果有些地方用到b指针,不希望在a指向的内容发生变化时也跟着变化,则需要用到深拷贝。

通俗理解为:浅拷贝好比你的影子,你死了,影子也没了;深拷贝好比克隆人,你死了,它还在。

对象的copy和mutablecopy方法

不管是集合对象还是非集合对象,接收到copy和mutablecopy消息时,都遵循以下准则:

  • copy返回immutable对象
  • mutablecopy返回mutable对象

下面对非集合对象和集合对象的copy和mutablecopy方法进行具体的阐述。

1.非集合类对象的copy和mutablecopy方法

非集合类对象指的是nsstring,nsnumber...这些类。下面的例子以nsstring类为例。

首先来看immutable对象拷贝的例子:

?
1
2
3
4
5
nsstring *string = @"test";
nsstring *copystring = [string copy];
nsmutablestring *mutablecopystring = [string mutablecopy];
 
nslog(@"%p \n %p \n %p \n", string, copystring, mutablecopystring);

打印结果:

0x101545068
0x101545068
0x60000024e940

通过打印结果我们可以看出来,copystring和string的地址值一样,而mutablecopystring和string的地址值不一样,这就说明imutable对象的copy方法进行了浅拷贝,mutablecopy方法进行了深拷贝。

再来看看mutable对象拷贝的例子:

?
1
2
3
4
5
nsmutablestring *string = [[nsmutablestring alloc] initwithstring:@"test"];
nsstring *copystring = [string copy];
nsmutablestring *mutablecopystring = [string mutablecopy];
 
nslog(@"%p \n%p \n%p \n", string, copystring, mutablecopystring);

打印结果:

0x600000240e40
0xa000000747365744
0x6000002411a0

通过打印结果可以看出来,copystring和string的内存地址不同,mutablecopystring和string的内存地址也不同。这说明mutable对象的copy方法和mutablecopy方法都进行了深拷贝。

总结起来就是:

immutable对象的copy方法进行了浅拷贝
immutable对象的mutablecopy方法进行了深拷贝
mutable对象的copy方法进行了深拷贝
mutable对象的mutablecopy方法进行了深拷贝。

用代码表示就是:

?
1
2
3
4
[immutableobject copy];//浅拷贝
[immutableobject mutablecopy];//深拷贝
[mutableobject copy];//深拷贝
[mutableobject mutablecopy];//深拷贝

以上是通过打印内存地址得出的结论,下面我们通过查看源码来证实一下我们的结论。

在opensource.apple.com的git仓库中的runtime源码中有nsobject.mm这个文件,在这个文件中有copy和mutablecopy方法的实现:

?
1
2
3
4
5
6
7
- (id)copy {
 return [(id)self copywithzone:nil];
}
 
- (id)mutablecopy {
 return [(id)self mutablecopywithzone:nil];
}

我们发现copy和mutablecopy方法只是简单的调用了copywithzone:和mutablecopywithzone:两个方法。然后我在searchcode.com中找到了nsstring和nsmutablestring的source code。

nsstring.m中,找到了关于copy的方法:

?
1
2
3
4
5
6
7
8
9
- (id)copywithzone:(nszone *)zone {
 if (nsstringclass == nil)
 nsstringclass = [nsstring class];
 return retain(self);
}
 
- (id)mutablecopywithzone:(nszone*)zone {
 return [[nsmutablestring allocwithzone:zone] initwithstring:self];
}

通过这个源码我们知道了,对于nsstring对象,调用copy方法就是调用了copywithzone:方法。而copywithzone:方法并没有创建新的对象,而是使指针持有了原来的nsstring对象,所以nsstring的copy方法是浅拷贝。

而调用mutablecopy方法就是调用了mutablecopywithzone:方法。从mutablecopywithzone:的实现我们可以看到,这个方法是创建了一个新的可变的字符串对象。因此nsstring的mutablecopy方法是深拷贝。

nsmutablestring.m中,只找到了copy和copywithzone:方法,并没有找到mutablecopywithzone:方法:

?
1
2
3
4
5
6
7
-(id)copy {
 return [[nsstring alloc] initwithstring:self];
}
 
-(id)copywithzone:(nszone*)zone {
 return [[nsstring allocwithzone:zone] initwithstring:self];
}

对nsmutablestring对象调用copy方法会调用这里的copywithzone:方法的实现,我们可以看到这里创建了一个新的不可变的字符串。所以对nsmutablestring对象执行copy方法是深拷贝。

由于在nsmutablestring中没有实现mutablecopywithzone:方法,所以会调用父类的mutablecopywithzone:方法,也就是nsstring类的mutablecopywithzone:方法,而我们知道,nsstring类的mutablecopywithzone:方法会创建一个新的可变字符串。所以对nsmutablestring对象执行mutablecopy方法是深拷贝。

2.集合对象的copy和mutablecopy

集合对象指的是nsarray,nsdictionary,nsset等之类的对象。下面以nsarray为例看看immutable对象使用copy和mutablecopy的例子:

?
1
2
3
4
5
nsarray *array = @[@"1", @"2", @"3"];
nsarray *copyarray = [array copy];
nsmutablearray *mutablecopyarray = [array mutablecopy];
 
nslog(@"%p\n%p\n%p", array, copyarray, mutablecopyarray);

打印结果:

0x60400025bed0
0x60400025bed0
0x60400025c2f0

通过打印结果可以看出来,copyarray的地址和array的地址是一样的,说明对array进行copy是进行浅拷贝。而

mutablecopyarray的地址和array的地址是不一样的,说明对array进行mutablecopy是进行了深拷贝。

再来看mutable对象执行copy和mutablecopy的例子:

?
1
2
3
4
5
nsmutablearray *array = [[nsmutablearray alloc] initwitharray:@[@"1", @"2", @"3"]];
nsarray *copyarray = [array copy];
nsmutablearray *mutablecopyarray = [array mutablecopy];
 
nslog(@"%p\n%p\n%p", array, copyarray, mutablecopyarray);

打印结果:

0x604000447440
0x604000447050
0x604000447080

通过打印结果可以看出,copyarray和mutablecopyarray的地址都和array的地址不同,这说明对可变数组进行copy和mutablecopy操作都进行了深拷贝。

因此得出结论:

在集合类对象中,对immutable对象进行copy操作是浅拷贝,进行mutablecopy操作是深拷贝。对mutable对象进行copy操作是深拷贝,进行mutablecopy操作是深拷贝。

用代码表示就是:

?
1
2
3
4
[immutableobject copy];//浅拷贝
[immutableobject mutablecopy];//深拷贝
[mutableobject copy];//深拷贝
[mutableobject mutablecopy];//深拷贝

以上是通过打印内存地址得到的结论,下面我们通过源码来验证一下我们的结论。

nsarray.m中,我找到了copywithzone:和mutablecopywithzone:方法。

?
1
2
3
4
5
6
7
8
9
10
11
- (id)copywithzone:(nszone *)zone
{
 return retain(self);
}
 
- (id)mutablecopywithzone:(nszone*)zone
{
 if (nsmutablearrayclass == nil)
 nsmutablearrayclass = [nsmutablearray class];
 return [[nsmutablearrayclass alloc] initwitharray:self];
}

当调用copy方法时,实际上是执行了这里的copywithzone:方法,在这个方法里面并没有创建新的对象,而只是持有了旧的对象,因此,对于不可变的数组对象,执行copy操作是浅拷贝。

当调用mutablecopy方法时,实际上是执行了这里的mutablecopywithzone:方法,在这个方法里面,利用原来的数组对象,创建了一个新的可变数组对象,因此对于不可变的数组对象,执行mutablecopy操作是深拷贝。

nsarray.m这个文件的第825行是nsmutablearray的实现。在第875行找到了copywithzone:的实现,没有找到mutablecopywithzone:的实现:

?
1
2
3
4
5
6
- (id)copywithzone:(nszone*)zone
{
 if (nsarrayclass == nil)
 nsarrayclass = [nsarray class];
 return [[nsarrayclass alloc] initwitharray:self copyitems:yes];
}

当调用copy方法时,实际是调用了这里的copywithzone:方法,在这个方法的实现里,是利用原来的可变数组创建了一个新的不可变数组,因此对可变数组执行copy操作是深拷贝。

当调用mutablecopy时,由于nsmutablearray本身没有实现mutablecopywithzone:方法,所以会调用父类也就是nsarray类的实现,而通过上面我们也能看到nsarray的实现:利用原数组创建了一个新的可变数组,因此,对可变数组进行mutablecopy操作是深拷贝。

回答经典面试题

面试题:为什么nsstring类型的成员变量的修饰属性用copy而不是strong呢?

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

假设有一个nsstring类型的成员变量string,对其进行赋值:

?
1
2
nsstring *teststring = @"test";
self.string = teststring;

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

?
1
self.string = [teststring copy];

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

?
1
self.string = teststring;

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

?
1
2
3
4
5
nsmutablestring *mutablestring = [[nsmutablestring alloc] initwithstring:@"test"];
self.string = mutablestring;
nslog(@"%@", self.string);
[mutablestring appendstring:@"addstring"];
nslog(@"%@", self.string);

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

2018-09-04 10:50:16.909998+0800 copytest[2856:78171] test
2018-09-04 10:50:16.910128+0800 copytest[2856:78171] testaddstring

很显然,当mutablestring的值发生了改变后,string的值也随之发生改变,因为self.string = mutablestring;这行代码实际上是执行了一次指针拷贝。string的值随mutablestring的值的发生改变这显然不是我们想要的结果。

如果成员变量string是用copy修饰,打印结果就是:

2018-09-04 10:58:07.705373+0800 copytest[3024:84066] test
2018-09-04 10:58:07.705496+0800 copytest[3024:84066] test

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

回答面试题:

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://www.jianshu.com/p/1ffb40a23c1d