iOS copy mutableCopy詳解

一、從面向對象到Objective-C概覽copy

1、面向對象:

In object-oriented programming, object copying is creating a copy of an existing object, a unit of data in object-oriented programming. The resulting object is called an object copy or simply copy of the original object. Copying is basic but has subtleties and can have significant overhead. There are several ways to copy an object, most commonly by a copy constructor or cloning. Copying is done mostly so the copy can be modified or moved, or the current value preserved. If either of these is unneeded, a reference to the original data is sufficient and more efficient, as no copying occurs.

在面向對象的程序設計中,對象的copy就是創建一個已經存在的對象的copy。這種對象的創建的結果被稱爲原始對象的copy。copy是很基礎的,但是也有其精巧的地方,並且可能造成巨大的消耗。有很多種方式可以copy對象,最常用的就是copy構造器和克隆。copy經常用於對象的修改、移動和保護。如果上述的幾種應用都不需要,持有原始對象的引用就足夠了,並不需要copy。

2、OC:

In Objective-C, the methods copy and mutableCopy are inherited by all objects and intended for performing copies; the latter is for creating a mutable type of the original object. These methods in turn call the copyWithZone and mutableCopyWithZone methods, respectively, to perform the copying. An object must implement the corresponding copyWithZone method to be copyable.

在OC中,copy和mutableCopy兩個方法是被所有對象繼承的(有點小毛病,應該指所有繼承自NSObject的類),這兩個方法就是爲copy準備的。其中,mutableCopy是爲了創建原始對象的可變類型的copy。這兩個方法分別調用copyWithZone和mutableCopyWithZone兩個方法來進行copy。一個對象必須實現copyWithZone或者mutableCopyWithZone,才能進行copy或者mutableCopy。

那麼,我們可以從以上獲取到什麼信息?

  • copy經常用於對象的修改、移動和保護。如果上述的幾種應用都不需要,持有原始對象的引用就足夠了,並不需要copy。

  • 一個類必須實現copyWithZone或者mutableCopyWithZone,才能進行copy或者mutableCopy。

下一階段,本文將展開講述OC中的copy相關信息以及如何使用copy方法。

二、Objective-C中copy相關

1、OC中的copy相關內容

  • 在XCode 裏Foundation.framework下的Headers裏,也在系統裏找到原文件:/System/Library/Frameworks/Foundation.framework/Versions/C/Headers/NSObject.h

1
2
3
4
5
6
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
@end
  • 在/usr/include/objc 下面找到 runtime 的 NSObject.h

1
2
- (id)copy;
- (id)mutableCopy;
  • 修飾屬性的關鍵字copy

2、這裏需要注意的有以下幾點

  • 若想使用copymutableCopy,需要分別實現NSCopying協議和NSMutableCopying協議,即實現copyWithZone:mutableCopyWithZone:方法。

  • 繼承自NSObject的大部分框架類均默認實現了NSCopying,並且一些具備可變類型的類如NSString、NSArray、NSDictionary,以及它們的可變類型類NSMutableString、NSMutableArray和NSMutableDictionary也實現了NSMutableCopying。(查了大部分常用類,均實現了NSCopying,所以暫時這麼說吧,可能有人說NSNumber並沒有實現NSCopying,那你可以看一下它的父類NSValue,其實現了NSCopying)

  • 對於一些自定義類,需要自己實現NSCopying。具體方式且看下部分。

三、非容器對象的深淺copy

首先,我們談一下非容器對象的深淺copy,這些非容器對象,包含常用的NSString、NSNumber等,也包括我們自定義的一些非容器類的實例。下面分三個三面進行分析。

1、首先說說深淺copy

準則
淺copy:指針複製,不會創建一個新的對象。
深copy:內容複製,會創建一個新的對象。

此處,不進行過多的解釋,從下面的結果分析中,按例子來理解。

2、框架類的深淺copy

