[TOC]
引言
根據(jù)拷貝內(nèi)容的不同,分為深淺拷貝
- 深拷貝:內(nèi)容拷貝,且將指針指向新的內(nèi)容
- 淺拷貝:只是簡(jiǎn)單的指針賦值
蘋(píng)果為什么這么設(shè)計(jì)呢?總結(jié)起來(lái)很簡(jiǎn)單:即安全又省內(nèi)存。但是要理解或者避免踩一些坑,還需要看下面的介紹
內(nèi)存
不得不先說(shuō)到內(nèi)存,又不得不說(shuō)內(nèi)存分區(qū):程序底層——程序如何在RAM ROM運(yùn)行,內(nèi)存分配與分區(qū)
看下面圖片:

obj1是定義在函數(shù)外部的全局變量,處于全局區(qū);obj2是定義在函數(shù)內(nèi)的局部變量,處于棧區(qū)。它們都指向了處于堆區(qū)的對(duì)象。
obj1與obj2是指針,它們指向的對(duì)象是內(nèi)容,那么現(xiàn)在再看深淺拷貝的現(xiàn)象,或者說(shuō)執(zhí)行的結(jié)果:淺拷貝只是多個(gè)指針指向同一對(duì)象內(nèi)容,深拷貝就是每個(gè)指針都指向了一個(gè)對(duì)象內(nèi)容,互不影響。
自定義對(duì)象需要自己實(shí)現(xiàn)NSCoping協(xié)議,一般情況下,自定義對(duì)象都是可變對(duì)象,本節(jié)討論的也都是針對(duì)系統(tǒng)對(duì)象
指針也是會(huì)存在堆區(qū)的,比如在block里面我們知道,如果指針使用了__block修飾,那么指針會(huì)存放在堆區(qū)。
返回值的一些基本規(guī)則
無(wú)論是集合對(duì)象還是非集合對(duì)象,在收到copy和mutableCopy消息時(shí),都遵守以下規(guī)則:
- 1 copy返回immutable對(duì)象;
- 2 mutableCopy返回mutable對(duì)象;
那么很簡(jiǎn)單,可變與不可變對(duì)象的轉(zhuǎn)變:
- 不可變對(duì)象→可變對(duì)象的轉(zhuǎn)換:不可變對(duì)象.mutableCopy。
- 可變->不可變:可變對(duì)象.copy;
集合拷貝
系統(tǒng)提供的集合類型,比如字典、數(shù)組、NSSet等集合類型內(nèi)存基本都是如下結(jié)構(gòu):集合內(nèi)存結(jié)構(gòu)圖

我們可以上面代碼(代碼處于方法內(nèi))做個(gè)分析,加深對(duì)內(nèi)存的理解。@"123"、@"456"是const屬性,因此處于常量區(qū),指針str1、str2、arr局部變量指針處于棧區(qū),@[]數(shù)組內(nèi)容存放位置處于堆區(qū),數(shù)組里面的內(nèi)容存放的是指針str1與str2,當(dāng)然處于堆區(qū)
其實(shí)arr = @[str1,str2]相當(dāng)于[arr addObject:str1];[arr addObject:str2];,數(shù)組里面有兩個(gè)強(qiáng)指針指向了對(duì)象@"123"與@"456"。
圖中只是字符串是常量所以在常量區(qū),如果他們是NSDate、UIView等等則會(huì)處于堆區(qū)
下面的分析也是基于三種程度的拷貝,記為CopyLevel,拷貝層次,簡(jiǎn)寫(xiě)CL1、CL2、CL3
- CL1:arr數(shù)組指針,如果只發(fā)生這層拷貝,則和非集合對(duì)象一樣,是淺拷貝
- CL2:arr數(shù)組指針指向的的內(nèi)容,即存儲(chǔ)的對(duì)象指針。發(fā)生本層拷貝,從非集合角度來(lái)說(shuō)已經(jīng)發(fā)生了內(nèi)容拷貝,即深拷貝。但從集合角度來(lái)說(shuō),還是淺拷貝。
- CL3:arr數(shù)組里面存儲(chǔ)的指針指向的內(nèi)容,如果發(fā)生本層拷貝,可以叫做集合的單層深拷貝。
毫無(wú)疑問(wèn),CL1是肯定會(huì)進(jìn)行的。重點(diǎn)就在于CL2于CL3.
不可變集合的copy與mutableCopy
下面代碼,不可變集合arrM1的copy與mutableCopy。arrM2:mutableCopy,arr:copy

