從源碼看iOS中的深拷貝和淺拷貝

關(guān)于iOS中對象的深拷貝和淺拷貝的文章有很多,但是大部分都是基于打印內(nèi)存地址來推導(dǎo)結(jié)果,這篇文章是從源碼的角度來分析深拷貝和淺拷貝。

深拷貝和淺拷貝的概念

拷貝的方式有兩種:深拷貝和淺拷貝。

  • 淺拷貝又叫指針拷貝,比如說有一個指針,這個指針指向一個字符串,也就是說這個指針變量的值是這個字符串的地址,那么此時對這個字符串進(jìn)行指針拷貝的意思就是又創(chuàng)建了一個指針變量,這個指針變量的值是這個字符串的地址,也就是這個字符串的引用計數(shù)+1。
  • 深拷貝又叫內(nèi)容拷貝,比如有一個指針,這個指針指向一個字符串,也就是說這個指針變量的值是這個字符串的地址值,那么此時對這個字符串進(jìn)行內(nèi)容拷貝,就會創(chuàng)建一個新的指針,在一個新的地址區(qū)域創(chuàng)建一個字符串,這個字符串的值和原字符串的值相同,新的指針指向這個新創(chuàng)建的字符串。這時原字符串的引用計數(shù)沒有+1。

對象的copy和mutableCopy方法

不管是集合對象還是非集合對象,接收到copy和mutableCopy消息時,都遵循以下準(zhǔn)則:

  • copy返回immutable對象
  • mutableCopy返回mutable對象
    下面對非集合對象和集合對象的copy和mutableCopy方法進(jìn)行具體的闡述。

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);

打印結(jié)果:

0x101545068 
0x101545068 
0x60000024e940

通過打印結(jié)果我們可以看出來,copyString和string的地址值一樣,而mutableCopyString和string的地址值不一樣,這就說明imutable對象的copy方法進(jìn)行了淺拷貝,mutableCopy方法進(jìn)行了深拷貝。

  • 再來看看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);

打印結(jié)果:

0x600000240e40 
0xa000000747365744 
0x6000002411a0

通過打印結(jié)果可以看出來,copyString和string的內(nèi)存地址不同,mutableCopyString和string的內(nèi)存地址也不同。這說明mutable對象的copy方法和mutableCopy方法都進(jìn)行了深拷貝。
總結(jié)起來就是:

immutable對象的copy方法進(jìn)行了淺拷貝
immutable對象的mutableCopy方法進(jìn)行了深拷貝
mutable對象的copy方法進(jìn)行了深拷貝
mutable對象的mutableCopy方法進(jìn)行了深拷貝。

用代碼表示就是:

    [immutableObject copy];//淺拷貝
    [immutableObject mutableCopy];//深拷貝
    [mutableObject copy];//深拷貝
    [mutableObject mutableCopy];//深拷貝

以上是通過打印內(nèi)存地址得出的結(jié)論,下面我們通過查看源碼來證實(shí)一下我們的結(jié)論。
在opensource.apple.com的git倉庫中的runtime源碼中有NSObject.mm這個文件,在這個文件中有copy和mutableCopy方法的實(shí)現(xiàn):

- (id)copy {
    return [(id)self copyWithZone:nil];
}

- (id)mutableCopy {
    return [(id)self mutableCopyWithZone:nil];
}

我們發(fā)現(xiàn)copy和mutableCopy方法只是簡單的調(diào)用了copyWithZone:mutableCopyWithZone:兩個方法。然后我在searchcode.com中找到了NSString和NSMutableString的Source Code。
NSString.m中,找到了關(guān)于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對象,調(diào)用copy方法就是調(diào)用了copyWithZone:方法。而copyWithZone:方法并沒有創(chuàng)建新的對象,而是使指針持有了原來的NSString對象,所以NSString的copy方法是淺拷貝。
而調(diào)用mutableCopy方法就是調(diào)用了mutableCopyWithZone:方法。從mutableCopyWithZone:的實(shí)現(xiàn)我們可以看到,這個方法是創(chuàng)建了一個新的可變的字符串對象。因此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對象調(diào)用copy方法會調(diào)用這里的copyWithZone:方法的實(shí)現(xiàn),我們可以看到這里創(chuàng)建了一個新的不可變的字符串。所以對NSMutableString對象執(zhí)行copy方法是深拷貝。
由于在NSMutableString中沒有實(shí)現(xiàn)mutableCopyWithZone:方法,所以會調(diào)用父類的mutableCopyWithZone:方法,也就是NSString類的mutableCopyWithZone:方法,而我們知道,NSString類的mutableCopyWithZone:方法會創(chuàng)建一個新的可變字符串。所以對NSMutableString對象執(zhí)行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);

打印結(jié)果:

0x60400025bed0
0x60400025bed0
0x60400025c2f0

通過打印結(jié)果可以看出來,copyArray的地址和array的地址是一樣的,說明對array進(jìn)行copy是進(jìn)行淺拷貝。而mutableCopyArray的地址和array的地址是不一樣的,說明對array進(jìn)行mutableCopy是進(jìn)行了深拷貝。
再來看mutable對象執(zhí)行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);

打印結(jié)果:

0x604000447440
0x604000447050
0x604000447080

通過打印結(jié)果可以看出,copyArray和mutableCopyArray的地址都和array的地址不同,這說明對可變數(shù)組進(jìn)行copy和mutableCopy操作都進(jìn)行了深拷貝。
因此得出結(jié)論:

