一、從物件導向到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
123456@protocol NSCopying- (id)copyWithZone:(nullable NSZone *)zone;@end@protocol NSMutableCopying- (id)mutableCopyWithZone:(nullable NSZone *)zone;@end - 在/usr/include/objc 下面找到 runtime 的 NSObject.h
12- (id)copy;- (id)mutableCopy; - 修飾屬性的關鍵字copy
2、這裡需要注意的有以下幾點
- 若想使用copy和mutableCopy,需要分別實現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 @property (nonatomic, assign) NSInteger a; @end @implementation Model1 - (id)copyWithZone:(NSZone *)zone { return self; } @end // Model定義,copyWithZone第二種實現(深copy) @interface Model1 : NSObject @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); |
結果分析
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
七、感謝
最後,感謝來自百度的@楊飛宇同學,和他的討論給了我很多靈感。也感謝大家的閱讀,希望對您有所幫助。如果有錯誤的地方或者不理解的地方,希望大家在評論區積極指出。如果對您有所幫助,希望給作者一個喜歡和關注,您的支援是我最核心的動力。