- 根據(jù)第一行打印結(jié)果:arrM2和arr都進(jìn)行CL1拷貝
- 第二行打印結(jié)果:arrM2與arrM1結(jié)果不同,說(shuō)明進(jìn)行了數(shù)組拷貝;arr與arrM1結(jié)果相同,說(shuō)明沒(méi)有,進(jìn)行數(shù)組拷貝
- 第三行打印結(jié)果:都相同,說(shuō)明指向的內(nèi)容沒(méi)有發(fā)生拷貝
可變集合的copy與mutableCopy
下面代碼,可變集合arrM1的copy與mutableCopy。arrM2:mutableCopy,arr:copy

- 根據(jù)第一行打印結(jié)果:arrM2和arr都進(jìn)行CL1拷貝
- 第二行打印結(jié)果:結(jié)果均不同,說(shuō)明都進(jìn)行了數(shù)組拷貝;
- 第三行打印結(jié)果:都相同,說(shuō)明指向的內(nèi)容沒(méi)有發(fā)生拷貝
一般結(jié)論
我們知道,對(duì)于非集合對(duì)象,有如下結(jié)論:
// 不可變,線程安全
[immutableObject copy] // 淺復(fù)制
[immutableObject mutableCopy] // 深復(fù)制,對(duì)于集合則是只拷?貝數(shù)組的內(nèi)容,數(shù)組的內(nèi)容是指針,而指針的內(nèi)容不會(huì)被拷?
// 可變對(duì)象,線程不安全
[mutableObject copy] //深復(fù)制,對(duì)于集合則是只拷?貝數(shù)組的內(nèi)容,數(shù)組的內(nèi)容是指針,而指針的內(nèi)容不會(huì)被拷?
[mutableObject mutableCopy] //深復(fù)制,對(duì)于集合則是只拷?貝數(shù)組的內(nèi)容,數(shù)組的內(nèi)容是指針,而指針的內(nèi)容不會(huì)被拷?
集合的單層深拷貝,CL3層的拷貝(one-level-deep copy)
我們需要使用- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;方法,且flag為YES。