準則
探究框架類深copy還是淺copy,需要清楚的是該類如何實現的NSCopying和NSMutableCopy的兩個方法copyWithZone:和mutableCopyWithZone:。然而OC並不開源,並且本文這裏也不會進行源碼的推測。
那麼,我們應該遵循怎樣一個原則呢?如下:

  • 對immutableObject,即不可變對象,執行copy,會得到不可變對象,並且是淺copy。

  • 對immutableObject,即不可變對象,執行mutableCopy,會得到可變對象,並且是深copy。

  • 對mutableObject,即可變對象,執行copy,會得到不可變對象,並且是深copy。

  • 對mutableObject,即可變對象,執行mutableCopy,會得到可變對象,並且是深copy。

代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 此處以NSString爲例探究框架類深淺copy
 
// 不可變對象
NSString *str = @"1";
NSString *str1 = [str copy];
NSString *str2 = [str mutableCopy];
 
// 可變對象
NSMutableString *mutableStr = [NSMutableString stringWithString:@"1"];
NSMutableString *mutableStr1 = [mutableStr copy];
NSMutableString *mutableStr2 = [mutableStr mutableCopy];
 
// 打印對象的指針來確認是否創建了一個新的對象
// 不可變對象原始指針
NSLog(@"%p", str);
// 不可變對象copy後指針
NSLog(@"%p", str1);
// 不可變對象mutalbeCopy後指針
NSLog(@"%p", str2);
 
// 可變對象原始指針
NSLog(@"%p", mutableStr);
// 可變對象copy後指針
NSLog(@"%p", mutableStr1);
// 可變對象mutalbeCopy後指針
NSLog(@"%p", mutableStr2);

結果分析

1
2
3
4
5
6
7
// 此處依次對應上述6個log,可見與前面所講的原則吻合(此處不驗證可變類型和不可變類型,默認上述原則正確即可)。
2016-10-21 10:50:52.879 Memory[67680:5623387] 0x10d85a1b0
2016-10-21 10:50:52.879 Memory[67680:5623387] 0x10d85a1b0
2016-10-21 10:50:52.879 Memory[67680:5623387] 0x60800007a080
2016-10-21 10:50:52.879 Memory[67680:5623387] 0x60800007a9c0
2016-10-21 10:50:52.880 Memory[67680:5623387] 0xa000000000000311
2016-10-21 10:50:52.880 Memory[67680:5623387] 0x60800007a900

3、自定義類的深淺copy

準則
對於一個我們自定義的類型,顯然比框架類容易操縱的多。此處就拿NSCopying舉例(因爲從沒有自定義過具有可變類型的類,當然,如果有需要的話,也可以實現NSMutableCopying)。自定義的類就和2中的原則沒有半毛錢關係了,一切就看你怎麼實現NSCopying協議中的copyWithZone:方法。

代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Model定義,copyWithZone第一種實現(淺copy)
@interface Model1 : NSObject <nscopying>
@property (nonatomic, assign) NSInteger a;
@end
 
@implementation Model1
- (id)copyWithZone:(NSZone *)zone {
    return self;
}
@end
 
// Model定義,copyWithZone第二種實現(深copy)
@interface Model1 : NSObject <nscopying>
@property (nonatomic, assign) NSInteger a;
@end
 
@implementation Model1
- (id)copyWithZone:(NSZone *)zone {
    Model1 *model = [[Model1 allocWithZone:zone] init];
    model.a = self.a;
    return model;
}
@end
 
// 分別選擇上述兩種model進行指針打印。
Model1 *model = [[Model1 alloc] init];
Model1 *copyModel = [model copy];
 
NSLog(@"%p", model);
NSLog(@"%p", copyModel);</nscopying></nscopying>

結果分析

1
2
3
4
5
6
// 對應上述一,可見實現了淺copy
2016-10-21 11:12:03.149 Memory[67723:5636292] 0x60000000c9d0
2016-10-21 11:12:03.149 Memory[67723:5636292] 0x60000000c9d0
// 對應上述二,可見實現了深copy
2016-10-21 11:16:46.803 Memory[67752:5640133] 0x60800001df00
2016-10-21 11:16:46.803 Memory[67752:5640133] 0x60800001def0

