iOS面試之@property

原文鏈接

@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";

編譯注益,不會有任何問題。運行溯捆,也沒問題。但是當(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)在再編譯辙诞,運行,執(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)該都知道這一點,在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)鍵字,或者是修飾符敲才。

修飾符種類

修飾符有四種:

  1. 原子性裹纳。原子性有nonatomic、atomic兩個值紧武,如果不寫nonatomic,那么默認(rèn)是atomic的剃氧。如果屬性是atomic的,那么在訪問其getter和setter方法之前阻星,會有一些判斷朋鞍,大概是判斷是否可以訪問等,這里系統(tǒng)使用的是自旋鎖妥箕。由于使用atomic并不能絕對保證線程安全滥酥,且會耗費一些性能,因此通常情況下都使用nonatomic畦幢。
  2. 讀寫權(quán)限坎吻。讀寫權(quán)限有兩個取值,readwrite和readonly宇葱。聲明屬性時瘦真,如果不指定讀寫權(quán)限,那么默認(rèn)是readwrite的黍瞧。如果某個屬性不想讓其他人來寫诸尽,那么可以設(shè)置成readonly。
  3. 內(nèi)存管理印颤。內(nèi)存管理的取值有assign您机、strong、weak膀哲、copy往产、unsafe_unretained。
  4. 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-06 15:42:26.837215+0800 TestClock[15405:175815] stu.name = aaa
2018-12-06 15:42:26.837837+0800 TestClock[15405:175815] stu.name = bbb

證實了即使使用了atomic,也不能保證線程安全钮莲。

weak和assign區(qū)別

經(jīng)常會有面試題問weak和assign的區(qū)別免钻,這里介紹一下。

weak和strong是對應(yī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;

編譯当宴,沒有問題,運行也沒有問題泽疆。我們再聲明一個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);

運行,執(zhí)行到self.assignBtn的時候崩潰了捌年,崩潰信息是

 EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

weak和assign修飾對象時的差別體現(xiàn)出來了瓢娜。

weak修飾的對象,當(dāng)對象釋放之后礼预,即引用計數(shù)為0時眠砾,對象會置為nil

2018-12-06 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é)點來尋找阔拳,找到第一塊足夠大的空間時,分配空間,返回糊肠。根據(jù)兩者的數(shù)據(jù)結(jié)構(gòu)辨宠,可以推斷,堆空間上是存在碎片的货裹。

回到問題嗤形,為何assign修飾基本數(shù)據(jù)類型沒有野指針的問題?因為這些基本數(shù)據(jù)類型是分配在棧上弧圆,棧上空間的分配和回收都是系統(tǒng)來處理的赋兵,因此開發(fā)者無需關(guān)注,也就不會產(chǎn)生野指針的問題搔预。

棧是線程安全的嘛

擴展一下霹期,棧是線程安全的嘛?回答問題之前拯田,先看一下進(jìn)程和線程的關(guān)系历造。

進(jìn)程和線程的關(guān)系

線程是進(jìn)程的一個實體,是CPU調(diào)度和分派的基本單位船庇。一個進(jìn)程可以擁有多個線程帕膜。線程本身是不配擁有系統(tǒng)資源的,只擁有很少的溢十,運行中必不可少的資源(如程序計數(shù)器、寄存器达吞、棧)张弛。但是線程可以與同屬于一個進(jìn)程的其他線程,共享進(jìn)程所擁有的資源酪劫。一個進(jìn)程中所有的線程共享該進(jìn)程的地址空間吞鸭,但是每個線程有自己獨立的棧,iOS系統(tǒng)中覆糟,每個線程棧的大小是1M刻剥。而堆則不同。堆是進(jìn)程所獨有的滩字,通常一個進(jìn)程有一個堆,這個堆為本進(jìn)程中的所有線程所共享。

棧的線程安全

其實通過上面的介紹郑原,該問題答案已經(jīng)很明顯了:棧是線程安全的砸喻。

堆是多個線程所共有的空間,操作系統(tǒng)在對進(jìn)程進(jìn)行初始化的時候挟裂,會對堆進(jìn)行分配享钞;
棧是每個線程所獨有的,保存線程的運行狀態(tài)和局部變量诀蓉。棧在線程開始的時化栗竖,每個線程的棧是互相獨立的暑脆,因此棧是線程安全的。

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-06 22:02:41.350812+0800 TestClock[884:17969] str = 0x106ec1290
2018-12-06 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-06 22:10:08.457179+0800 TestClock[1000:21900] mutStr = 0x600002100540
2018-12-06 22:10:08.457261+0800 TestClock[1000:21900] after mutStr = 0x600002100540
不可變對象用strong

