@property介紹
相信做過iOS開發(fā)的同學(xué)都使用過@property嗤锉,@property翻譯過來是屬性沐飘。在定義一個類時匿乃,常常會有多個@property合敦,有了@property初橘,我們可以用來保存類的一些信息或者狀態(tài)。比如定義一個Student類:
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@end
Student類中有兩個屬性蛤肌,分別是name和sex壁却。
在程序中使用時批狱,可以使用
self.name = @"xxx";
self.sex = @"xxx";
那么裸准,為什么可以這樣用呢?self.name是self的name變量嘛赔硫?還是其他的什么炒俱?屬性中的copy代表什么?nonatomic呢爪膊?下面來看一下這些問題的答案权悟。
@property 本質(zhì)
@property到底是什么呢?實際上@property = 實例變量 + get方法 + set方法推盛。也就是說屬性
@property (nonatomic, copy) NSString *name;
代表的有實例變量峦阁,get方法和set方法。如果大家用過Java耘成,相信對set方法和get方法應(yīng)該很熟悉榔昔,這里的set、get方法和Java里面的作用是一樣的瘪菌,get方法用來獲取變量的值撒会,set方法用來設(shè)置變量的值。使用@property生成的實例變量师妙、get方法诵肛、set方法的命名有嚴(yán)格的規(guī)范,實例變量的名稱默穴、get方法名怔檩、set方法名稍后再介紹。
這里需要注意的是蓄诽,包括實例變量珠洗、get方法和set方法,不會真的出現(xiàn)在我們的編輯器里面若专,使用屬性生成的實例變量许蓖、get方法、set方法是在編譯過程中生成的。下面介紹一下set方法膊爪、get方法以及自動生成的實例變量自阱。
setter方法
set方法也可以稱為setter方法,之后看到setter方法直接理解成set方法即可米酬。同理沛豌,get方法也被稱為getter方法。
還是以上面的屬性:
@property (nonatomic, copy) NSString *name;
為例赃额,屬性name生成的setter方法是
- (void)setName:(NSString *)name;
該命名方法是固定的加派,是約定成束的。如果屬性名是firstName跳芳,那么setter方法是:
- (void)setFirstName:(NSString *)firstName;
項目中芍锦,很多時候會有重寫setter方法的需求,只要重寫對應(yīng)的方法即可飞盆。比如說重寫name屬性的setter方法:
- (void)setName:(NSString *)name
{
NSLog(@"rewrite setter");
_name = name;
}
關(guān)于_name是什么娄琉,后續(xù)會介紹。
getter方法
以屬性
@property (nonatomic, copy) NSString *name;
為例吓歇,編譯器自動生成的getter方法是
- (NSString *)name;
getter方法的命名也是固定的孽水。如果屬性名是firstName,那么getter方法是:
- (NSString *)firstName;
重寫getter方法:
- (NSString *)name
{
NSLog(@"rewrite getter");
return _name;
}
如果我們定義了name屬性城看,并且按照上面所述妙同,重寫了getter方法和setter方法艇劫,Xcode會提示如下的錯誤:
Use of undeclared identifier '_name'; did you mean 'name'?
稍后我們再解釋為何會有該錯誤衣摩,以及如何解決工育。先來看一下_name到底是什么。
實例變量
既然@property = 實例變量 + getter + setter鹃愤,那么屬性所生成的實例變量名是什么呢簇搅?根據(jù)上面的例子,也很容易猜到软吐,項目中也經(jīng)常使用瘩将,實例變量的名稱就是_name。實例變量的命名也是有固定格式的凹耙,下劃線+屬性名姿现。如果屬性是@property firstName,那么生成的實例變量就是_firstName肖抱。這也是為何我們在setter方法和getter方法备典,以及其他的方法中可以使用_name的原因。
這里再提一下意述,無論是實例變量提佣,還是setter吮蛹、getter方法,命名都是有嚴(yán)格規(guī)范的拌屏。正是因為有了這種規(guī)范潮针,編譯器才能夠自動生成方法,這也要求我們在項目中倚喂,對變量的命名每篷,方法的命名遵循一定的規(guī)范。
自動合成
定義一個@property端圈,在編譯期間焦读,編譯器會生成實例變量、getter方法舱权、setter方法矗晃,這些方法、變量是通過自動合成(autosynthesize)的方式生成并添加到類中刑巧。實際上喧兄,一個類經(jīng)過編譯后无畔,會生成變量列表ivar_list啊楚,方法列表method_list,每添加一個屬性浑彰,在變量列表ivar_list會添加對應(yīng)的變量恭理,如_name,方法列表method_list中會添加對應(yīng)的setter方法和getter方法郭变。
動態(tài)合成
既然有自動合成颜价,那么相對應(yīng)的就要有非自動合成,非自動合成又稱為動態(tài)合成诉濒。定義一個屬性周伦,默認(rèn)是自動合成的,默認(rèn)會生成getter方法和setter方法未荒,這也是為何我們可以直接使用self.屬性名的原因专挪。實際上,自動合成對應(yīng)的代碼是:
@synthesize name = _name;
這行代碼是編譯器自動生成的片排,無需我們來寫寨腔。相應(yīng)的,如果我們想要動態(tài)合成率寡,需要自己寫如下代碼:
@dynamic sex;
這樣代碼就告訴編譯器迫卢,sex屬性的變量名、getter方法冶共、setter方法由開發(fā)者自己來添加乾蛤,編譯器無需處理每界。
那么這樣寫和自動合成有什么區(qū)別呢?來看下面的代碼:
Student *stu = [[Student alloc] init];
stu.sex = @"male";
編譯家卖,不會有任何問題盆犁。運(yùn)行,也沒問題篡九。但是當(dāng)代碼執(zhí)行到這一行的時候谐岁,程序崩潰了,崩潰信息是:
[Student setSex:]: unrecognized selector sent to instance 0x60000217f1a0
即:Student沒有setSex方法榛臼,沒有屬性sex的setter方法伊佃。這就是動態(tài)合成和自動合成的區(qū)別。動態(tài)合成沛善,需要開發(fā)者自己來寫屬性的setter方法和getter方法航揉。添加上setter方法:
- (void)setSex:(NSString *)sex
{
_sex = sex;
}
由于使用@dynamic,編譯器不會自動生成變量金刁,因此除此之外帅涂,還需要手動定義_sex變量,如下:
@interface Student : NSObject
{
NSString *_sex;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@end
現(xiàn)在再編譯尤蛮,運(yùn)行媳友,執(zhí)行沒有錯誤和崩潰。
重寫setter产捞、getter方法的注意事項
上面的例子中醇锚,重寫了屬性name的getter方法和setter方法,如下:
- (void)setName:(NSString *)name
{
NSLog(@"rewrite setter");
_name = name;
}
- (NSString *)name
{
NSLog(@"rewrite getter");
return _name;
}
但是編譯器會提示錯誤坯临,錯誤信息如下:
Use of undeclared identifier '_name'; did you mean 'name'?
提示沒有_name變量焊唬。為什么呢?我們沒有聲明@dynamic看靠,那默認(rèn)就是@autosynthesize赶促,為何沒有_name變量呢?奇怪的是挟炬,倘若我們把getter方法鸥滨,或者setter方法注釋掉,gettter辟宗、setter方法只留下一個爵赵,不會有錯誤,為什么呢泊脐?
還是編譯器做了些處理空幻。對于一個可讀寫的屬性來說,當(dāng)我們重寫了其setter容客、getter方法時秕铛,編譯器會認(rèn)為開發(fā)者想手動管理@property约郁,此時會將@property作為@dynamic來處理,因此也就不會自動生成變量但两。解決方法鬓梅,顯示的將屬性和一個變量綁定:
@synthesize name = _name;
這樣就沒問題了。如果一個屬性是只讀的谨湘,重寫了其getter方法時绽快,編譯器也會認(rèn)為該屬性是@dynamic,關(guān)于可讀寫紧阔、只讀坊罢,下面會介紹。這里提醒一下擅耽,當(dāng)項目中重寫了屬性的getter方法和setter方法時活孩,注意下是否有編譯的問題。
修改實例變量的名稱
使用自動合成時乖仇,針對
@property (nonatomic, copy) NSString *name;
屬性憾儒,生成的變量名是_name。倘若乃沙,不習(xí)慣使用下劃線開頭的變量名起趾,能否指定屬性對應(yīng)的變量名呢?答案是可以的崔涂,使用的是上面介紹過的@synthesize關(guān)鍵字阳掐。如下:
@synthesize name = stuName;
這樣始衅,name屬性生成的變量名就是stuName,后續(xù)使用時需要寫stuName,而不是_name冷蚂。如getter、setter方法:
- (void)setName:(NSString *)name
{
NSLog(@"rewrite setter");
stuName = name;
}
- (NSString *)name
{
NSLog(@"rewrite getter");
return stuName;
}
注意:雖然可以使用@synthesize關(guān)鍵字修改變量名汛闸,但是如無特殊需求蝙茶,不建議這樣做。因為默認(rèn)情況下編譯器已經(jīng)為我們生成了變量名诸老,大多數(shù)的項目隆夯、開發(fā)者也都會遵循這樣的規(guī)范,既然蘋果已經(jīng)定義了一個好的規(guī)范别伏,為什么不遵守呢蹄衷?
getter方法中為何不能用self.
有經(jīng)驗的開發(fā)者應(yīng)該都知道這一點(diǎn),在getter方法中是不能使用self.的厘肮,比如:
- (NSString *)name
{
NSLog(@"rewrite getter");
return self.name; // 錯誤的寫法愧口,會造成死循環(huán)
}
原因代碼注釋中已經(jīng)寫了,這樣會造成死循環(huán)类茂。這里需要注意的是:self.name實際上就是執(zhí)行了屬性name的getter方法耍属,getter方法中又調(diào)用了self.name托嚣, 會一直遞歸調(diào)用,直到程序崩潰厚骗。通常程序中使用:
self.name = @"aaa";
這樣的方式示启,setter方法會被調(diào)用。
@property修飾符
當(dāng)我們定義一個字符串屬性時领舰,通常我們會這樣寫:
@property (nonatomic, copy) NSString *name;
當(dāng)我們定義一個NSMutableArray類型的屬性時夫嗓,通常我們會這樣寫:
@property (nonatomic, strong) NSMutableArray *books;
而當(dāng)我們定一個基本數(shù)據(jù)類型時,會這樣寫:
@property (nonatomic, assign) int age;
定義一個屬性時冲秽,nonatomic啤月、copy、strong劳跃、assign等被稱作是關(guān)鍵字谎仲,或者是修飾符。
修飾符種類
修飾符有四種:
- 原子性刨仑。原子性有nonatomic郑诺、atomic兩個值,如果不寫nonatomic,那么默認(rèn)是atomic的杉武。如果屬性是atomic的辙诞,那么在訪問其getter和setter方法之前,會有一些判斷轻抱,大概是判斷是否可以訪問等飞涂,這里系統(tǒng)使用的是自旋鎖。由于使用atomic并不能絕對保證線程安全祈搜,且會耗費(fèi)一些性能较店,因此通常情況下都使用nonatomic。
- 讀寫權(quán)限容燕。讀寫權(quán)限有兩個取值梁呈,readwrite和readonly。聲明屬性時蘸秘,如果不指定讀寫權(quán)限官卡,那么默認(rèn)是readwrite的。如果某個屬性不想讓其他人來寫醋虏,那么可以設(shè)置成readonly寻咒。
- 內(nèi)存管理。內(nèi)存管理的取值有assign颈嚼、strong毛秘、weak、copy粘舟、unsafe_unretained熔脂。
- set佩研、get方法名。如果不想使用自動合成所生成的setter霞揉、getter方法旬薯,聲明屬性時甚至可以指定方法名。比如指定getter方法名:
@property (nonatomic, assign, getter=isPass) BOOL pass;
屬性pass的getter方法就是
- (BOOL)isPass;
默認(rèn)修飾符
聲明屬性時适秩,如果不顯示指定修飾符绊序,那么默認(rèn)的修飾符是哪些呢?或者說未指定的修飾符秽荞,默認(rèn)取值是什么呢骤公?如果是基本數(shù)據(jù)類型,默認(rèn)取值是:
atomic,readwrite,assign
如果是Objective-C對象扬跋,默認(rèn)取值是:
atomic,readwrite,strong
atomic是否是線程安全的
上面提到了阶捆,聲明屬性時,通常使用nonatomic修飾符钦听,原因就是因為atomic并不能保證絕對的線程安全洒试。舉例來說,假設(shè)有一個線程A在不斷的讀取屬性name的值朴上,同時有一個線程B修改了屬性name的值垒棋,那么即使屬性name是atomic,線程A讀到的仍舊是修改后的值痪宰,可見不是線程安全的叼架。如果想要實現(xiàn)線程安全,需要手動的實現(xiàn)鎖衣撬。下面是一段示例代碼:
聲明name屬性乖订,使用atomic修飾符
@property (atomic, copy) NSString *name;
對屬性name賦值。同時淮韭,一個線程在不斷的讀取name的值垢粮,另一個線程在不斷的設(shè)置name的值:
stu.name = @"aaa";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for(int i = 0 ; i < 1000; ++i){
NSLog(@"stu.name = %@",stu.name);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
stu.name = @"bbb";
});
看一下輸出:
2018-12-16 15:42:26.837215+0800 TestClock[15405:175815] stu.name = aaa
2018-12-16 15:42:26.837837+0800 TestClock[15405:175815] stu.name = bbb
證實了即使使用了atomic,也不能保證線程安全。
weak和assign區(qū)別
經(jīng)常會有面試題問weak和assign的區(qū)別靠粪,這里介紹一下。
weak和strong是對應(yīng)的毫蚓,一個是強(qiáng)引用占键,一個是弱引用。weak和assign的區(qū)別主要是體現(xiàn)在兩者修飾OC對象時的差異元潘。上面也介紹過畔乙,assign通常用來修飾基本數(shù)據(jù)類型,如int翩概、float牲距、BOOL等返咱,weak用來修飾OC對象,如UIButton牍鞠、UIView等咖摹。
基本數(shù)據(jù)類型用weak來修飾
假設(shè)聲明一個int類型的屬性,但是用weak來修飾难述,會發(fā)生什么呢萤晴?
@property (nonatomic, weak) int age;
Xcode會直接提示錯誤,錯誤信息如下:
Property with 'weak' attribute must be of object type
也就是說胁后,weak只能用來修飾對象店读,不能用來修飾基本數(shù)據(jù)類型,否則會發(fā)生編譯錯誤攀芯。
對象使用assign來修飾
假設(shè)聲明一個UIButton類型的屬性屯断,但是用assign來修飾,會發(fā)生什么呢侣诺?
@property (nonatomic, assign) UIButton *assignBtn;
編譯裹纳,沒有問題,運(yùn)行也沒有問題紧武。我們再聲明一個UIButton,使用weak來修飾剃氧,對比一下:
@interface ViewController ()
@property (nonatomic, assign) UIButton *assignBtn;
@property (nonatomic, weak) UIButton *weakButton;
@end
正常初始化兩個button:
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100,100,100,100)];
[btn setTitle:@"Test" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor lightGrayColor];
self.assignBtn = btn;
self.weakButton = btn;
此時打印兩個button,沒有區(qū)別阻星。釋放button:
btn = nil;
釋放之后打印self.weakBtn和self.assignBtn
NSLog(@"self.weakBtn = %@",self.weakButton);
NSLog(@"self.assignBtn = %@",self.assignBtn);
運(yùn)行朋鞍,執(zhí)行到self.assignBtn的時候崩潰了,崩潰信息是
EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
weak和assign修飾對象時的差別體現(xiàn)出來了妥箕。
weak修飾的對象滥酥,當(dāng)對象釋放之后,即引用計數(shù)為0時畦幢,對象會置為nil
2018-12-16 16:17:05.774298+0800 TestClock[15863:192570] self.weakBtn = (null)
而向nil發(fā)送消息是沒有問題的坎吻,不會崩潰。
assign修飾的對象宇葱,當(dāng)對象釋放之后瘦真,即引用計數(shù)為0時,對象會變?yōu)橐爸羔樖蚯疲恢乐赶蚰闹罹。傧蛟搶ο蟀l(fā)消息,非常容易崩潰印颤。
因此您机,當(dāng)屬性類型是對象時,不要使用assign,會帶來一些風(fēng)險际看。
堆和棧
上面說到咸产,屬性用assign修飾,當(dāng)被釋放后仲闽,容易變?yōu)橐爸羔樐砸纾菀讕肀罎栴},那么蔼囊,為何基本數(shù)據(jù)類型可以用assign來修飾呢焚志?這就涉及到堆和棧的問題。
相對來說畏鼓,堆的空間大酱酬,通常是不連續(xù)的結(jié)構(gòu),使用鏈表結(jié)構(gòu)云矫。使用堆中的空間膳沽,需要開發(fā)者自己去釋放。OC中的對象让禀,如 UIButton 挑社、UILabel ,[[UIButton alloc] init] 出來的巡揍,都是分配在堆空間上痛阻。
棧的空間小,約1M左右腮敌,是一段連續(xù)的結(jié)構(gòu)阱当。棧中的空間,開發(fā)者不需要管糜工,系統(tǒng)會幫忙處理弊添。iOS開發(fā) 中 int、float等變量分配內(nèi)存時是在棧上捌木。如果椨桶樱空間使用完,會發(fā)生棧溢出的錯誤刨裆。
由于堆澈圈、棧結(jié)構(gòu)的差異,棧和堆分配空間時的尋址方式也是不一樣的崔拥。因為棧是連續(xù)的控件极舔,所以棧在分配空間時,會直接在未使用的空間中分配一段出來链瓦,供程序使用;如果剩下的空間不夠大,直接棧溢出慈俯;堆是不連續(xù)的渤刃,堆尋找合適空間時,是順著鏈表結(jié)點(diǎn)來尋找贴膘,找到第一塊足夠大的空間時卖子,分配空間,返回刑峡。根據(jù)兩者的數(shù)據(jù)結(jié)構(gòu)洋闽,可以推斷,堆空間上是存在碎片的突梦。
回到問題诫舅,為何assign修飾基本數(shù)據(jù)類型沒有野指針的問題?因為這些基本數(shù)據(jù)類型是分配在棧上宫患,棧上空間的分配和回收都是系統(tǒng)來處理的刊懈,因此開發(fā)者無需關(guān)注,也就不會產(chǎn)生野指針的問題娃闲。
棧是線程安全的嘛
擴(kuò)展一下虚汛,棧是線程安全的嘛?回答問題之前皇帮,先看一下進(jìn)程和線程的關(guān)系卷哩。
進(jìn)程和線程的關(guān)系
線程是進(jìn)程的一個實體,是CPU調(diào)度和分派的基本單位属拾。一個進(jìn)程可以擁有多個線程将谊。線程本身是不配擁有系統(tǒng)資源的,只擁有很少的捌年,運(yùn)行中必不可少的資源(如程序計數(shù)器瓢娜、寄存器、棧)礼预。但是線程可以與同屬于一個進(jìn)程的其他線程眠砾,共享進(jìn)程所擁有的資源。一個進(jìn)程中所有的線程共享該進(jìn)程的地址空間托酸,但是每個線程有自己獨(dú)立的棧褒颈,iOS系統(tǒng)中,每個線程棧的大小是1M励堡。而堆則不同谷丸。堆是進(jìn)程所獨(dú)有的,通常一個進(jìn)程有一個堆应结,這個堆為本進(jìn)程中的所有線程所共享刨疼。
棧的線程安全
其實通過上面的介紹泉唁,該問題答案已經(jīng)很明顯了:棧是線程安全的。
堆是多個線程所共有的空間揩慕,操作系統(tǒng)在對進(jìn)程進(jìn)行初始化的時候亭畜,會對堆進(jìn)行分配; 棧是每個線程所獨(dú)有的迎卤,保存線程的運(yùn)行狀態(tài)和局部變量拴鸵。棧在線程開始的時化,每個線程的棧是互相獨(dú)立的蜗搔,因此棧是線程安全的劲藐。
copy、strong樟凄、mutableCopy
屬性修飾符中聘芜,還有一個經(jīng)常被問到的面試題是copy和strong。什么時候用copy不同,為什么厉膀?什么時候用strong,為什么?以及mutableCopy又是什么二拐?這一節(jié)介紹一下這些內(nèi)容服鹅。
copy和strong
首先看一下copy和strong,copy和strong的區(qū)別也是面試中出現(xiàn)頻率最高的百新。之前舉得例子中其實已經(jīng)出現(xiàn)了copy和strong:
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, strong) NSMutableArray *books;
通常情況下企软,不可變對象屬性修飾符使用copy,可變對象屬性修飾符使用strong饭望。
可變對象和不可變對象
Objective-C中存在可變對象和不可變對象的概念仗哨。像NSArray、NSDictionary铅辞、NSString這些都是不可變對象厌漂,像NSMutableArray、NSMutableDictionary斟珊、NSMutableString這些是可變對象苇倡。可變對象和不可變對象的區(qū)別是囤踩,不可變對象的值一旦確定就不能再修改旨椒。下面看個例子來說明。
- (void)testNotChange
{
NSString *str = @"123";
NSLog(@"str = %p",str);
str = @"234";
NSLog(@"after str = %p",str);
}
NSString是不可變對象堵漱。雖然在程序中修改了str的值综慎,但是此處的修改實際上是系統(tǒng)重新分配了空間,定義了字符串勤庐,然后str重新指向了一個新的地址示惊。這也是為何修改之后地址不一致的原因:
2018-12-16 22:02:41.350812+0800 TestClock[884:17969] str = 0x106ec1290
2018-12-16 22:02:41.350919+0800 TestClock[884:17969] after str = 0x106ec12d0
再來看可變對象的例子:
- (void)testChangeAble
{
NSMutableString *mutStr = [NSMutableString stringWithString:@"abc"];
NSLog(@"mutStr = %p",mutStr);
[mutStr appendString:@"def"];
NSLog(@"after mutStr = %p",mutStr);
}
NSMutableString是可變對象好港。程序中改變了mutStr的值,且修改前后mutStr的地址一致:
2018-12-16 22:10:08.457179+0800 TestClock[1000:21900] mutStr = 0x600002100540
2018-12-16 22:10:08.457261+0800 TestClock[1000:21900] after mutStr = 0x600002100540
不可變對象用strong
上面說了涝涤,可變對象使用strong媚狰,不可變對象使用copy岛杀。那么阔拳,如果不可變對象使用strong來修飾,會有什么問題呢类嗤?寫代碼測試一下:
@property (nonatomic, strong) NSString *strongStr;
首先明確一點(diǎn)糊肠,既然類型是NSString,那么則代表我們不希望testStr被改變遗锣,否則直接使用可變對象NSMutableString就可以了货裹。另外需要提醒的一點(diǎn)是,NSMutableString是NSString的子類精偿,對繼承了解的應(yīng)該都知道弧圆,子類是可以用來初始化父類的。
介紹完之后笔咽,來看一段代碼搔预。
- (void)testStrongStr
{
NSString *tempStr = @"123";
NSMutableString *mutString = [NSMutableString stringWithString:tempStr];
self.strongStr = mutString; // 子類初始化父類
NSLog(@"self str = %p mutStr = %p",self.strongStr,mutString); // 兩者指向的地址是一樣的
[mutString insertString:@"456" atIndex:0];
NSLog(@"self str = %@ mutStr = %@",self.strongStr,mutString); // 兩者的值都會改變,不可變對象的值被改變
}
注意:我們定義的不可變對象strongStr,在開發(fā)者無感知的情況下被篡改了叶组。所謂無感知拯田,是因為開發(fā)者沒有顯示的修改strongStr的值,而是再修改其他變量的值時甩十,strongStr被意外的改變船庇。這顯然不是我們想得到的,而且也是危險的侣监。項目中出現(xiàn)類似的bug時鸭轮,通常都很難定位。這就是不可變對象使用strong修飾所帶來的風(fēng)險橄霉。
可變對象用copy
上面說了不可變對象使用strong的問題窃爷,那么可變對象使用copy有什么問題呢?還是寫代碼來驗證一下:
@property (nonatomic, copy) NSMutableString *mutString;
這里還是強(qiáng)調(diào)一下酪劫,既然屬性類型是可變類型吞鸭,說明我們期望再程序中能夠改變mutString的值,否則直接使用NSString了覆糟。
看一下測試代碼:
- (void)testStrCopy
{
NSString *str = @"123";
self.mutString = [NSMutableString stringWithString:str];
NSLog(@"str = %p self.mutString = %p",str,self.mutString); // 兩者的地址不一樣
[self.mutString appendString:@"456"]; // 會崩潰刻剥,因為此時self.mutArray是NSString類型,是不可變對象
}
執(zhí)行程序后滩字,會崩潰造虏,崩潰原因是:
[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xed877425eeef9883
即 self.mutString沒有appendString方法御吞。self.mutString是NSMutableString類型,為何沒有appendString方法呢漓藕?這就是使用copy造成的陶珠。看一下
self.mutString = [NSMutableString stringWithString:str];
這行代碼到底發(fā)生了什么享钞。這行代碼實際上完成了兩件事:
// 首先聲明一個臨時變量
NSMutableString *tempString = [NSMutableString stringWithString:str];
// 將該臨時變量copy揍诽,賦值給self.mutString
self.mutString = [tempString copy];
注意,通過[tempString copy]得到的self.mutString是一個不可變對象栗竖,不可變對象自然沒有appendString方法暑脆,這也是為何會崩潰的原因。
copy和mutableCopy
另外常用來做對比的是copy和mutableCopy狐肢。copy和mutableCopy之間的差異主要和深拷貝和淺拷貝有關(guān)添吗,先看一下深拷貝、淺拷貝的概念份名。
深拷貝碟联、淺拷貝
所謂淺拷貝,在Objective-C中可以理解為引用計數(shù)加1僵腺,并沒有申請新的內(nèi)存區(qū)域鲤孵,只是另外一個指針指向了該區(qū)域。深拷貝正好相反想邦,深拷貝會申請新的內(nèi)存區(qū)域裤纹,原內(nèi)存區(qū)域的引用計數(shù)不變∩ッ唬看圖來說明深拷貝和淺拷貝的區(qū)別鹰椒。
首先A指向一塊內(nèi)存區(qū)域,現(xiàn)在設(shè)置B = A
現(xiàn)在B和A指向了同一塊內(nèi)存區(qū)域呕童,即為淺拷貝漆际。
再來看深考貝
首先A指向一塊內(nèi)存區(qū)域,現(xiàn)在設(shè)置B = A
A和B指向的不是同一塊內(nèi)存區(qū)域夺饲,只是這兩塊內(nèi)存區(qū)域中的內(nèi)容是一樣的奸汇,即為深拷貝。
可變對象的copy往声、mutableCopy
可變對象的copy和mutableCopy都是深拷貝擂找。以可變對象NSMutableString和NSMutableArray為例,測試代碼:
- (void)testMutableCopy
{
NSMutableString *str1 = [NSMutableString stringWithString:@"abc"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"a",@"b", nil];
NSArray *array2 = [array1 copy];
NSMutableArray *array3 = [array1 mutableCopy];
NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
輸出結(jié)果:
2018-12-17 13:01:27.525064+0800 TestClock[9357:143436] str1 = 0x60000086d8f0 str2 = 0xc8c1a5736a50d5fe str3 = 0x60000086d9b0
2018-12-17 13:01:27.525198+0800 TestClock[9357:143436] array1 = 0x600000868000 array2 = 0x60000067e5a0 array3 = 0x600000868030
可以看到浩销,只要是可變對象贯涎,無論是集合對象,還是非集合對象慢洋,copy和mutableCopy都是深拷貝塘雳。
不可變對象的copy陆盘、mutableCopy
不可變對象的copy是淺拷貝,mutableCopy是深拷貝败明。以NSString和NSArray為例隘马,測試代碼如下:
- (void)testCopy
{
NSString *str1 = @"123";
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
NSArray *array1 = @[@"1",@"2"];
NSArray *array2 = [array1 copy];
NSMutableArray *array3 = [array1 mutableCopy];
NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
輸出結(jié)果:
2018-12-17 13:06:29.439108+0800 TestClock[9442:147133] str1 = 0x1045612b0 str2 = 0x1045612b0 str3 = 0x6000017e4450
2018-12-17 13:06:29.439236+0800 TestClock[9442:147133] array1 = 0x6000019f5c80 array2 = 0x6000019f5c80 array3 = 0x6000017e1170
可以看到,只要是不可變對象妻顶,無論是集合對象酸员,還是非集合對象,copy都是淺拷貝盈包,mutableCopy都是深拷貝沸呐。
自定義對象如何支持copy方法
項目開發(fā)中經(jīng)常會有自定義對象的需求,那么自定義對象是否可以copy呢呢燥?如何支持copy?
自定義對象可以支持copy方法寓娩,我們所需要做的是:自定義對象遵守NSCopying協(xié)議叛氨,且實現(xiàn)copyWithZone方法。NSCopying協(xié)議是系統(tǒng)提供的棘伴,直接使用即可寞埠。
遵守NSCopying協(xié)議:
@interface Student : NSObject <NSCopying>
{
NSString *_sex;
}
@property (atomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) int age;
@end
實現(xiàn)CopyWithZone方法:
- (instancetype)initWithName:(NSString *)name age:(int)age sex:(NSString *)sex
{
if(self = [super init]){
self.name = name;
_sex = sex;
self.age = age;
}
return self;
}
- (instancetype)copyWithZone:(NSZone *)zone
{
// 注意,copy的是自己焊夸,因此使用自己的屬性
Student *stu = [[Student allocWithZone:zone] initWithName:self.name age:self.age sex:_sex];
return stu;
}
測試代碼:
- (void)testStudent
{
Student *stu1 = [[Student alloc] initWithName:@"Wang" age:18 sex:@"male"];
Student *stu2 = [stu1 copy];
NSLog(@"stu1 = %p stu2 = %p",stu1,stu2);
}
輸出結(jié)果:
stu1 = 0x600003a41e60 stu2 = 0x600003a41fc0
這里是一個深拷貝仁连,根據(jù)copyWithZone方法的實現(xiàn),應(yīng)該很容易明白為何是深拷貝阱穗。
除了NSCopying協(xié)議和copyWithZone方法饭冬,對應(yīng)的還有NSMutableCopying協(xié)議和mutableCopyWithZone方法,實現(xiàn)都是類似的揪阶,不做過多介紹昌抠。