Objective-C 编程:iOS 中的深拷贝与浅拷贝

导读

最近经常在关注的公众号或者技术网站看到关于 iOS 深拷贝与浅拷贝的话题。看到一篇技术文章,点击收藏或者只是读过一遍,并不会让你深刻领悟其中的技术原理,只有照着源码敲一遍,运行调试一通,你才会深谙其道,说不定还会有新的发现。所以,这篇文章就是阅读了前人的文章,自己又敲了一遍,加深一下印象,然后还有一点点新的感悟和理解。

一、概念与总结

1. 浅拷贝

浅拷贝就是对内存地址的复制,让【目标对象指针】和【源对象指针】指向同一块内存空间,当内存被销毁时,指向这块内存的所有指针需要重新定义才可以使用,要不然会成为野指针。

浅拷贝就是拷贝【指向原来对象的】指针,使原对象的引用计数+1,可以理解为创建了一个指向原对象的新指针,并没有创建一个全新的对象。

2. 深拷贝

深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。

深拷贝就是创建【值与原对象相同,但是内存地址不同的】新对象,创建后的对象和原对象没有任何关系。

总结

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

  • 是否影响内存地址的引用计数;
  • 是否创建新的内存地址。

二、示例分析

在 iOS 中,深拷贝与浅拷贝要更加的复杂,涉及到容器与非容器、可变与不可变对象的 copymutableCopy,还有 @property 属性中的内存管理。

1. 非集合对象的 copy 与 mutableCopy

1.1 不可变对象 NSString → NSMutableString

NSString *string1 = @"Hello";

// 引用计数+1。
NSString *string2 = string1;

// NSString 的 copy 是浅拷贝,且 copy 返回的对象是不可变对象。
NSMutableString *string3 = [string1 copy];
// copy 返回的是不可变对象,string3 不能被修改,因此程序会 crash。
// [string3 appendString:@" World"];

// NSString 的 mutableCopy 是深拷贝。
NSMutableString *string4 = [string1 mutableCopy];
[string4 appendString:@" World"];

NSLog(@"string1:<%p : %@> \n",string1,string1);
NSLog(@"string2:<%p : %@> \n",string2,string2);
NSLog(@"string3:<%p : %@> \n",string3,string3);
NSLog(@"string4:<%p : %@> \n",string4,string4);

打印结果:

string1:<0x100001040 : Hello>
string2:<0x100001040 : Hello>
string3:<0x100001040 : Hello>
string4:<0x1003004a0 : Hello World>

结论:

  • NSStringcopy 是浅拷贝,且 copy 返回的对象是不可变对象;
  • NSStringmutableCopy 是深拷贝。

1.2 可变对象 NSMutableString → NSMutableString

NSMutableString *mStr1 = [NSMutableString stringWithString:@"Hello"];

// 引用计数+1。
NSMutableString *mStr2 = mStr1;

// NSMutableString 的 copy 是深拷贝,且 copy 返回的对象是不可变对象。
NSMutableString *mStr3 = [mStr1 copy];
// copy 返回的是不可变对象,mStr3 不能被修改,因此程序会 crash。
// [mStr3 appendString:@" World"];

// NSMutableString 的 mutableCopy 是深拷贝。
NSMutableString *mStr4 = [mStr1 mutableCopy];
[mStr4 appendString:@" World"];

NSLog(@"mStr1:<%p : %@> \n",mStr1,mStr1);
NSLog(@"mStr2:<%p : %@> \n",mStr2,mStr2);
NSLog(@"mStr3:<%p : %@> \n",mStr3,mStr3);
NSLog(@"mStr4:<%p : %@> \n",mStr4,mStr4);

打印结果:

mStr1:<0x100200f10 : Hello>
mStr2:<0x100200f10 : Hello>
mStr3:<0x6f6c6c654855 : Hello>
mStr4:<0x100200ba0 : Hello World>

结论:

  • NSMutableStringcopy 是深拷贝,且 copy 返回的对象是不可变对象;
  • NSMutableStringmutableCopy 是深拷贝;
  • NSMutableStringcopymutableCopy 都是深拷贝;
  • ⚠️ 虽然 NSMutableStringcopy 是深拷贝,但 copy 返回的对象是不可变对象,也就是说:如果对「用 copy 声明过的 NSMutableString 类型的」值进行修改会导致程序 crash。这下明白 @property 属性声明 NSMutableString 不使用 copy 的原因了吧:
@property (nonatomic, copy) NSString *string;
@property (nonatomic, strong) NSMutableString *mutableString;

1.3 可变对象 NSMutableString → NSString

💡 引用这个示例的原因在后面【项目应用】中会讲到。

NSMutableString *mStr1 = [NSMutableString stringWithString:@"Hello"];

// 引用计数+1。
NSString *str2 = mStr1;

// NSMutableString 的 copy 是深拷贝。
NSString *str3 = [mStr1 copy];

