1. 屬性中copy
與strong
特性的區別
在開始學習淺復制(Shallow Copy)、深復制(Deep Copy)之前,先了解下屬性中copy
與strong
特性的區別。
copy
特性如下:
- copy:創建一個對象的副本。在創建的那一刻新對象與原始對象內容相同。
- 新的對象引用計數為1,與原始對象引用計數無關,且原始對象引用計數不會改變。
- 使用copy創建的新對象也是強引用,使用完成后需要負責釋放該對象。
-
copy
特性可以減少對象對上下文的依賴。新對象、原始對象中任一對象的值改變不會影響另一對象的值。 - 要想設置該對象的特性為
copy
,該對象必須遵守NSCopying
協議,Foundation類默認實現了NSCopying
協議,所以只需要為自定義的類實現該協議即可。
strong
特性如下:
- 創建一個強引用的指針,引用對象引用計數加1。
-
strong
特性表示兩個對象內存地址相同(建立一個指針,進行指針拷貝),內容會一直保持相同,直到更改一方內存地址,或將其設置為nil
。 - 如果有多個對象同時引用一個屬性,任一對象對該屬性的修改都會影響其他對象獲取的值。
如果想要對屬性中特性進行更全面了解,可以查看 iOS中定義屬性時的atomic、nonatomic、copy、assign、strong、weak等幾個特性的區別 這篇文章。
2. 淺復制與深復制
對象的拷貝有淺復制和深復制兩種方式。淺復制只復制指向對象的指針,并不復制對象本身;深復制是直接復制整個對象到另一塊內存中。即淺復制是復制指針,深復制是復制內容。
NSObject提供了copy
和mutableCopy
方法,copy
復制后對象是不可變對象(immutable),mutableCopy
復制后的對象是可變對象(mutable),與原始對象是否可變無關。
下面針對非集合類、集合類對象的深復制、淺復制進行說明。
2.1 非集合類對象的copy
與mutableCopy
非集合類對象指的是NSString
、NSNumber
之類的對象,深復制會復制引用對象的內容,而淺復制只復制引用這些對象的指針。因此,如果對象A被淺復制到對象B,對象B和對象A引用的是同一內存地址的實例變量或屬性。
2.1.1 不可變對象的copy
與mutableCopy
創建一個Single View Application模板的demo,demo名稱為copy&mutableCopy。進入ViewController.m
,在實現部分添加以下方法。
// 非容器類 不可變對象
- (void)immutableObject {
// 1.創建一個string字符串。
NSString *string = @"github.com/pro648";
NSString *stringB = string;
NSString *stringCopy = [string copy];
NSMutableString *stringMutableCopy = [string mutableCopy];
// 2.輸出指針指向的內存地址。
NSLog(@"Memory location of string = %p",string);
NSLog(@"Memory location of stringB = %p",stringB);
NSLog(@"Memory location of stringCopy = %p",stringCopy);
NSLog(@"Memory location of stringMutableCopy = %p",stringMutableCopy);
}
上述代碼分步說明如下:
- 創建一個
string
字符串,之后通過賦值,調用copy
、mutableCopy
方法進行復制操作。 - 通過使用
%p
,輸出指針所指向內容的內存地址。
然后在viewDidLoad中調用該方法:
- (void)viewDidLoad {
[super viewDidLoad];
// 1.非容器類 不可變對象
[self immutableObject];
}
運行demo,可以看到控制臺輸出如下:
Memory location of string = 0x1060e6068
Memory location of stringB = 0x1060e6068
Memory location of stringCopy = 0x1060e6068
Memory location of stringMutableCopy = 0x600000072000
可以看到,string
、stringB
和stringCopy
內存地址一致,即指向的是同一塊內存區域,進行了淺復制操作。而stringMutableCopy
與另外三個變量內存地址不同,系統為其分配了新內存,即進行了深復制操作。
2.1.2 可變對象的copy
與mutableCopy
繼續在實現部分添加以下方法,并記得在viewDidLoad
中調用。
// 2.非容器類 可變對象
- (void)mutableObject {
// 1.創建一個可變字符串。
NSMutableString *mString = [NSMutableString stringWithString:@"github.com/pro648"];
NSString *mStringCopy = [mString copy];
NSMutableString *mutablemString = [mString copy];
NSMutableString *mStringMutableCopy = [mString mutableCopy];
// 2.在可變字符串后添加字符串。
[mString appendString:@"AA"];
[mutablemString appendString:@"BB"]; // 運行時,這一行會報錯。
[mStringMutableCopy appendString:@"CC"];
// 3.輸出指針指向的內存地址。
NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
}
在上面代碼中,注釋2部分為可變字符串拼接字符串,運行到為mutablemString
拼接字符串這一行代碼時,程序會崩潰,因為通過copy
方法獲得的字符串是不可變字符串。所以在運行前要注釋掉這一行。
運行demo,可以看到控制臺輸出如下:
Memory location of
mString = 0x60000007bc00,
mstringCopy = 0x600000051940,
mutablemString = 0x6000000517c0,
mStringMutableCopy = 0x60000007bec0
可以看到四個對象內存地址各不相同。所以,這里的copy
和mutableCopy
執行的均為深復制。
綜合上面兩個例子,我們可以得出這樣結論:
- 對不可變對象執行
copy
操作,是指針復制,執行mutableCopy
操作是內容復制。 - 對可變對象執行
copy
操作和mutableCopy
操作都是內容復制。
用代碼表示如下:
[immutableObject copy]; // 淺復制
[immutableObject mutableCopy]; // 深復制
[mutableObject copy]; // 深復制
[mutableObject mutableCopy] ; // 深復制
2.2 容器類對象的深復制、淺復制
容器類對象指NSArray
、NSDictionary
等。容器類對象的深復制、淺復制如下圖所示:
對于容器類,需要探討的是復制后容器內元素的變化,而非容器本身內存地址是否發生了變化。
2.2.1 容器類對象的淺復制
有許多方法可以對集合進行淺復制。當對集合進行淺復制時,將復制原始集合中元素指針到新的集合,即原始集合中元素引用計數加一。
<a id="YES">
在實現部分添加以下方法,并在viewDidLoad
中調用該方法。
// 3.淺復制容器類對象。
- (void)shallowCopyCollections {
// 1.創建一個不可變數組,數組內元素為可變字符串。
NSMutableString *red = [NSMutableString stringWithString:@"Red"];
NSMutableString *green = [NSMutableString stringWithString:@"Green"];
NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
// 2.進行淺復制。
NSArray *myArray2 = [myArray1 copy];
NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1 copyItems:NO];
// 3.修改myArray2的第一個元素。
NSMutableString *tempString = myArray2.firstObject;
[tempString appendString:@"Color"];
// 4.輸出四個數組內存地址及四個數組內容。
NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}
</a>
運行demo,可以看到控制臺輸出如下:
Memory location of
myArray1 = 0x60800004f240,
myArray2 0x60800004f240,
myMutableArray3 0x60800004ef40,
myArray4 0x60800004f090
Contents of
myArray1 (
RedColor,
Green,
Blue
),
myArray2 (
RedColor,
Green,
Blue
),
myMutableArray3 (
RedColor,
Green,
Blue
),
myArray4 (
RedColor,
Green,
Blue
)
可以看到myArray1
和myArray2
數組內存地址相同,myMutableArray3
和myArray4
與其它數組內存地址各不相同。這是因為mutableCopy
的對象會被分配新的內存,alloc
會為對象分配新的內存空間。
觀察數組內元素,發現修改myArray2
數組內第一個元素,四個數組第一個元素都發生了改變,所以這里只進行了淺復制。
2.2.2 容器類對象的深復制
有兩種方式對容器類對象進行深復制:
- 第一種方法是:使用
initWithArray: copyItems:
類型方法,其中,第二個參數為YES
。 - 第二種方法是:使用歸檔、解檔。
下面先看如何使用initWithArray: copyItems:
類型方法。使用該方法進行深復制時,第二個參數為YES
。如果使用該方法對集合進行深復制,那么集合內每個元素都會收到copyWithZone:
消息,我們平常使用copy
、mutableCopy
方法時,系統會把copy
和mutableCopy
自動替換為copyWithZone:
和mutableCopyWithZone:
。即copy
和mutableCopy
只是簡便方法。如果集合內元素遵守NSCopying
協議,元素被復制到新的集合。如果集合內元素不遵守NSCopying
協議,用這樣的方式進行深復制,會在運行時產生錯誤。
copyWithZone:
產生的是淺復制,所以,這種方法只能產生一層深復制 one-level-deep copy,如果集合內元素仍然是集合,則子集合內元素不會被深復制,只對子集合內元素指針進行復制。
如果集合內元素為不可變對象,發送
copyWithZone:
消息后進行指針復制,該對象仍然不可變,因此只進行指針復制即可滿足需求。如果集合內元素為可變對象,發送
copyWithZone:
消息后進行的是內容復制,復制后該元素不可變,此時,完成了一層深復制。
把上面代碼注釋2部分中,initWithArray: copyItems:
第二個參數修改為YES
,注釋4中輸出部分修改為輸出數組第一個元素內存地址。更新后如下:
// 4.容器類一層深復制
- (void)oneLevelDeepCopy {
// 1.創建一個不可變數組,數組內元素為可變字符串。
NSMutableString *red = [NSMutableString stringWithString:@"Red"];
NSMutableString *green = [NSMutableString stringWithString:@"Green"];
NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
// 2.進行淺復制。
NSArray *myArray2 = [myArray1 copy];
NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1 copyItems:YES];
// 3.修改myArray2的第一個元素。
NSMutableString *tempString = myArray2.firstObject;
[tempString appendString:@"Color"];
// 4.輸出數組內第一個元素內存地址,輸出四個數組。
NSLog(@"Memory location of \n myArray1.firstObject = %p, \n myArray2.firstObject %p, \n myMutableArray3.firstObject %p, \n myArray4.firstObject %p",myArray1.firstObject, myArray2.firstObject, myMutableArray3.firstObject, myArray4.firstObject);
NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}
運行demo,可以看到控制臺輸出如下:
Memory location of
myArray1.firstObject = 0x600000079980,
myArray2.firstObject 0x600000079980,
myMutableArray3.firstObject 0x600000079980,
myArray4.firstObject 0xa000000006465523
Contents of
myArray1 (
RedColor,
Green,
Blue
),
myArray2 (
RedColor,
Green,
Blue
),
myMutableArray3 (
RedColor,
Green,
Blue
),
myArray4 (
Red,
Green,
Blue
)
可以看到myArray4
數組內第一個元素與其它數組第一個元素內存地址不同,即進行了一層深復制。
這種對集合進行深復制的方法,對其它類型集合也有效。如詞典中
initWithDictionary: withItems:
方法。
如果你的數組內元素是另一個數組,想要進行完全深復制,可以使用歸檔、解歸檔方法。使用該方法時,歸檔對象要遵守NSCoding
協議。如果你對歸檔不熟悉,可以查看我的另一篇文章:數據存儲之歸檔解檔 NSKeyedArchiver NSKeyedUnarchiver。
下面使用歸檔、解檔的方法進行完全深復制。
// 5.使用歸檔進行完全深復制。
- (void)trueDeepCopy {
// 1.創建一個可變數組,數組第一個元素是另一個可變數組,第二個元素是另一個不可變數組。
NSMutableString *hue = [NSMutableString stringWithString:@"hue"];
NSMutableString *saturation = [NSMutableString stringWithString:@"saturation"];
NSMutableString *brightness = [NSMutableString stringWithString:@"brightness"];
NSMutableArray *hsbArray1 = [NSMutableArray arrayWithObjects:hue, saturation, brightness, nil];
NSArray *hsbArray2 = [NSArray arrayWithObjects:hue, saturation, brightness, nil];
NSMutableArray *hsbArray3 = [NSMutableArray arrayWithObjects:hsbArray1, hsbArray2, nil];
// 2.通過歸檔、解檔進行完全深復制。
NSData *dataArea = [NSKeyedArchiver archivedDataWithRootObject:hsbArray3];
NSMutableArray *hsbArray4 = [NSKeyedUnarchiver unarchiveObjectWithData:dataArea];
// 3.輸出hsbArray3和hsbArray4數組第一個元素內存地址。
NSLog(@"Memory location of \n hsbArray3.firstObject = %p, \n hsbArray4.firstObject = %p",hsbArray3.firstObject, hsbArray4.firstObject);
}
上面代碼中,可變數組hsbArray3
第一個元素是可變數組hsbArray1
,第二個元素是不可變數組hsbArray2
。
使用歸檔、讀取歸檔方法深復制后,在控制臺輸出hsbArray3
和hsbArray4
第一個元素內存地址。輸出如下:
Memory location of
hsbArray3.firstObject = 0x60000004b100,
hsbArray4.firstObject = 0x60000004b1f0
可以看到hsbArray3
和hsbArray4
數組內元素內存地址不同,即進行了一層深復制。
在trueDeepCopy
方法內,繼續為hsbArray4
數組內第一個元素tempArray1
可變數組添加字符串對象。為hsbArray4
第二個元素hsbArray2
數組添加字符串對象。最后輸出hsbArray3
和hsbArray4
數組內容。
// 5.使用歸檔進行完全深復制。
- (void)trueDeepCopy {
...
// 4.為hsbArray4第一個元素添加字符串。
NSMutableArray *tempArray1 = hsbArray4.firstObject;
[tempArray1 addObject:@"hsb"];
// 5.hsbArray4第二個元素是hsbArray2,而hsbArray2是不可變數組,這一步將產生錯誤。
// NSMutableArray *tempArray2 = hsbArray4[1];
// [tempArray2 addObject:@"Color"];
// 6.輸出數組內容。
NSLog(@"Contents of \n hsbArray3 %@, \n hsbArray4 %@",hsbArray3, hsbArray4);
}
因為hsbArray4
第二個元素是hsbArray2
副本,而hsbArray2
是不可變數組,這一步將產生錯誤。注釋掉5部分代碼后,控制臺輸出如下:
Contents of
hsbArray3 (
(
hue,
saturation,
brightness
),
(
hue,
saturation,
brightness
)
),
hsbArray4 (
(
hue,
saturation,
brightness,
hsb
),
(
hue,
saturation,
brightness
)
)
可以看到只有hsbArray4
數組第一個元素內對象發生了改變,所以,使用歸檔、讀取歸檔進行的是完全深復制。
復制集合時,該集合、集合內元素的可變性可能會受到影響。每種方法對任意深度集合中對象的可變性有稍微不同的影響。
-
copyWithZone:
創建對象的最外層 surface level不可變,所有更深層次對象的可變性不變。 -
mutableCopyWithZone:
創建對象的最外層 surface level可變,所有更深層次對象的可變性不變。 -
initWithArray: copyItems:
第二個參數為NO
,此時,所創建數組最外層可變性與初始化的可變性相同,所有更深層級對象可變性不變。 -
initWithArray: copyItems:
第二個參數為YES
,此時,所創建數組最外層可變性與初始化的可變性相同,下一層級是不可變的,所有更深層級對象可變性不變。 - 歸檔、解檔復制的集合,所有層級的可變性與原始對象相同。
2.3 自定義對象的深復制、淺復制
自定義的類需要我們自己實現NSCopying
、NSMutableCopying
協議,這樣才可以調用copy
和mutableCopy
方法。
添加父類為NSObject
,名稱為Person
的類。進入Person.h
,添加以下屬性和方法,同時讓該類遵守NSCopying
協議。
@interface Person : NSObject <NSCopying>
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSUInteger age;
- (void)setName:(NSString *)name withAge:(NSUInteger)age;
@end
NSCopying
協議只有一個必須實現的copyWithZone:
方法。進入Person.m
,實現屬性中setName: withAge:
方法和copyWithZone:
方法。
- (void)setName:(NSString *)name withAge:(NSUInteger)age {
_name = name;
_age = age;
}
- (id)copyWithZone:(NSZone *)zone {
Person *person = [[Person allocWithZone:zone] init];
[person setName:self.name withAge:self.age];
return person;
}
如果Person
類會被繼承,那么copyWithZone:
方法將被繼承,這時應將上面的
Person *person = [[Person allocWithZone:zone] init];
替換為
id person = [[[self class] allocWithZone: zone] init];
這樣,可以從該類分配一個新對象,而這個類是copy
的接收者。例如:如果Person
類有一個名為NewPerson
的子類,那么應該在繼承的方法中分配了新的NewPerson
對象,而不是Person
對象。
如果Person
類的父類也實現了NSCopying
協議,那么應該先調用父類的copy
方法,以復制繼承來的實例變量。如果需要實現可變復制,還需要遵守NSMutableCopying
協議。
進入ViewController.m
,導入Person.h
,在實現部分添加以下方法,并在viewDidLoad
中調用。
// 6.自定義類的復制
- (void)copyCustomClass {
Person *person = [[Person alloc] init];
[person setName:@"A" withAge:1];
Person *personCopy = [person copy];
[personCopy setName:@"B" withAge:2];
// 斷點位置
}
在copyCustomClass
方法最后一行設置斷點,運行demo,可以看到控制臺輸出如下圖:
通過上圖可以看到,person
和personCopy
內存地址不同,且person
和personCopy
中的name
和age
屬性的值各不相同。
3. 修改指針指向
現在看最后一個示例,在ViewController.m
的實現部分添加以下方法,并在viewDidLoad
中調用該方法。
// 7.更改指針指向地址
- (void)pointToAnotherMemoryAddress {
// 1.指針a、b同時指向字符串pro
NSString *a = @"pro";
NSString *b = a;
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 斷點1位置
// 2.指針a指向字符串pro648
a = @"pro648";
NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
// 斷點2位置
}
上述代碼分步說明如下:
- 指針
a
指向字符串pro
內存地址,b = a
表示b
是a
的淺復制,指針b
也指向字符串pro
內存地址。NSLog
語句可以說明這一問題。也可以在注釋斷點1位置所在行設置斷點,查看指針指向的內容。 - 修改指針
a
指向字符串pro648
,此時輸出a
、b
指針所指的向內存地址。并在斷點2位置所在行設置斷點。運行后可以看到控制臺輸出如下:
可以看到,a
、b
指針指向不同內存地址,a
指向字符串pro648
,b
指向字符串pro
。
這是因為
a = @"pro648";
等同于
a = [[NSString alloc] initWithString:@"pro648"];
a = @"pro648"
修改了a
指針指向的內存地址,而b
指針依然指向之前的內存地址。
NSString
與NSMutableString
的區別主要是:NSMutableString
對象所指向內存地址中的內容可以被修改,而NSString
對象所指向內存地址中內容不能被修改,但NSString
對象不是常量,可以通過為NSString
對象重新分配一塊內存來改變其指向的內容。
總結
淺拷貝盡可能少的復制對象,集合的淺拷貝副本只是集合結構的副本,而不是集合內元素的副本。淺拷貝獲得的副本與原始集合共享各個元素。
深拷貝復制一切內容。集合的深拷貝會復制集合的結構和元素,但如果集合內元素也是集合,則涉及到一層深拷貝、完全深拷貝。
Demo名稱:copy&mutableCopy
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: