就 iOS 開發而言,關於 copy 的幾個概念:數組
由上可知,copy 和深拷貝是兩個概念,二者並不必定相等,先給結果:bash
關閉 ARC 的狀況下,先看兩段代碼:app
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str1 = [[NSString alloc] initWithFormat:@"123abcdefghijklmn"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"%zd %zd %zd",str1.retainCount, str2.retainCount, str3.retainCount);
NSLog(@"%p %p %p",str1, str2, str3);
}
return 0;
}
複製代碼
猜一猜打印的結果是什麼?結果以下:iphone
2019-12-26 17:23:23.020793+0800 XKCopyTest[1862:60872367] 2 2 1
2019-12-26 17:23:23.021176+0800 XKCopyTest[1862:60872367] 0x100610460 0x100610460 0x1006103b0
Program ended with exit code: 0
複製代碼
先不看引用計數器,先看看內存地址,從打印結果中能夠看出:函數
NSString
類型,屬於不可變對象;copy
方法表示是不可變拷貝,須要獲得一個不可變副本;由此能夠進一步得出第一個結論:ui
由於str2
地址不變, 指向的仍然是str1
所指向的那個對象,沒有產生新的對象,因此此時的拷貝是淺拷貝;atom
copy
方法等價於retain
由於是淺拷貝,沒有產生新的對象,指針 str2 仍然指向源對象,因此此時copy
方法執行的邏輯等價於retain
,也就是僅僅讓源對象的引用計數器增長了1,因此最終 str1.retainCount
的結果是 2 。由於 str2 指向源對象,因此天然而言的str2.retainCount
的打印結果也是2。spa
這裏須要解釋下如此設計的緣由,就像咱們在使用 PC 文件時進行拷貝同樣,拷貝的本質是要生成一個和源文件相互獨立,互不干擾的副本,說具體點就是兩個文件修改以後不影響另一個文件。由於 str1 是不可變對象,copy
方法生成的也是不可變對象,源對象原本就不可變,因此就不存在源對象被修改的狀況了,因此直接把str2
指向源對象,既能夠實現拷貝的相互獨立,互不干擾的宗旨,還不用生成新的內存,節省內存空間,一箭雙鵰。設計
再來看看 str3
,從打印結果中咱們能夠得出:指針
mutableCopy
表示可變拷貝,須要獲得一個可變的副本;str3
的地址和str1
不相等,證實產生了一個新的對象;由於產生了新的對象,因此str3
中的拷貝操做屬於深拷貝。str3
也就指向了新產生的對象的內存地址,因而乎引用計數器就是 1。而 str1
和str2
指針所指向的對象是相同的,且被str1
和str2
指向(引用),因此最終引用計數器打印結果爲2。
將str1
改爲可變類型,也就是NSMutableString
,代碼以下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"123abcdefghijklmn"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"%zd %zd %zd",str1.retainCount, str2.retainCount, str3.retainCount);
NSLog(@"%p %p %p",str1, str2, str3);
}
return 0;
}
複製代碼
打印結果又是怎麼樣的?結果以下:
2019-12-26 17:29:07.661591+0800 XKCopyTest[1937:60876834] 1 1 1
2019-12-26 17:29:07.661965+0800 XKCopyTest[1937:60876834] 0x10057c600 0x1005086d0 0x100508700
Program ended with exit code: 0
複製代碼
理解了狀況一,再來看這個就比較簡單了,從打印結果和代碼中能夠得出結論:
str2 中的 copy 仍然屬於不可變拷貝,可是源對象是可變對象,因此一定會生成一個新對象,產生了新的對象就屬於內容拷貝,天然就是深拷貝;
mutableCopy
須要生成可變的副本,因此不管源對象是可變對象仍是不可變對象,mutableCopy
方法都會生成一個新的對象,因此一定是深拷貝。
對於 array、dictionary、data,也是同理,本文就再也不贅述。
上文中知道了,拷貝分深拷貝和淺拷貝,那麼@property
中的copy
關鍵字是幹嗎的呢?有沒有 mutablecopy
關鍵字呢?
先說結論:
copy
關鍵字的做用就是調用被賦值給屬性的對象的copyWithZone
方法,並將返回值賦值給屬性;再來看源碼, 首先看一段咱們經常使用的屬性聲明代碼:
@interface XKPerson()
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
@end
@implementation XKPerson
@end
複製代碼
使用編譯指令生成 cpp 文件:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XKPerson.m -o XKPerson.cpp
複製代碼
而後咱們來找找property
最後生成的代碼是怎樣的,cpp 文件中關於屬性的實現代碼如圖所示:
// @interface XKPerson()
// @property (copy, nonatomic) NSString *name;
// @property (assign, nonatomic) NSInteger age;
/* @end */
// @implementation XKPerson
static NSString * _I_XKPerson_name(XKPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_XKPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_XKPerson_setName_(XKPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct XKPerson, _name), (id)name, 0, 1); }
static NSInteger _I_XKPerson_age(XKPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_XKPerson$_age)); }
static void _I_XKPerson_setAge_(XKPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_XKPerson$_age)) = age; }
// @end
複製代碼
也就是說,@property
只是告訴編譯器,幫我生成 setter
和 getter
方法,也就是聲明並實現了四個方法:
這裏,由於咱們在探究屬性中的 copy,並且 copy 只在設置屬性的時候起做用,因此咱們只須要關注 _I_XKPerson_setName_
這個方法便可,其核心是調用了objc_setProperty()
這個函數,那麼咱們來到 objc4 的源碼,下載 源碼後看看objc_setProperty
這個函數作了啥,代碼以下:
#define MUTABLE_COPY 2
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
// copy 和 mutableCopy最多隻有一個爲真(1)
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
複製代碼
其他代碼就不貼了,MUTABLE_COPY
值爲2,而setter
中傳的值爲1,最終會進入到reallySetProperty
這個方法:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
// 修改 isa 指向
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
// copy的邏輯
if (copy) {
// 屬性修飾關鍵字只有 copy
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
// 屬性修飾關鍵字只有 copy , 這裏是實現了 mutableCopying 協議時的處理邏輯
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// 釋放原對象
objc_release(oldValue);
}
複製代碼
其實這段代碼仍是挺經典的,可是咱們只看 copy
的部分:
// copy的邏輯
if (copy) {
// 屬性修飾關鍵字只有 copy ,因此最終會進入到這裏
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
// 屬性修飾關鍵字只有 copy , 這裏是實現了 mutableCopying 協議時的處理邏輯
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
// NSString等不可變對象調用copy,其內部的代碼邏輯會走到這裏來
// 此時 copy 不產生新的對象,屬於淺拷貝,因此 copy 和 retain 的代碼邏輯等價(可是可不能將 copy 關鍵字替換成 retain 哦😯~)
newValue = objc_retain(newValue);
}
複製代碼
從源碼中就一目瞭然了:
copy
方法,則調用對象的 copyWithZone
方法;mutablecopy
,則調用對象的mutableCopyWithZone
方法;copy = 0
,mutablecopy = 0
,那麼最終會調用objc_retain
方法;其中,使用retain
修飾屬性時,就是第三種狀況,代碼中也能夠獲得驗證,修改copy
爲 retain
後編譯的結果:
// 最後一個參數由 1 變成了 0
static void _I_XKPerson_setName_(XKPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct XKPerson, _name), (id)name, 0, 0); }
複製代碼
另外,若是使用 strong
和 assign
修飾,最終 setter 不調用 objc_setProperty
方法而是經過偏移量進行指針賦值或者直接賦值,具體就再也不這裏探討了~~
這裏留個疑問:
若是用 copy 修飾,那麼屬性最終轉化成的 setter 中執行的 objc_setProperty
方法的傳值中,最後一個參數永遠爲1。而objc_setProperty
內部調用的也是reallySetProperty
方法。爲1時,調用reallySetProperty
方法中的參數永遠是bool copy = 1, bool mutablecopy = 0
,也就是會走到copyWithZone
這層邏輯。而使用 retain
修飾屬性,參數bool copy = 0, bool mutablecopy = 0
,會走到obje_retain
這層邏輯,概括以下:
關鍵字 | 參數copy的值 | 參數mutablecopy的值 | 代碼邏輯 |
---|---|---|---|
retain | 0 | 0 | obje_retain() |
copy | 1 | 0 | copyWithZone() |
只有objc_setProperty
最後一個參數爲 2 時,纔會走到 mutableCopy 的邏輯,因此reallySetProperty
方法中的mutablecopyWithZone
的代碼什麼時候會被調用呢???
先說結論,使用 copy 修飾屬性的意義在於:
不但願體如今使用使用 NSString 類型來聲明屬性,這樣若是使用 appendString:
的方法,就報報編譯錯誤。
可是,若是使用 strong 來修飾字符串屬性,加上強制類型轉換,仍然能夠實現直接修改內存地址中的值:
// person對象中使用 strong 修飾 屬性
@property (strong, nonatomic) NSString *name1;
XKPerson *p = [XKPerson new];
NSMutableString *name = [[NSMutableString alloc] initWithFormat:@"%@",@"Jack"];
// 此時若是是用 strong 修飾 name,雖然聲明的是 NSString 對象,但實際類型是 NSMutableString 類型
p.name = name;
[(NSMutableString *)p.name appendFormat:@"1"];
複製代碼
以上代碼就實現了直接修改屬性所指向的內存地址中的值,此時修改爲使用 copy 修飾,由於調用的copyWithZone
,結果返回的一定是不可變類型,因此即便賦值時是NSMutableString
類型,最終獲得的仍然是NSString
類型,這樣就起到了預期的效果;
因此,NSString
類型使用 copy 修飾是最好不過的~~~
這裏還涉及到一個場景,好比咱們開發中,但願字符串屬性跟隨某個字符串對象的值同時改變,這個時候就要使用 strong + NSMutableString了:
@property (strong, nonatomic) NSMutableString *name;
// 使用
XKPerson *p = [XKPerson new];
NSMutableString *name = [NSMutableString stringWithFormat:@"王"];
p.name = name;
NSLog(@"%@",p.name);
[name appendString:@"小二"];
NSLog(@"%@",p.name);
複製代碼
結果:
2019-12-27 17:53:51.730 XKStringTest[10172:63201310] 王
2019-12-27 17:53:51.731 XKStringTest[10172:63201310] 王小二
複製代碼
這個時候使用 copy 修飾反而會崩潰,和使用 copy 修飾 NSMutableArray時,往數組中新增元素時崩潰是一個道理哦😯~~~
上一章節講到了屬性修飾中 copy 關鍵字的本質是調用copyWithZone
方法。之因此可以使用 copy 修飾字符串、數組等,是由於這些系統對象實現了 copy 相關的協議。
因此,這裏就涉及到一個問題:自定義copyWithZone
方法。日常咱們使用 copy 修飾的最多的就是字符串
所以,OC 中給咱們提供了兩個協議:
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
@end
複製代碼
具體實現以下:
- (id)copyWithZone:(NSZone *)zone {
XKPerson *newP = [[XKPerson allocWithZone:zone] init];
newP.name = self.name;
newP.age = self.age;
return newP;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
XKPerson *newP = [[XKPerson allocWithZone:zone] init];
newP.name = self.name;
newP.age = 10;
return newP;
}
複製代碼
使用:
XKPerson *p = [XKPerson new];
p.name = @"Jack";
p.age = 18;
XKPerson *p1 = [p copy];
XKPerson *p2 = [p mutableCopy];
NSLog(@"%p %p %p", p, p1, p2);
p1.name = @"Lucy";
NSLog(@"%@",p1.name);
複製代碼
結果:
2019-12-27 17:42:11.326 XKStringTest[9991:63192515] 0x7ae30b10 0x7ae31340 0x7ae313e0
2019-12-27 17:42:11.327 XKStringTest[9991:63192515] Lucy
複製代碼
copy 在數據模型 model 中時可能會較多使用 copy,此時實現copy
協議便可,但在平時自定義對象使用 copy 並很少,這兩個協議就很少說了~~
略,會在 block進階中講到~~
以上,總結下 copy 的幾個重點,方便記憶:
copyWithZone
和mutableCopyWithZone
方法;