在集合類對象中,對immutable對象進(jìn)行copy操作是淺拷貝,進(jìn)行mutableCopy操作是深拷貝。對mutable對象進(jìn)行copy操作是深拷貝,進(jìn)行mutableCopy操作是深拷貝。

用代碼表示就是:

    [immutableObject copy];//淺拷貝
    [immutableObject mutableCopy];//深拷貝
    [mutableObject copy];//深拷貝
    [mutableObject mutableCopy];//深拷貝

以上是通過打印內(nèi)存地址得到的結(jié)論,下面我們通過源碼來驗(yàn)證一下我們的結(jié)論。
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];
}

當(dāng)調(diào)用copy方法時,實(shí)際上是執(zhí)行了這里的copyWithZone:方法,在這個方法里面并沒有創(chuàng)建新的對象,而只是持有了舊的對象,因此,對于不可變的數(shù)組對象,執(zhí)行copy操作是淺拷貝。
當(dāng)調(diào)用mutableCopy方法時,實(shí)際上是執(zhí)行了這里的mutableCopyWithZone:方法,在這個方法里面,利用原來的數(shù)組對象,創(chuàng)建了一個新的可變數(shù)組對象,因此對于不可變的數(shù)組對象,執(zhí)行mutableCopy操作是深拷貝。
NSArray.m這個文件的第825行是NSMutableArray的實(shí)現(xiàn)。在第875行找到了copyWithZone:的實(shí)現(xiàn),沒有找到mutableCopyWithZone:的實(shí)現(xiàn):

- (id)copyWithZone:(NSZone*)zone
{
    if (NSArrayClass == Nil)
        NSArrayClass = [NSArray class];
    return [[NSArrayClass alloc] initWithArray:self copyItems:YES];
}

當(dāng)調(diào)用copy方法時,實(shí)際是調(diào)用了這里的copyWithZone:方法,在這個方法的實(shí)現(xiàn)里,是利用原來的可變數(shù)組創(chuàng)建了一個新的不可變數(shù)組,因此對可變數(shù)組執(zhí)行copy操作是深拷貝。
當(dāng)調(diào)用mutableCopy時,由于NSMutableArray本身沒有實(shí)現(xiàn)mutableCopyWithZone:方法,所以會調(diào)用父類也就是NSArray類的實(shí)現(xiàn),而通過上面我們也能看到NSArray的實(shí)現(xiàn):利用原數(shù)組創(chuàng)建了一個新的可變數(shù)組,因此,對可變數(shù)組進(jìn)行mutableCopy操作是深拷貝。

回答經(jīng)典面試題

面試題:為什么NSString類型的成員變量的修飾屬性用copy而不是strong呢?

首先要搞清楚的就是對NSString類型的成員變量用copy修飾和用strong修飾的區(qū)別。如果使用了copy修飾符,那么在給成員變量賦值的時候就會對被賦值的對象進(jìn)行copy操作,然后再賦值給成員變量。如果使用的是strong修飾符,則不會執(zhí)行copy操作,直接將被賦值的變量賦值給成員變量。
假設(shè)有一個NSString類型的成員變量string,對其進(jìn)行賦值:

    NSString *testString = @"test";
    self.string = testString;

如果該成員變量是用copy修飾的,則等價于:

self.string = [testString copy];

如果是用strong修飾的,則沒有copy操作:

self.string = testString;

知道了使用copy和strong的區(qū)別后,我們再來分析為什么要使用copy修飾符。先看一段代碼:

    NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
    self.string = mutableString;
    NSLog(@"%@", self.string);
    [mutableString appendString:@"addstring"];
    NSLog(@"%@", self.string);

如果這里成員變量string是用strong修飾的話,打印結(jié)果就是:

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

很顯然,當(dāng)mutableString的值發(fā)生了改變后,string的值也隨之發(fā)生改變,因?yàn)?code>self.string = mutableString;這行代碼實(shí)際上是執(zhí)行了一次指針拷貝。string的值隨mutableString的值的發(fā)生改變這顯然不是我們想要的結(jié)果。
如果成員變量string是用copy修飾,打印結(jié)果就是:

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

這是因?yàn)槭褂胏opy修飾符后,self.string = mutableString;就等價于self.string = [mutableString copy];,也就是進(jìn)行了一次深拷貝,所以mutableString的值再發(fā)生變化就不會影響到string的值。

回答面試題:

NSString類型的成員變量使用copy修飾而不是strong修飾是因?yàn)橛袝r候賦給該成員變量的值是NSMutableString類型的,這時候如果修飾符是strong,那成員變量的值就會隨著被賦值對象的值的變化而變化。若是用copy修飾,則對NSMutableString類型的值進(jìn)行了一次深拷貝,成員變量的值就不會隨著被賦值對象的值的改變而改變。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文為轉(zhuǎn)載: 作者:zyydeveloper 鏈接:http://www.itdecent.cn/p/5f776a...
    Buddha_like閱讀 1,012評論 0 2
  • 前言 不敢說覆蓋OC中所有copy的知識點(diǎn),但最起碼是目前最全的最新的一篇關(guān)于 copy的技術(shù)文檔了。后續(xù)發(fā)現(xiàn)有新...
    zyydeveloper閱讀 3,703評論 4 35
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,194評論 8 265
  • 深拷貝和淺拷貝這個問題在面試中常常被問到,而在實(shí)際開發(fā)中,只要稍有不慎,就會在這里出現(xiàn)問題。尤其對于初學(xué)者來說,我...
    西門淋雨閱讀 1,924評論 0 1
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,621評論 1 32

友情鏈接更多精彩內(nèi)容