// NSMutableString 的 mutableCopy 是深拷贝。
NSString *str4 = [mStr1 mutableCopy];

NSLog(@"mStr1:<%p : %@> \n",mStr1,mStr1);
NSLog(@"str2 :<%p : %@> \n",str2,str2);
NSLog(@"str3 :<%p : %@> \n",str3,str3);
NSLog(@"str4 :<%p : %@> \n",str4,str4);

打印结果:

mStr1:<0x10030a240 : Hello>
str2 :<0x10030a240 : Hello>
str3 :<0x6f6c6c654855 : Hello>
str4 :<0x100309fb0 : Hello>

2.集合对象的 copy 与 mutableCopy

2.1 不可变对象 NSArray

NSArray *array1 = @[@"Hello",@"World"];

// NSArray 的 copy 是浅拷贝,且 copy 返回的对象是不可变对象。
NSArray *array2 = [array1 copy];

// NSArray 的 mutableCopy 是深拷贝。
NSArray *array3 = [array1 mutableCopy];

NSLog(@"array1:<%p : %@> \n",array1,array1);
NSLog(@"array2:<%p : %@> \n",array2,array2);
NSLog(@"array3:<%p : %@> \n",array3,array3);

打印结果:

array1:<0x1003065d0 : (
    Hello,
    World
)>
array2:<0x1003065d0 : (
    Hello,
    World
)>
array3:<0x100306bd0 : (
    Hello,
    World
)>

结论:

  • NSArraycopy 是浅拷贝,且 copy 返回的对象是不可变对象;
  • NSArraymutableCopy 是深拷贝。

2.2 可变对象 NSMutableArray

NSMutableArray *mArray1 = [NSMutableArray arrayWithObjects:@"Hello",@"World", nil];

// NSMutableArray 的 copy 是深拷贝,且 copy 返回的对象是不可变对象。
NSMutableArray *mArray2 = [mArray1 copy];

// NSMutableArray 的 mutableCopy 是深拷贝。
NSMutableArray *mArray3 = [mArray1 mutableCopy];

NSLog(@"mArray1:<%p : %@> \n",mArray1,mArray1);
NSLog(@"mArray2:<%p : %@> \n",mArray2,mArray2);
NSLog(@"mArray3:<%p : %@> \n",mArray3,mArray3);

打印结果:

mArray1:<0x100203da0 : (
    Hello,
    World
)>
mArray2:<0x100204030 : (
    Hello,
    World
)>
mArray3:<0x1002040b0 : (
    Hello,
    World
)>

结论:

  • NSMutableArraycopy 是深拷贝,且 copy 返回的对象是不可变对象;
  • NSMutableArraymutableCopy 是深拷贝。
  • 重要:同样地,@property 属性声明:
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mutableArray;

2.3 单层深拷贝

特别注意的是:对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,只能算是单层深复制,即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然指向原数组元素值,并没有另外复制一份),这就叫做 单层深复制

示例代码:

NSMutableString *mString1 = [NSMutableString stringWithString:@"Hello"];
NSMutableString *mString2 = [NSMutableString stringWithString:@"World"];
NSMutableArray *mArray1 = [NSMutableArray arrayWithObjects:mString1,mString2, nil];

NSMutableArray *mArray2 = [mArray1 copy];
NSMutableArray *mArray3 = [mArray1 mutableCopy];

