iOS 可變拷貝VS不可變拷貝

iOS 可變拷貝VS不可變拷貝

概念

我們先來了解兩個概念

深拷貝deep copy: 直接拷貝整個對象內存到另一塊內存中;

淺拷貝shallow copy: 并不拷貝對象本身,僅僅是拷貝指向對象的指針;

詳細介紹看下面這段Apple Documents描述;

The normal copy is a shallow copy that produces a new collection that shares ownership of the objects with the original. Deep copies create new objects from the originals and add those to the new collection.

字符串

我們分成2種情況測試:

  1. 初始值不可變
    NSString *originStr = @"hello";
    NSString *copyStr = [originStr copy];
    NSString *mulCopyStr = [originStr mutableCopy];
    NSLog(@"originStr address: %p", originStr);
    NSLog(@"copyStr address: %p", copyStr);
    NSLog(@"mulCopyStr address: %p", mulCopyStr);
2017-09-21 13:36:08.859 iOSLockDemo[35388:2491304] originStr address: 0x1021e6078
2017-09-21 13:36:08.859 iOSLockDemo[35388:2491304] copyStr address: 0x1021e6078
2017-09-21 13:36:08.860 iOSLockDemo[35388:2491304] mulCopyStr address: 0x608000078000

通過log可以發現,此時copy是淺拷貝,mutableCopy是深拷貝;

  1. 初始值可變
    NSString *originStr = [@"hello" mutableCopy];
    NSString *copyStr = [originStr copy];
    NSString *mulCopyStr = [originStr mutableCopy];
    NSLog(@"originStr address: %p", originStr);
    NSLog(@"copyStr address: %p", copyStr);
    NSLog(@"mulCopyStr address: %p", mulCopyStr);
2017-09-21 13:39:33.036 iOSLockDemo[35469:2498312] originStr address: 0x60800007bd40
2017-09-21 13:39:33.037 iOSLockDemo[35469:2498312] copyStr address: 0xa00006f6c6c65685
2017-09-21 13:39:33.037 iOSLockDemo[35469:2498312] mulCopyStr address: 0x60800007c000

通過log可以發現,此時copymutableCopy都是深拷貝;

集合

  1. 初始值為不可變集合
    NSArray *originArr = @[@"item1"];
    NSLog(@"originArr address: %p", originArr);
    NSLog(@"originArr item address: %p", originArr[0]);
    
    NSArray *copyArr = [originArr copy];
    NSLog(@"copyArr address: %p", copyArr);
    NSLog(@"copyArr item address: %p", copyArr[0]);
    
    NSArray *mulCopyArr = [originArr mutableCopy];
    NSLog(@"mulCopyArr address: %p", mulCopyArr);
    NSLog(@"mulCopyArr item address: %p", mulCopyArr[0]);

2017-09-21 13:48:16.515 iOSLockDemo[35683:2513469] originArr address: 0x600000006ca0
2017-09-21 13:48:16.516 iOSLockDemo[35683:2513469] originArr item address: 0x10d338080
2017-09-21 13:48:16.516 iOSLockDemo[35683:2513469] copyArr address: 0x600000006ca0
2017-09-21 13:48:16.517 iOSLockDemo[35683:2513469] copyArr item address: 0x10d338080
2017-09-21 13:48:16.517 iOSLockDemo[35683:2513469] mulCopyArr address: 0x608000244800
2017-09-21 13:48:16.518 iOSLockDemo[35683:2513469] mulCopyArr item address: 0x10d338080

通過Log,我們可以發現,當初始值不可變時

copy操作,無論集合本身,還是集合里的objects都是指針拷貝,這時我們可以稱這個過程是淺拷貝。

mutableCopy操作,集合指針發生了變化,說明生成了新的集合,但是集合里的objects指針還是和原來一樣。那么這時我們還能稱之為深拷貝嗎?

下面我們引入一個概念單層深拷貝one-level-deep copy;什么意思呢?下面我們看一段代碼就明白了;

Apple Documents提供了一個進行one-level-deep copy的方法。