四、容器對象的深淺copy

前文已經知道了深淺copy的區別,你也大致猜到了爲什麼將容器對象拿出來作爲一塊。對,因爲容器中可能包含很多對象,而這些對象也需要區分深淺copy。往深裏說,容器中可能包含容器對象,那更是麻煩了。不要急,看下面,以NSArray的深淺copy爲例,將容器的深淺copy分爲四種。

1、淺copy

準則
容器的淺copy,符合三.2中的原則。
代碼

1
2
3
4
5
6
// 和NSString淺copy的驗證步驟一樣
NSArray *arr = [NSArray arrayWithObjects:@"1", nil];
NSArray *copyArr = [arr copy];
 
NSLog(@"%p", arr);
NSLog(@"%p", copyArr);

結果分析

1
2
3
// 無疑是淺copy(你可能會問,爲什麼不看一下arr和copyArr內部元素的指針對比?這裏並沒有必要,最外層對象都沒有創建新的,裏面不用驗證)
2016-10-21 11:27:57.554 Memory[67778:5646253] 0x600000010690
2016-10-21 11:27:57.554 Memory[67778:5646253] 0x600000010690

2、單層深copy

準則
容器的單層深copy,符合三.2中的原則(只是深copy變成了單層深copy)。這裏的單層指的是完成了NSArray對象的深copy,而未對其容器內對象進行處理。
代碼

1
2
3
4
5
6
7
8
9
NSArray *arr = [NSArray arrayWithObjects:@"1", nil];
NSArray *copyArr = [arr mutableCopy];
 
NSLog(@"%p", arr);
NSLog(@"%p", copyArr);
 
// 打印arr、copyArr內部元素進行對比
NSLog(@"%p", arr[0]);
NSLog(@"%p", copyArr[0]);

結果分析

1
2
3
4
5
// 可發現前兩項地址不同,即完成深copy,但是後兩項相同,這代表容器內部的元素並沒有完成深copy,所有稱之爲單層深copy
2016-10-21 11:32:27.157 Memory[67801:5649757] 0x6000000030d0
2016-10-21 11:32:27.157 Memory[67801:5649757] 0x600000242e50
2016-10-21 11:32:27.157 Memory[67801:5649757] 0x10dd811b0
2016-10-21 11:32:27.157 Memory[67801:5649757] 0x10dd811b0

3、雙層深copy

準則
容器的雙層深copy已經脫離了三.2中的原則。這裏的雙層指的是完成了NSArray對象和NSArray容器內對象的深copy(爲什麼不說完全,是因爲無法處理NSArray中還有一個NSArray這種情況)。
代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 隨意創建一個NSMutableString對象
NSMutableString *mutableString = [NSMutableString stringWithString:@"1"];
// 隨意創建一個包涵NSMutableString的NSMutableArray對象
NSMutableString *mutalbeString1 = [NSMutableString stringWithString:@"1"];
NSMutableArray *mutableArr = [NSMutableArray arrayWithObjects:mutalbeString1, nil];
// 將mutableString和mutableArr放入一個新的NSArray中
NSArray *testArr = [NSArray arrayWithObjects:mutableString, mutableArr, nil];
// 通過官方文檔提供的方式創建copy
NSArray *testArrCopy = [[NSArray alloc] initWithArray:testArr copyItems:YES];
 
// testArr和testArrCopy指針對比
NSLog(@"%p", testArr);
NSLog(@"%p", testArrCopy);
 
// testArr和testArrCopy中元素指針對比
// mutableString對比
NSLog(@"%p", testArr[0]);
NSLog(@"%p", testArrCopy[0]);
// mutableArr對比
NSLog(@"%p", testArr[1]);
NSLog(@"%p", testArrCopy[1]);
 