NSLog(@"mArray1:<%p : %@> \n",mArray1,mArray1);
NSLog(@"mArray2:<%p : %@> \n",mArray2,mArray2);
NSLog(@"mArray3:<%p : %@> \n",mArray3,mArray3);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray1[0],mArray1[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray2[0],mArray2[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray3[0],mArray3[1]);

NSLog(@"-----------修改原值后---------");
[mString1 appendString:@"ios"];

NSLog(@"mArray1:<%p : %@> \n",mArray1,mArray1);
NSLog(@"mArray2:<%p : %@> \n",mArray2,mArray2);
NSLog(@"mArray3:<%p : %@> \n",mArray3,mArray3);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray1[0],mArray1[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray2[0],mArray2[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray3[0],mArray3[1]);

打印结果:

mArray1:<0x100306bc0 : (
    Hello,
    World
)>
mArray2:<0x100306d10 : (
    Hello,
    World
)>
mArray3:<0x100306dc0 : (
    Hello,
    World
)>
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 

 -----------修改原值后---------
mArray1:<0x100306bc0 : (
    Helloios,
    World
)>
mArray2:<0x100306d10 : (
    Helloios,
    World
)>
mArray3:<0x100306dc0 : (
    Helloios,
    World
)>
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 
数组元素地址:<value1:0x100306620>,<value2:0x100306a00> 

结论:

  • mArray1、mArray2、mArray3 的地址都不一样,开辟了新的内存空间,证明 NSMutableArraycopymutableCopy 的确都是深拷贝。
  • 所有数组中值的地址是一样的,并没有开辟新的内存,而是指针指向了原地址。因此说它是单层深拷贝
  • 修改原数组 mArray1 中的值后,mArray2 和 mArray3 数组中的值也跟着发生了变化。也证明了它是单层深拷贝

2.4 集合对象的完全深拷贝

2.2 中提到的 NSMutableArray 的拷贝只是单层深拷贝,下面的方法可以实现完全深拷贝:

(1)归档解档法

NSMutableString *mString1 = [NSMutableString stringWithString:@"Hello"];
NSMutableString *mString2 = [NSMutableString stringWithString:@"World"];
NSMutableArray *mArray1 = [NSMutableArray arrayWithObjects:mString1,mString2, nil];

// 归档
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mArray1];
// 解档
NSArray *array = [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data error:nil];

NSLog(@"mArray1:<%p : %@> \n",mArray1,mArray1);
NSLog(@"array:<%p : %@> \n",array,array);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray1[0],mArray1[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",array[0],array[1]);

打印结果:

mArray1:<0x100303270 : (
    Hello,
    World
)>
array:<0x100305400 : (
    Hello,
    World
)>
数组元素地址:<value1:0x100302f30>,<value2:0x1003030d0>
数组元素地址:<value1:0x1003053c0>,<value2:0x100305990>

结论:

  • 两个数组的内存地址不同,数组中元素的内存地址也不同,实现了完全深拷贝。

(2) - (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;

NSMutableString *mString1 = [NSMutableString stringWithString:@"Hello"];
NSMutableString *mString2 = [NSMutableString stringWithString:@"World"];
NSMutableArray *mArray1 = [NSMutableArray arrayWithObjects:mString1,mString2, nil];

NSArray *array = [[NSArray alloc] initWithArray:mArray1 copyItems:YES];

NSLog(@"mArray1:<%p : %@> \n",mArray1,mArray1);
NSLog(@"array:<%p : %@> \n",array,array);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",mArray1[0],mArray1[1]);
NSLog(@"数组元素地址:<value1:%p>,<value2:%p> \n",array[0],array[1]);

打印结果:

mArray1:<0x100403270 : (
    Hello,
    World
)>
array:<0x1004034d0 : (
    Hello,
    World
)>
数组元素地址:<value1:0x100402f30>,<value2:0x1004030d0>
数组元素地址:<value1:0x6f6c6c654855>,<value2:0x646c726f5755>

结论

  • 两个数组的内存地址不同,数组中元素的内存地址也不同,实现了完全深拷贝。(那之前的归档解档法是不是有点旁门左道,或者说有点杂技代码(hack)的嫌疑🤔)

三、准则

  1. 不可变对象的 copy 方法是浅拷贝,mutableCopy 方法是深拷贝。
  2. 可变对象的 copymutableCopy 方法都是深拷贝(区别完全深拷贝与单层深拷贝) 。
  3. 无论是可变对象还是不可变对象, copy 方法返回的对象都是不可变对象。

最佳实践

自定义指定初始化方法

推荐:

- (instancetype)initWithUsername:(NSString *)username
                        password:(NSString *)password {
    if (self) {
        _username = [username copy];
        _password = [password copy];
    }
    return self;
}

反对:

- (instancetype)initWithUsername:(NSString *)username
                        password:(NSString *)password {
    if (self) {
        _username = username;
        _password = password;
    }
    return self;
}

原因:

如果传入的 NSString 对象类型是 NSMutableString 类型,一旦对原数据修改,会引起不必要的麻烦。如果使用 copy 方法,不管它是 NSString 对象的copy浅拷贝还是 NSMutableString 对象的copy深拷贝,得到的总是不可变对象。

示例代码:

// 1.3 扩展
NSMutableString *mStr1 = [NSMutableString stringWithString:@"Hello"];

// 引用计数+1
NSString *str2 = mStr1;
NSString *str3 = [mStr1 copy];

NSLog(@"mStr1:<%p : %@> \n",mStr1,mStr1);
NSLog(@"str2 :<%p : %@> \n",str2,str2);
NSLog(@"str3 :<%p : %@> \n",str3,str3);

NSLog(@"-----------修改原值后---------");
[mStr1 appendString:@" World"];

NSLog(@"mStr1:<%p : %@> \n",mStr1,mStr1);
NSLog(@"str2 :<%p : %@> \n",str2,str2);
NSLog(@"str3 :<%p : %@> \n",str3,str3);

打印结果:

mStr1:<0x100304b00 : Hello>
str2 :<0x100304b00 : Hello>
str3 :<0x6f6c6c654855 : Hello>
-----------修改原值后---------
mStr1:<0x100304b00 : Hello World>
str2 :<0x100304b00 : Hello World>
str3 :<0x6f6c6c654855 : Hello>

setter方法

@property (nonatomic, copy) NSString *str;

- (void)setStr:(NSString *)str {
    // _str = str; 不要这样写
    _str = [str copy];
}

参考

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

推荐阅读更多精彩内容