可以看到,三行打印結(jié)果都不一樣,即發(fā)生了CL3層的拷貝。
此方法執(zhí)行后,arrM1集合里的每個(gè)對(duì)象都會(huì)收到 copyWithZone: 消息。如果集合里的對(duì)象遵循 NSCopying 協(xié)議,那么對(duì)象就會(huì)被深拷貝到新的集合,如果沒(méi)有遵循就直接崩潰了。
等一等,好像有另一個(gè)問(wèn)題:此方法只是會(huì)給集合的每個(gè)對(duì)象發(fā)送copyWithZone:方法,那么對(duì)于不可變對(duì)象,copyWithZone:的執(zhí)行還是淺拷貝。讀者大概也注意到了,圖中示例代碼,arrM1數(shù)組存的也是可變對(duì)象dict1,所以有CL3層的拷貝。那如果arrM1存的不是可變對(duì)象呢?結(jié)果就是沒(méi)有CL3層的拷貝,大家可以用代碼測(cè)試下!
為啥叫單層深復(fù)制呢? 因?yàn)樗唤oarrM1數(shù)組存的對(duì)象發(fā)送了copyWithZone:方法,而沒(méi)有對(duì)dict1發(fā)送copyWithZone:方法,dict1也是集合,它里面也存放著對(duì)象呢。。。即集合里面存放的集合。。。好繞,哈哈
另外,除了此方法,集合的解檔歸檔,也是可以實(shí)現(xiàn)單層深拷貝的。
繞的東西就到這里,下面看些感興趣的東西:
一些坑
- Mutable變copy的坑
有一點(diǎn)需要注意了:copy返回值為不可變對(duì)象,如果使用可變對(duì)象的接口就會(huì)crash。例如:
- (void)arrMCopyTest {
NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
NSMutableArray *arr = [arrM copy];
// 下面代碼崩潰
[arr addObject:@"789"];
}
[arrM copy];返回的是不可變類型,即NSArray,向一個(gè)NSArray對(duì)象發(fā)送addObject消息當(dāng)然方法找不到崩潰。
另一個(gè)問(wèn)題,arr是NSMutableArray類型,它指向父類NSArray編譯器為什么不報(bào)錯(cuò)呢?copy返回的是id類型,編譯器不會(huì)對(duì)id(俗稱萬(wàn)能指針)進(jìn)行類型檢查,所以會(huì)經(jīng)??吹酵扑]使用instancetype,而不是id
下面的類似錯(cuò)誤就很常見(jiàn)了:
@property (nonatomic, copy) NSMutableArray *arr;
- (void)arrMCopyTest {
NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
self.arr = arrM;
// 下面代碼崩潰
[self.arr addObject:@"789"];
}
因?yàn)閟elf.arr為copy修飾,那么self.arr = arrM就相當(dāng)于_arr = [arrM copy]
- 屬性指定為copy,卻沒(méi)有被copy
@property (nonatomic, copy) NSString *str;
- (void)viewDidLoad {
NSMutableString *str = [NSMutableString stringWithFormat:@"123"];
// self.str = str;
_str = str;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[str appendString:@"456"];
NSLog(@"change");
});
}
這里在block里面對(duì)str進(jìn)行操作,居然沒(méi)有對(duì)它進(jìn)行__block修飾?。?!感興趣可以看看這篇博客:iOS中block的使用、實(shí)現(xiàn)底層、循環(huán)引用、存儲(chǔ)位置
打印結(jié)果:
2017-07-23 00:33:06.344 CopyTest[95611:31912803] 123
2017-07-23 00:33:07.518 CopyTest[95611:31912803] change
2017-07-23 00:33:08.636 CopyTest[95611:31912803] 123456
都是123456,self.str被意外改變了,如果將代碼_str = str;-->self.str = str;值就不會(huì)改變了。因?yàn)橄喈?dāng)于_str = [str copy];。
所以建議除了在初始化和釋放時(shí)(init、dealloc方法中,懶加載還是使用self.),蘋(píng)果推薦我們使用_下劃線的方式直接訪問(wèn)變量,其它地方盡量使用self.來(lái)訪問(wèn)。另外我們還經(jīng)常getter或者setter方法里面做一些自定義操作,如果_方式則這些自定義操作就不會(huì)被執(zhí)行。而且在block里面使用_方式訪問(wèn)變量會(huì)更隱蔽的引起循環(huán)引用的問(wèn)題!
- setter方法
@property (nonatomic, copy) NSString *str;
- (void)setStr:(NSString *)str {
// _str = str; 不要這樣寫(xiě)
_str = [str copy];
}
講了這些,大家會(huì)不會(huì)猛然想到
@property (nonatomic, weak) id delegate;
_delegate = obj;
這樣會(huì)不會(huì)造成_delegate為指向的對(duì)象引用計(jì)數(shù)為0時(shí),系統(tǒng)還會(huì)不會(huì)將_delegate置為nil?答案是,您多慮了,會(huì)的。這和copy不一樣。為啥不一樣?牽涉到runtime哈希表什么的就不在展開(kāi)了。。。