[[NSArray alloc] initWithArray:<#(nonnull NSArray *)#> copyItems:<#(BOOL)#>]

You can use the collection’s equivalent of initWithArray:copyItems: with YES as the second parameter. If you create a deep copy of a collection in this way, each object in the collection is sent a copyWithZone: message. If the objects in the collection have adopted the NSCopying protocol, the objects are deeply copied to the new collection, which is then the sole owner of the copied objects. If the objects do not adopt the NSCopying protocol, attempting to copy them in such a way results in a runtime error.

用這種方法深復制,集合里的每個對象都會收到copyWithZone:消息。如果集合里的對象遵循NSCopying協議,那么對象就會被深拷貝(deep copy)到新的集合,并且這個新的集合是被拷貝對象的唯一所有者,如果對象沒有遵循 NSCopying 協議,而嘗試用這種方法進行深復制,會在運行時出錯。

看到這里,是不是覺得生活很美好,這方法使用很簡單?too young too naive;下面我們看幾個例子;

  1. 集合不可變,集合內元素為非集合對象,并且集合內元素為不可變對象。
    NSArray *originArr = @[@"11", @"22"];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
   
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr address: 0x608000029180
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] originArr subArr item 1 address: 0x10f0120a0
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] copyArr address: 0x600000028600
2017-09-21 15:58:53.073 iOSLockDemo[38249:2650494] copyArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] copyArr subArr item 1 address: 0x10f0120a0
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr address: 0x60000005c5f0
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr subArr item 0 address: 0x10f012080
2017-09-21 15:58:53.074 iOSLockDemo[38249:2650494] mutCopyArr subArr item 1 address: 0x10f0120a0

通過log發現了什么?無論可變拷貝,還是非可變拷貝,進行深拷貝的仍然只是集合本身,而集合內的元素還是指針拷貝,這下懵了,說好的集合內對象進行深拷貝呢?

再看下面一個例子

  1. 集合不可變,集合內元素為非集合對象,并且集合內元素為可變對象。
    NSArray *originArr = @[[@"11" mutableCopy], [@"22"  mutableCopy]];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:06:16.961 iOSLockDemo[38399:2658835] originArr address: 0x60000003a900
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] originArr subArr item 0 address: 0x60000007c9c0
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] originArr subArr item 1 address: 0x60000007cc80
2017-09-21 16:06:16.964 iOSLockDemo[38399:2658835] copyArr address: 0x60000003a920
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] copyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] copyArr subArr item 1 address: 0xa000000000032322
2017-09-21 16:06:16.965 iOSLockDemo[38399:2658835] mutCopyArr address: 0x60800005e390
2017-09-21 16:06:16.966 iOSLockDemo[38399:2658835] mutCopyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:06:16.966 iOSLockDemo[38399:2658835] mutCopyArr subArr item 1 address: 0xa000000000032322

通過log發現,無論可變拷貝,還是非可變拷貝,集合本身和集合內對象都進行了深拷貝,但是兩次拷貝的結果中,集合內對象的指針地址居然相同(ps: 多次測試,仍然如此,有興趣的同學,可以多加幾組拷貝試試),這下更懵了。

  1. 集合可變,集合內元素為非集合對象,并且集合內元素為不可變對象
    NSArray *originArr = [@[@"11", @"2"] mutableCopy];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:15:27.249 iOSLockDemo[38637:2669678] originArr address: 0x60000005a880
2017-09-21 16:15:27.249 iOSLockDemo[38637:2669678] originArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] originArr subArr item 1 address: 0x10baf80a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr address: 0x608000035580
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] copyArr subArr item 1 address: 0x10baf80a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr address: 0x60800005e5a0
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr subArr item 0 address: 0x10baf8080
2017-09-21 16:15:27.250 iOSLockDemo[38637:2669678] mutCopyArr subArr item 1 address: 0x10baf80a0

通過log,這組結果和第一組是一樣的。好,我們再看一組實驗。

  1. 集合可變,集合內元素為非集合對象,并且集合內元素為可變對象
    NSArray *originArr = [@[[@"11" mutableCopy], [@"22"  mutableCopy]] mutableCopy];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:17:48.706 iOSLockDemo[38703:2673443] originArr address: 0x6000000482b0
2017-09-21 16:17:48.706 iOSLockDemo[38703:2673443] originArr subArr item 0 address: 0x600000267600
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] originArr subArr item 1 address: 0x6000002678c0
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr address: 0x60800003d120
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] copyArr subArr item 1 address: 0xa000000000032322
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr address: 0x608000046510
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr subArr item 0 address: 0xa000000000031312
2017-09-21 16:17:48.707 iOSLockDemo[38703:2673443] mutCopyArr subArr item 1 address: 0xa000000000032322

這組實驗結果和第二組一樣。是不是徹底懵了?難道是官方文檔寫錯了?好,那我們自定義一個對象,并實現NSCopying協議,然后在集合中使用我們自定義的對象。

#import <Foundation/Foundation.h>

@interface Person : NSObject<NSCopying>

@end

#import "Person.h"

@implementation Person

- (id)copyWithZone:(nullable NSZone *)zone {
    return [Person allocWithZone:zone];
}

@end
    
    NSArray *originArr = @[[Person new], [Person new]];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 16:33:46.501 iOSLockDemo[39061:2691963] originArr address: 0x60000003cc60
