深拷貝和淺拷貝(Shallow copy 和 Deep copy)
一.概念定義
對(duì)象復(fù)制有兩種:淺拷貝和深拷貝。 普通副本是淺拷貝梧喷,它生成一個(gè)新集合概龄,該集合與原始對(duì)象共享對(duì)象的所有權(quán)昌腰。 深層副本從原始文件創(chuàng)建新對(duì)象,并將其添加到新集合中坏挠。
1.淺拷貝
淺拷貝就是對(duì)內(nèi)存地址的復(fù)制,讓目標(biāo)對(duì)象指針和源對(duì)象指向同一片內(nèi)存空間邪乍,當(dāng)內(nèi)存銷(xiāo)毀的時(shí)候降狠,指向這片內(nèi)存的幾個(gè)指針需要重新定義才可以使用,要不然會(huì)成為野指針庇楞。指針拷貝榜配,即修改A,B也會(huì)跟這改變
2.深拷貝
深拷貝是指拷貝對(duì)象的具體內(nèi)容吕晌,而內(nèi)存地址是自主分配的蛋褥,拷貝結(jié)束之后,兩個(gè)對(duì)象雖然存的值是相同的睛驳,但是內(nèi)存地址不一樣烙心,兩個(gè)對(duì)象也互不影響膜廊,互不干涉。分配了新內(nèi)存弃理,即A和B沒(méi)有任何關(guān)系溃论,改變A的值B也不會(huì)跟著變
3.總結(jié):
深拷貝就是內(nèi)容拷貝,淺拷貝就是指針拷貝痘昌。
本質(zhì)區(qū)別在于:
- 是否開(kāi)啟新的內(nèi)存地址(內(nèi)存地址是否一致)
- 是否影響內(nèi)存地址的引用計(jì)數(shù)
二.示例分析 -- 實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)
在iOS中深拷貝與淺拷貝要更加的復(fù)雜钥勋,涉及到容器與非容器、可變與不可變對(duì)象的copy與mutableCopy辆苔。下面用示例逐一分析:
2.1 非容器類(lèi)對(duì)象的深拷貝算灸、淺拷貝
這里指的是NSString,NSNumber等等一類(lèi)的對(duì)象
2.1.1 不可變對(duì)象的copy與mutableCopy
// 非容器類(lèi) 不可變對(duì)象
- (void)immutableObject {
// 1.創(chuàng)建一個(gè)string字符串驻啤。
NSString *string = @"Jason Mraz";
NSString *stringB = string;
NSString *stringCopy = [string copy];
NSString *stringMutableCopy = [string mutableCopy];
// 2.輸出指針指向的內(nèi)存地址菲驴。
NSLog(@"string = %p",string);
NSLog(@"stringB = %p",stringB);
NSLog(@"stringCopy = %p",stringCopy);
NSLog(@"stringMutableCopy = %p",stringMutableCopy);
}
//打印結(jié)果
string = 0x1085da078
stringB = 0x1085da078
stringCopy = 0x1085da078
stringMutableCopy = 0x60400005b4b0
可以看到,string骑冗、stringB和stringCopy內(nèi)存地址一致赊瞬,即指向的是同一塊內(nèi)存區(qū)域,進(jìn)行了淺復(fù)制操作贼涩。而stringMutableCopy與另外三個(gè)變量?jī)?nèi)存地址不同巧涧,系統(tǒng)為其分配了新內(nèi)存,即進(jìn)行了深復(fù)制操作遥倦。
即在<非容器類(lèi) 不可變對(duì)象> copy實(shí)現(xiàn)了淺拷貝谤绳,mutableCopy實(shí)現(xiàn)了深拷貝
2.1.2 可變對(duì)象的copy與mutableCopy
// 2.非容器類(lèi) 可變對(duì)象
- (void)mutableObject {
// 1.創(chuàng)建一個(gè)可變字符串。
NSMutableString *mString = [NSMutableString stringWithString:@"Coca Cola"];
NSString *mStringCopy = [mString copy];
NSMutableString *mutablemString = [mString copy];
NSMutableString *mStringMutableCopy = [mString mutableCopy];
// 2.在可變字符串后添加字符串袒哥。
[mString appendString:@"AA"];
[mutablemString appendString:@"BB"]; // 運(yùn)行時(shí)缩筛,這一行會(huì)報(bào)錯(cuò)。
[mStringMutableCopy appendString:@"CC"];
// 3.輸出指針指向的內(nèi)存地址堡称。
NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
}
在上面代碼中瞎抛,注釋2部分為可變字符串拼接字符串,運(yùn)行到為mutablemString拼接字符串這一行代碼時(shí)却紧,程序會(huì)崩潰婿失,因?yàn)橥ㄟ^(guò)copy方法獲得的字符串是不可變字符串。所以在運(yùn)行前要注釋掉這一行啄寡。
//打印結(jié)果
mString = 0x608000050470,
mstringCopy = 0xa1b0ce20f6c30889,
mutablemString = 0xa1b0ce20f6c30889,
mStringMutableCopy = 0x608000050770
可以看到使用copy的對(duì)象內(nèi)存地址是相同的豪硅,但所有數(shù)據(jù)都有原數(shù)據(jù)不同。所以挺物,這里的copy和mutableCopy執(zhí)行的均為深復(fù)制懒浮。
綜合上面兩個(gè)例子,我們可以得出這樣結(jié)論:
- 對(duì)不可變對(duì)象執(zhí)行copy操作,是指針復(fù)制砚著,執(zhí)行mutableCopy操作是內(nèi)容復(fù)制次伶。
- 對(duì)可變對(duì)象執(zhí)行copy操作和mutableCopy操作都是內(nèi)容復(fù)制。
- copy返回不可變對(duì)象稽穆,mutableCopy返回可變對(duì)象
用代碼表示如下:
[immutableObject copy]; // 淺復(fù)制
[immutableObject mutableCopy]; // 深復(fù)制
[mutableObject copy]; // 深復(fù)制
[mutableObject mutableCopy]; // 深復(fù)制
2.2 容器類(lèi)對(duì)象的深拷貝冠王、淺拷貝
容器類(lèi)對(duì)象指NSArray、NSDictionary等舌镶。容器類(lèi)對(duì)象的深復(fù)制柱彻、淺復(fù)制如下圖所示:
對(duì)于容器類(lèi),除了容器本身內(nèi)存地址是否發(fā)生了變化外餐胀,也需要探討的是復(fù)制后容器內(nèi)元素的變化哟楷。
// 3.淺復(fù)制容器類(lèi)對(duì)象
- (void)shallowCopyCollections {
// 1.創(chuàng)建一個(gè)不可變數(shù)組,數(shù)組內(nèi)元素為可變字符串否灾。
NSMutableString *red = [NSMutableString stringWithString:@"Red"];
NSMutableString *green = [NSMutableString stringWithString:@"Green"];
NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
// 2.進(jìn)行淺復(fù)制卖擅。
NSArray *myArray2 = [myArray1 copy];
NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1];
// 3.修改myArray2的第一個(gè)元素。
NSMutableString *tempString = myArray2.firstObject;
[tempString appendString:@"Color"]; //第一個(gè)元素添加Color
myMutableArray3[0] = @"no good";
[myMutableArray3 addObject:@"11111"];
[myMutableArray3 removeObjectAtIndex:1];
// 4.輸出四個(gè)數(shù)組內(nèi)存地址及四個(gè)數(shù)組內(nèi)容墨技。
NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}
運(yùn)行demo惩阶,可以看到控制臺(tái)輸出如下:
myArray1 = 0x6000002402a0,
myArray2 0x6000002402a0,
myMutableArray3 0x600000240300,
myArray4 0x600000240210
Contents of
myArray1 (
RedColor,
Green,
Blue
),
myArray2 (
RedColor,
Green,
Blue
),
myMutableArray3 (
"no good",
Blue,
11111
),
myArray4 (
RedColor,
Green,
Blue
)
可以看到myArray1和myArray2數(shù)組內(nèi)存地址相同,myMutableArray3和myArray4與其它數(shù)組內(nèi)存地址各不相同扣汪。這是因?yàn)閙utableCopy的對(duì)象會(huì)被分配新的內(nèi)存断楷,alloc會(huì)為對(duì)象分配新的內(nèi)存空間。
觀察數(shù)組內(nèi)元素,發(fā)現(xiàn)修改myArray2數(shù)組內(nèi)第一個(gè)元素,四個(gè)數(shù)組第一個(gè)元素都發(fā)生了改變澳叉,所以這里對(duì)于數(shù)組內(nèi)元素只進(jìn)行了淺復(fù)制污抬。但通過(guò)對(duì)myMutableArray3數(shù)組內(nèi)元素的增刪改查發(fā)現(xiàn)改變時(shí)并未影響其余數(shù)組,內(nèi)存地址也不與其他數(shù)組一致侥蒙,即對(duì)于容器類(lèi)對(duì)象進(jìn)行復(fù)制操作時(shí)暗膜,深拷貝也只是單層拷貝,可以把深拷貝理解為單層深拷貝鞭衩,容器內(nèi)元素還是淺拷貝(指針拷貝)
2.3 自定義對(duì)象的深拷貝学搜、淺拷貝
自定義的類(lèi)需要我們自己實(shí)現(xiàn)NSCopying、NSMutableCopying協(xié)議论衍,這樣才可以調(diào)用copy和mutableCopy方法瑞佩。如果沒(méi)有遵循,拷貝時(shí)會(huì)直接Crash坯台。
@interface Person : NSObject <NSCopying>
@property (nonatomic,copy) NSString *name;
-(id)copyWithZone:(NSZone *)zone;
@end
NSCopying協(xié)議只有一個(gè)必須實(shí)現(xiàn)的copyWithZone: 方法炬丸。進(jìn)入Person.m,實(shí)現(xiàn)copyWithZone: 方法。
-(id)copyWithZone:(NSZone *)zone{
Person *copy = [[[self class] allocWithZone:zone] init];
copy->_name = [_name copy];
//copy.name = self.name;
return copy;
}
測(cè)試代碼如下:
// 4.自定義對(duì)象
- (void)personCopy {
//model可變對(duì)象, 測(cè)試copy的意義:會(huì)生成新的地址,如果用=就是指向相同的地址
Person *person = [Person new];
person.name = @"qqq";
Person *newPerson = [Person new];
newPerson = [person copy];
NSLog(@"person.name=%p",person.name);
NSLog(@"newPerson.name=%p",newPerson.name);
newPerson.name = @"PPP";
NSLog(@"person.name=%p",person.name);
NSLog(@"newPerson.name=%p",newPerson.name);
}
打印結(jié)果如下:
person.name=0x10921c288
newPerson.name=0x10921c288
person.name=0x10921c288
newPerson.name=0x10921c2e8
斷點(diǎn)結(jié)果如下:
結(jié)合打印數(shù)據(jù)和斷點(diǎn)圖片可以看到當(dāng)自定義類(lèi)person使用copy方法后稠炬,person.name和newPerson.name依然是相同地址焕阿,即指針拷貝,但person和newPerson地址不同首启,結(jié)論與可變數(shù)組一致暮屡,都為單層深拷貝
至于為什么修改了修改newPerson.name的值person.name地址一樣確沒(méi)有跟著改變,后面會(huì)單獨(dú)一個(gè)模塊講述
三.完全深拷貝的實(shí)現(xiàn)
我們之前測(cè)試看到即使是深拷貝也是單層深拷貝毅桃,下面我們介紹實(shí)現(xiàn)完全深拷貝的方法
數(shù)組存放model(最常見(jiàn)的模型)
①第一種方案
// 5.數(shù)組存放自定義對(duì)象
- (void)arrayPersonCopy {
//copy兩塊內(nèi)存地址不一樣 深拷貝
Person *person = [Person new];
person.name = @"qqq";
Person *newPerson = [Person new];
newPerson.name = @"www";
//單層深拷貝 內(nèi)部自定義變量還是指向同一地址 會(huì)根據(jù)內(nèi)容改變
NSArray *listArr = @[person,newPerson];
NSLog(@"listArr == %@",listArr);
Person *eee = listArr[0];
eee.name = @"111";
//循環(huán)取出內(nèi)部元素逐個(gè)復(fù)制
NSArray *arr = [NSArray array];
NSMutableArray *tempArr = [NSMutableArray array];
for (Person *tempP in listArr) {
Person *newPerson = [tempP copy];
[tempArr addObject:newPerson];
}
arr = tempArr;
Person *ddd = listArr[0];
ddd.name = @"asjdkasjkdakjas";
NSLog(@"%@",arr);
}
打印結(jié)果如下:
listArr == (
"<Person: 0x608000002f50>",
"<Person: 0x608000002f00>"
)
arr == (
"<Person: 0x608000002f60>",
"<Person: 0x608000002f90>"
)
可以看到數(shù)組本身和內(nèi)部元素內(nèi)存地址都不相同褒纲,實(shí)現(xiàn)了深拷貝,但還是需要注意層級(jí)問(wèn)題疾嗅,如果model里還有容器類(lèi)對(duì)象依然存在無(wú)法完全復(fù)制的情況
②第二種方案
NSArray *arr = [[NSArray alloc] initWithArray:listArr copyItems:YES];
可以實(shí)現(xiàn)多層深拷貝外厂,但必須保證容器的內(nèi)部元素都可以實(shí)現(xiàn)了NSCopying協(xié)議,也就是實(shí)現(xiàn)了copyWithZone方法代承,不然會(huì)發(fā)生崩潰
③第三種方案
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: listArr]];
需要在model.m中實(shí)現(xiàn)歸檔編/解碼方法
// 歸檔需要實(shí)現(xiàn)的方法
// 1.編碼方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"PersonName"];
}
// 2.解碼方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"PersonName"];
}
return self;
}
可以使用歸檔功能實(shí)現(xiàn)深復(fù)制汁蝶,可以將對(duì)象歸檔到一個(gè)緩沖區(qū),然后把它從緩沖區(qū)解歸檔论悴,這樣就實(shí)現(xiàn)了深復(fù)制掖棉。
四.修改指針指向
// 6.更改指針指向地址
- (void)pointToAnotherMemoryAddress {
// 1.指針a、b同時(shí)指向字符串pro
NSString *a = @"gaoyu";
NSString *b = a;
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 斷點(diǎn)1位置
// 2.指針a指向字符串pro648
a = @"http://www.reibang.com/u/9b0fa2e3ac62";
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 斷點(diǎn)2位置
}
指針a指向字符串gaoyu內(nèi)存地址膀估,b = a表示b是a的淺復(fù)制幔亥,指針b 也指向字符串gaoyu內(nèi)存地址。也可以看到a和b的值是一樣的
可以看到察纯,a帕棉、b指針指向不同內(nèi)存地址,a指向字符串https饼记,b指向字符串gaoyu香伴。
這是因?yàn)樽址谴嬖谟诔A繀^(qū)的內(nèi)存數(shù)據(jù),"="號(hào)的賦值已經(jīng)更改了a元素的內(nèi)存地址具则,但b還是指向之前的內(nèi)存地址即纲,所以a和b的內(nèi)存地址已經(jīng)不一樣了,對(duì)應(yīng)的值也不一樣博肋,自然不會(huì)因?yàn)閍的改變而改變b(常量區(qū)每個(gè)字符串的內(nèi)存地址都是固定的)
五.屬性中copy與strong特性的區(qū)別
首先要搞清楚的就是對(duì)NSString類(lèi)型的成員變量用copy修飾和用strong修飾的區(qū)別低斋。如果使用了copy修飾符,那么在給成員變量賦值的時(shí)候就會(huì)對(duì)被賦值的對(duì)象進(jìn)行copy操作匪凡,然后再賦值給成員變量膊畴。如果使用的是strong修飾符,則不會(huì)執(zhí)行copy操作病游,直接將被賦值的變量賦值給成員變量巴比。
假設(shè)有一個(gè)NSString類(lèi)型的成員變量string,對(duì)其進(jìn)行賦值:
NSString *testString = @"test";
self.string = testString;
如果該成員變量是用copy修飾的,則等價(jià)于:
self.string = [testString copy];
如果是用strong修飾的轻绞,則沒(méi)有copy操作:
self.string = testString; //指針拷貝
知道了使用copy和strong的區(qū)別后采记,我們?cè)賮?lái)分析為什么要使用copy修飾符。先看一段代碼:
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
self.string = mutableString;
NSLog(@"%@", self.string);
[mutableString appendString:@"addstring"];
NSLog(@"%@", self.string);
如果這里成員變量string是用strong修飾的話政勃,打印結(jié)果就是:
test
testaddstring
很顯然唧龄,當(dāng)mutableString的值發(fā)生了改變后,string的值也隨之發(fā)生改變奸远,因?yàn)?code>self.string = mutableString;這行代碼實(shí)際上是執(zhí)行了一次指針拷貝既棺。string的值隨mutableString的值的發(fā)生改變這顯然不是我們想要的結(jié)果。
如果成員變量string是用copy修飾懒叛,打印結(jié)果就是:
test
test
這是因?yàn)槭褂胏opy修飾符后丸冕,self.string = mutableString;
就等價(jià)于self.string = [mutableString copy];
,也就是進(jìn)行了一次深拷貝薛窥,所以mutableString的值再發(fā)生變化就不會(huì)影響到string的值胖烛。
結(jié)論:
NSString類(lèi)型的成員變量使用copy修飾而不是strong修飾是因?yàn)橛袝r(shí)候賦給該成員變量的值是NSMutableString類(lèi)型的,這時(shí)候如果修飾符是strong诅迷,那成員變量的值就會(huì)隨著被賦值對(duì)象的值的變化而變化佩番。若是用copy修飾,則對(duì)NSMutableString類(lèi)型的值進(jìn)行了一次深拷貝罢杉,成員變量的值就不會(huì)隨著被賦值對(duì)象的值的改變而改變趟畏。
當(dāng)然,最后附送簡(jiǎn)單易懂對(duì)照表
本文知識(shí)點(diǎn)匯總:
①深/淺拷貝的定義及理解
②copy和mutableCopy的區(qū)別與實(shí)現(xiàn)的拷貝效果
③完全深拷貝的實(shí)現(xiàn)方法
④NSString在賦值時(shí)修改了指針指向
⑤屬性中copy與strong特性的區(qū)別
面試相關(guān):
深/淺拷貝這塊的知識(shí)也是一道經(jīng)典面試題滩租,如果被問(wèn)到拷貝相關(guān)的知識(shí)赋秀,多半是在考驗(yàn)?zāi)銓?duì)于內(nèi)存分配,內(nèi)存地址相關(guān)的內(nèi)容律想,Good Luck
總結(jié)
No1:可變對(duì)象的copy和mutableCopy方法都是深拷貝(區(qū)別完全深拷貝與單層深拷貝) 猎莲。
No2:不可變對(duì)象的copy方法是淺拷貝,mutableCopy方法是深拷貝蜘欲。
No3:copy方法返回的對(duì)象都是不可變對(duì)象益眉。
No4:"="等號(hào)是淺拷貝晌柬,即指針拷貝
Demo下載地址:
https://github.com/gaoyuGood/copy-mutableCopy
參考文獻(xiàn):
深復(fù)制姥份、淺復(fù)制、copy年碘、mutableCopy
從源碼看iOS中的深拷貝和淺拷貝
OC之?dāng)?shù)組的copy方法
iOS 圖文并茂的帶你了解深拷貝與淺拷貝