上面說了,可變對象使用strong斑芜,不可變對象使用copy肩刃。那么,如果不可變對象使用strong來修飾,會有什么問題呢盈包?寫代碼測試一下:

@property (nonatomic, strong) NSString *strongStr;

首先明確一點沸呐,既然類型是NSString,那么則代表我們不希望testStr被改變呢燥,否則直接使用可變對象NSMutableString就可以了崭添。另外需要提醒的一點是,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;

這里還是強調(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ū)別。

image

首先A指向一塊內(nèi)存區(qū)域参萄,現(xiàn)在設(shè)置B = A

image

現(xiàn)在B和A指向了同一塊內(nèi)存區(qū)域卫枝,即為淺拷貝。

再來看深考貝

image

首先A指向一塊內(nèi)存區(qū)域讹挎,現(xiàn)在設(shè)置B = A

image

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-07 13:01:27.525064+0800 TestClock[9357:143436] str1 = 0x60000086d8f0 str2 = 0xc8c1a5736a50d5fe str3 = 0x60000086d9b0
2018-12-07 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-07 13:06:29.439108+0800 TestClock[9442:147133] str1 = 0x1045612b0 str2 = 0x1045612b0 str3 = 0x6000017e4450
2018-12-07 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)都是類似的通孽,不做過多介紹序宦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市背苦,隨后出現(xiàn)的幾起案子互捌,更是在濱河造成了極大的恐慌,老刑警劉巖行剂,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秕噪,死亡現(xiàn)場離奇詭異,居然都是意外死亡厚宰,警方通過查閱死者的電腦和手機腌巾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來固阁,“玉大人壤躲,你說我怎么就攤上這事城菊”溉迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵凌唬,是天一觀的道長并齐。 經(jīng)常有香客問我漏麦,道長,這世上最難降的妖魔是什么况褪? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任撕贞,我火速辦了婚禮,結(jié)果婚禮上测垛,老公的妹妹穿的比我還像新娘捏膨。我一直安慰自己,他們只是感情好食侮,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布号涯。 她就那樣靜靜地躺著,像睡著了一般锯七。 火紅的嫁衣襯著肌膚如雪链快。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天眉尸,我揣著相機與錄音域蜗,去河邊找鬼。 笑死噪猾,一個胖子當(dāng)著我的面吹牛霉祸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畏妖,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脉执,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了戒劫?” 一聲冷哼從身側(cè)響起半夷,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迅细,沒想到半個月后巫橄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡茵典,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年湘换,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片统阿。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡彩倚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扶平,到底是詐尸還是另有隱情帆离,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布结澄,位于F島的核電站哥谷,受9級特大地震影響岸夯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜们妥,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一猜扮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧监婶,春花似錦旅赢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至集惋,卻和暖如春孕似,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刮刑。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工喉祭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雷绢。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓泛烙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翘紊。 傳聞我的和親對象是個殘疾皇子蔽氨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,098評論 1 32
  • 1.設(shè)計模式是什么鹉究? 你知道哪些設(shè)計模式,并簡要敘述踪宠?設(shè)計模式是一種編碼經(jīng)驗自赔,就是用比較成熟的邏輯去處理某一種類型...
    龍飝閱讀 2,148評論 0 12
  • 十九世紀(jì)美國緬因州,在一個貧窮柳琢,偏遠(yuǎn)绍妨,環(huán)境惡劣,仿佛被上帝遺忘的地方圣克勞茲孤兒院中柬脸,拉奇醫(yī)生終其一生都在做著上帝...
    伶壹閱讀 674評論 2 1
  • 霧 悲性的蔓延 行走在霧里 猶如灶臺上煙氣里的灰塵 飄忽不定 我想也許是自己聾了吧 霧氣里只聽得到 自己的心跳 那...
    StoneVolcanic閱讀 199評論 0 0
  • 教學(xué)的事他去,是讓自己心煩的,然后就會影射到其他事情倒堕。以前以為想要追求的事灾测,現(xiàn)在想來也沒什么了,倒不如讓自己過得舒坦涩馆,...
    小樹灰灰閱讀 137評論 0 0