// mutableArr中的元素對比,即mutalbeString1對比
NSLog(@"%p", testArr[1][0]);
NSLog(@"%p", testArrCopy[1][0]);

結果分析

1
2
3
4
5
6
7
8
9
// 這裏可以發現,copy後,只有mutableArr中的mutalbeString1指針地址沒有變化。而testArr的指針和testArr中的mutableArr、mutableString的指針地址均發生變化。所以稱之爲雙層深複製。
2016-10-21 12:03:15.549 Memory[67855:5668888] 0x60800003c7a0
2016-10-21 12:03:15.549 Memory[67855:5668888] 0x60800003c880
2016-10-21 12:03:15.549 Memory[67855:5668888] 0x608000260540
2016-10-21 12:03:15.550 Memory[67855:5668888] 0xa000000000000311
2016-10-21 12:03:15.550 Memory[67855:5668888] 0x60800005d610
2016-10-21 12:03:15.550 Memory[67855:5668888] 0x60800000d2e0
2016-10-21 12:03:15.550 Memory[67855:5668888] 0x608000260980
2016-10-21 12:03:15.550 Memory[67855:5668888] 0x608000260980

限制
initWithArray: copyItems:會使NSArray中元素均執行copy方法。這也是我在testArr中放入NSMutableArray和NSMutableString的原因。如果我放入的是NSArray或者NSString,執行copy後,只會發生指針複製;如果我放入的是未實現NSCopying協議的對象,調用這個方法甚至會crash。這裏,官方文檔的描述有誤。

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.

4、完全深copy

準則
如果想完美的解決NSArray嵌套NSArray這種情形,可以使用歸檔、解檔的方式。
代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 隨意創建一個NSMutableString對象
NSMutableString *mutableString = [NSMutableString stringWithString:@"1"];
// 隨意創建一個包涵NSMutableString的NSMutableArray對象
NSMutableString *mutalbeString1 = [NSMutableString stringWithString:@"1"];
NSMutableArray *mutableArr = [NSMutableArray arrayWithObjects:mutalbeString1, nil];
// 將mutableString和mutableArr放入一個新的NSArray中
NSArray *testArr = [NSArray arrayWithObjects:mutableString, mutableArr, nil];
// 通過歸檔、解檔方式創建copy
NSArray *testArrCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
                            [NSKeyedArchiver archivedDataWithRootObject:testArr]];;
 
// testArr和testArrCopy指針對比
NSLog(@"%p", testArr);
NSLog(@"%p", testArrCopy);
 
// testArr和testArrCopy中元素指針對比
// mutableString對比
NSLog(@"%p", testArr[0]);
NSLog(@"%p", testArrCopy[0]);
// mutableArr對比
NSLog(@"%p", testArr[1]);
NSLog(@"%p", testArrCopy[1]);
 
// mutableArr中的元素對比,即mutalbeString1對比
NSLog(@"%p", testArr[1][0]);
NSLog(@"%p", testArrCopy[1][0]);

結果分析

1
2
3
4
5
6
7
8
9
// 可見完成了完全深複製,testArr和testArrCopy中的元素,以及容器中容器的指針地址完全不同,所以完成了完全深複製。
2016-10-21 12:19:34.022 Memory[67887:5677318] 0x60800002db00
2016-10-21 12:19:34.022 Memory[67887:5677318] 0x60800002dc20
2016-10-21 12:19:34.022 Memory[67887:5677318] 0x608000260400
2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080002603c0
2016-10-21 12:19:34.023 Memory[67887:5677318] 0x608000051d90
2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080000521e0
2016-10-21 12:19:34.023 Memory[67887:5677318] 0x608000260600
2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080002606c0

限制
歸檔和解檔的前提是NSArray中所有的對象都實現了NSCoding協議。

五、拾遺

1、關鍵字copy

