从源码看iOS中的深拷贝和浅拷贝

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

深拷贝和浅拷贝的概念

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

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

对象的copy和mutableCopy方法

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

  • copy返回immutable对象
  • mutableCopy返回mutable对象
    下面对非集合对象和集合对象的copy和mutableCopy方法进行具体的阐述。

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

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

  • 首先来看immutable对象拷贝的例子:
    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对象拷贝的例子:
    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方法进行了深拷贝。

用代码表示就是:

    [immutableObject copy];//浅拷贝
    [immutableObject mutableCopy];//深拷贝
    [mutableObject copy];//深拷贝
    [mutableObject mutableCopy];//深拷贝

以上是通过打印内存地址得出的结论,下面我们通过查看源码来证实一下我们的结论。
在opensource.apple.com的git仓库中的runtime源码中有NSObject.mm这个文件,在这个文件中有copy和mutableCopy方法的实现:

- (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的方法:

- (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:方法:

-(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的例子:

    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的例子:

    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操作是深拷贝。

用代码表示就是:

    [immutableObject copy];//浅拷贝
    [immutableObject mutableCopy];//深拷贝
    [mutableObject copy];//深拷贝
    [mutableObject mutableCopy];//深拷贝

以上是通过打印内存地址得到的结论,下面我们通过源码来验证一下我们的结论。
NSArray.m中,我找到了copyWithZone:mutableCopyWithZone:方法。

- (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:的实现:

- (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,对其进行赋值:

    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修饰的话,打印结果就是:

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类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 本文为转载: 作者:zyydeveloper 链接:http://www.jianshu.com/p/5f776a...
    Buddha_like阅读 823评论 0 2
  • 前言 不敢说覆盖OC中所有copy的知识点,但最起码是目前最全的最新的一篇关于 copy的技术文档了。后续发现有新...
    zyydeveloper阅读 3,244评论 4 35
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,012评论 8 265
  • 深拷贝和浅拷贝这个问题在面试中常常被问到,而在实际开发中,只要稍有不慎,就会在这里出现问题。尤其对于初学者来说,我...
    西门淋雨阅读 1,734评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32