2017-09-21 16:33:46.501 iOSLockDemo[39061:2691963] originArr subArr item 0 address: 0x60000001d790
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] originArr subArr item 1 address: 0x60000001d7a0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr address: 0x60800003d2e0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr subArr item 0 address: 0x60800001d2d0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] copyArr subArr item 1 address: 0x60800001d2a0
2017-09-21 16:33:46.502 iOSLockDemo[39061:2691963] mutCopyArr address: 0x60800005fda0
2017-09-21 16:33:46.503 iOSLockDemo[39061:2691963] mutCopyArr subArr item 0 address: 0x60800001d2b0
2017-09-21 16:33:46.503 iOSLockDemo[39061:2691963] mutCopyArr subArr item 1 address: 0x60800001d2c0

好神奇,這次結果和蘋果官方文檔寫的一致了。這是為什么呢?

[[NSArray alloc] initWithArray: copyItems:得到的最終結果其實是和copyWithZone:的實現方式息息相關的,之所以集合中是字符串時會出現和官方文檔相左的結果;

以下是個人猜測,望了解的同學可以指點一二。


因為NSString在實現copyWithZone:根據不同的情況作了優化。如果當前是可變對象,則返回一個不可變的副本,如果當前對象是不可變對象,則返回當前對象。**

**至于第二次實驗中兩次拷貝結果中的容器中的對象地址相同的情況,是NSArray在實現[[NSArray alloc] initWithArray: copyItems:為了節約內存開銷,進行了優化。當第一次調用時,會給集合內對象發送copyWithZone:生成新的不可變副本,以后調用時,不再向原對象發送消息,而是使用第一次生成的不可變副本。


下面我們測試一下多層集合嵌套的情況。

    NSArray *originArr = @[@[[Person new], [Person new]]];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray *copyArr = [[NSArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray *mutCopyArr = [[NSMutableArray alloc] initWithArray:originArr copyItems:YES];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 17:16:34.672 iOSLockDemo[39913:2737590] originArr address: 0x600000005670
2017-09-21 17:16:34.672 iOSLockDemo[39913:2737590] originArr subArr address: 0x60000002d820
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] originArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] originArr subArr item 1 address: 0x600000005660
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr address: 0x600000005610
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr address: 0x60000002d820
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.673 iOSLockDemo[39913:2737590] copyArr subArr item 1 address: 0x600000005660
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr address: 0x600000057970
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr address: 0x60000002d820
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr item 0 address: 0x600000005650
2017-09-21 17:16:34.674 iOSLockDemo[39913:2737590] mutCopyArr subArr item 1 address: 0x600000005660

通過log,可以發現,只有最外層的集合地址發生了變化,其他的不變。

由此我們得出結論:

one-level-deep copy所謂的單層深拷貝的意思是,只有非嵌套集合,且集合內對象在實現copyWithZone:時返回新的對象時,才會產生真正意義上的深拷貝,也就是完全拷貝(real-deep copy)

那么怎樣才能實現真正意義上的深拷貝呢?看下面實現。

完全拷貝

    NSArray *originArr = @[@[[@"11" mutableCopy], [@"22"  mutableCopy]]];
    NSLog(@"originArr address: %p", originArr);
    [self print:originArr arrName:@"originArr"];
    
    NSArray* copyArr = [NSKeyedUnarchiver unarchiveObjectWithData:
                                  [NSKeyedArchiver archivedDataWithRootObject:originArr]];
    NSLog(@"copyArr address: %p", copyArr);
    [self print:copyArr arrName:@"copyArr"];
    
    NSArray* mutCopyArr = [NSKeyedUnarchiver unarchiveObjectWithData:
                        [NSKeyedArchiver archivedDataWithRootObject:originArr]];
    NSLog(@"mutCopyArr address: %p", mutCopyArr);
    [self print:mutCopyArr arrName:@"mutCopyArr"];
2017-09-21 17:27:49.138 iOSLockDemo[40148:2754251] originArr address: 0x6080000025d0
2017-09-21 17:27:49.138 iOSLockDemo[40148:2754251] originArr subArr address: 0x608000026ce0
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] originArr subArr item 0 address: 0x608000072980
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] originArr subArr item 1 address: 0x6080000729c0
2017-09-21 17:27:49.139 iOSLockDemo[40148:2754251] copyArr address: 0x600000002550
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr address: 0x600000027780
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr item 0 address: 0x600000077600
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] copyArr subArr item 1 address: 0x600000077740
2017-09-21 17:27:49.140 iOSLockDemo[40148:2754251] mutCopyArr address: 0x600000002570
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr address: 0x600000027620
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr item 0 address: 0x600000077540
2017-09-21 17:27:49.141 iOSLockDemo[40148:2754251] mutCopyArr subArr item 1 address: 0x600000077680

通過log,可以發現,這次是完全拷貝

結語

對其他集合類型NSSet,NSDictionary也具有以上特點。有興趣的同學可以試試。

參考

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html#//apple_ref/doc/uid/TP40010162-SW3

https://www.zybuluo.com/MicroCai/note/50592

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容