代碼與結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 首先分別給出copy和strong修飾的屬性,以NSString舉例
// 1、strong
@property (nonatomic, strong) NSString *str;
// 2、copy
@property (nonatomic, copy) NSString *str;
 
// 分別對1和2執行下述代碼
NSMutableString *mutableStr = [NSMutableString stringWithFormat:@"123"];
self.str = mutableStr;
[mutableStr appendString:@"456"];
NSLog(@"%@", self.str);
NSLog(@"%p", self.str);
NSLog(@"%@", mutableStr);
NSLog(@"%p", mutableStr);
 
// 結果1
2016-10-21 14:08:46.657 Memory[68242:5714288] 123456
2016-10-21 14:08:46.657 Memory[68242:5714288] 0x608000071040
2016-10-21 14:08:46.657 Memory[68242:5714288] 123456
2016-10-21 14:08:46.657 Memory[68242:5714288] 0x608000071040
// 結果2
2016-10-21 14:11:16.879 Memory[68264:5716282] 123
2016-10-21 14:11:16.880 Memory[68264:5716282] 0xa000000003332313
2016-10-21 14:11:16.880 Memory[68264:5716282] 123456
2016-10-21 14:11:16.880 Memory[68264:5716282] 0x60000007bbc0

分析

  • 結果1爲strong修飾的結果,可見 [mutableStr appendString:@"456"]修改mutableStr造成了self.str的改變,顯然不安全;結果2爲copy修飾的結果,可見 [mutableStr appendString:@"456"]修改mutableStr未造成self.str的改變,顯然安全。(從內存地址的變化也可以看出來)

  • 這裏可以推測出,copy關鍵字是在str屬性的set方法裏面返回了mutableStr的copy,而strong關鍵字僅僅是返回了mutableStr。

2、深淺copy對引用計數的影響

淺copy,類似strong,持有原始對象的指針,會使retainCount加一。
深copy,會創建一個新的對象,不會對原始對象的retainCount變化。

1
2
3
4
5
// 也許你會疑問arc下如何訪問retainCount屬性,這裏提供了兩種方式(下面代碼中a代表一個任意對象,這個對象最好不要是NSString和NSNumber,因爲用它們進行測試會出問題)
// kvc方式
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)a));
// 橋接字方式
NSLog(@"Retain count %@", [a valueForKey:@"retainCount"]);

3、可變和不可變

可變和不可變上文談的不是很多,因爲本文認爲這完全與NSCopying和NSMutableCopying的實現息息相關。當然,對於框架類,我們可以簡單的認爲,copy方法返回的就是不可變對象,mutableCopy返回的就是可變對象。如果是自定義的類,就看你怎麼實現NSCopying和NSMutableCopying協議了。

4、copy和block

首先,MRR時代用retain修飾block會產生崩潰,因爲作爲屬性的block在初始化時是被存放在靜態區的,如果block內調用外部變量,那麼block無法保留其內存,在初始化的作用域內使用並不會有什麼影響,但一旦出了block的初始化作用域,就會引起崩潰。所有MRC中使用copy修飾,將block拷貝到堆上。
其次,在ARC時代,因爲ARC自動完成了對block的copy,所以修飾block用copy和strong都無所謂。

5、strong和shallowCopy

這個問題困惑了很久,最後只能得出一個結論,淺copy和strong引用的區別僅僅是淺copy多執行一步copyWithZone:方法。

六、文獻

1、https://developer.apple.com/library/prerelease/content/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html#//apple_ref/doc/uid/TP40010162-SW1
2、https://en.wikipedia.org/wiki/Object_copying

七、感謝

最後,感謝來自百度的@楊飛宇同學,和他的討論給了我很多靈感。也感謝大家的閱讀,希望對您有所幫助。如果有錯誤的地方或者不理解的地方,希望大家在評論區積極指出。如果對您有所幫助,希望給作者一個喜歡和關注,您的支持是我最核心的動力。


原文鏈接:http://www.jianshu.com/p/ebbac2fec4c6

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章