一、從面向對象到Objective-C概覽copy
- 面向對象:
在面向對象的程序設計中,對象的copy就是創建一個已經存在的對象的copy。這種對象的創建的結果被稱為原始對象的copy。copy是很基礎的,但是也有其精巧的地方,并且可能造成巨大的消耗。有很多種方式可以copy對象,最常用的就是copy構造器和克隆。copy經常用于對象的修改、移動和保護。如果上述的幾種應用都不需要,持有原始對象的引用就足夠了,并不需要copy。
- OC:
在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
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
@end
- 在/usr/include/objc 下面找到 runtime 的 NSObject.h
- (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。
代碼
// 此處以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);
結果分析
// 此處依次對應上述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:方法。
代碼
// 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);
結果分析
// 對應上述一,可見實現了淺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中的原則。
代碼
// 和NSString淺copy的驗證步驟一樣
NSArray *arr = [NSArray arrayWithObjects:@"1", nil];
NSArray *copyArr = [arr copy];
NSLog(@"%p", arr);
NSLog(@"%p", copyArr);
結果分析
// 無疑是淺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,而未對其容器內對象進行處理。
代碼
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]);
結果分析
// 可發現前兩項地址不同,即完成深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這種情況)。
代碼
// 隨意創建一個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]);
結果分析
// 這里可以發現,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。這里,官方文檔的描述有誤。
4、完全深copy
準則
如果想完美的解決NSArray嵌套NSArray這種情形,可以使用歸檔、解檔的方式。
代碼
// 隨意創建一個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]);
結果分析
// 可見完成了完全深復制,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
代碼與結果
// 首先分別給出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變化。
// 也許你會疑問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:方法。