weak和assign區別
經常會有面試題問weak和assign的區別,這里介紹一下。
weak和strong是對應的,一個是強引用,一個是弱引用。weak和assign的區別主要是體現在兩者修飾OC對象時的差異。上面也介紹過,assign通常用來修飾基本數據類型,如int、float、BOOL等,weak用來修飾OC對象,如UIButton、UIView等。
基本數據類型用weak來修飾
假設聲明一個int類型的屬性,但是用weak來修飾,會發生什么呢?
@property (nonatomic, weak) int age;
復制代碼
Xcode會直接提示錯誤,錯誤信息如下:
Property with 'weak' attribute must be of object type
復制代碼
也就是說,weak只能用來修飾對象,不能用來修飾基本數據類型,否則會發生編譯錯誤。
對象使用assign來修飾
假設聲明一個UIButton類型的屬性,但是用assign來修飾,會發生什么呢?
@property (nonatomic, assign) UIButton *assignBtn;
復制代碼
編譯,沒有問題,運行也沒有問題。我們再聲明一個UIButton,使用weak來修飾,對比一下:
1 @interface ViewController () 2 3 @property (nonatomic, assign) UIButton *assignBtn; 4 5 @property (nonatomic, weak) UIButton *weakButton; 6 7 @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; NSLog(@"這時候打印self.assignBtn,self.weakButton 此時打印兩個button,沒有區別"); //如果加上如下 btn = nil
btn = nil; NSLog(@"這時候打印self.assignBtn,self.weakButton 就會crash問題");
NSLog(@"self.weakBtn = %@",self.weakButton);
NSLog(@"self.assignBtn = %@",self.assignBtn);
釋放之后打印self.weakBtn和self.assignBtn
1 NSLog(@"self.weakBtn = %@",self.weakButton); 2 NSLog(@"self.assignBtn = %@",self.assignBtn);
運行,執行到self.assignBtn的時候崩潰了,崩潰信息是
EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
復制代碼
weak和assign修飾對象時的差別體現出來了。
weak修飾的對象,當對象釋放之后,即引用計數為0時,指針變量同時也會置為nil
2018-12-06 16:17:05.774298+0800 TestClock[15863:192570] self.weakBtn = (null)
而向nil發送消息是沒有問題的,不會崩潰。
assign修飾的對象,當對象釋放之后,即引用計數為0時,指針變量並不會同時置為nil,全局變量就是變為野指針,不知道指向哪,再向該對象發消息,非常容易崩潰。
因此,當屬性類型是對象時,不要使用assign,會帶來一些風險。
堆和棧
上面說到,屬性用assign修飾,當被釋放后,容易變為野指針,容易帶來崩潰問題,那么,為何基本數據類型可以用assign來修飾呢?這就涉及到堆和棧的問題。
相對來說,堆的空間大,通常是不連續的結構,使用鏈表結構。使用堆中的空間,需要開發者自己去釋放。OC中的對象,如 UIButton 、UILabel ,[[UIButton alloc] init] 出來的,都是分配在堆空間上。
棧的空間小,約1M左右,是一段連續的結構。棧中的空間,開發者不需要管,系統會幫忙處理。iOS開發 中 int、float等變量分配內存時是在棧上。如果棧空間使用完,會發生棧溢出的錯誤。
由於堆、棧結構的差異,棧和堆分配空間時的尋址方式也是不一樣的。因為棧是連續的控件,所以棧在分配空間時,會直接在未使用的空間中分配一段出來,供程序使用;如果剩下的空間不夠大,直接棧溢出;堆是不連續的,堆尋找合適空間時,是順着鏈表結點來尋找,找到第一塊足夠大的空間時,分配空間,返回。根據兩者的數據結構,可以推斷,堆空間上是存在碎片的。
回到問題,為何assign修飾基本數據類型沒有野指針的問題?因為這些基本數據類型(變量和值)是分配在棧上,棧上空間的分配和回收都是系統來處理的,因此開發者無需關注,也就不會產生野指針的問題。
棧是線程安全的嘛
擴展一下,棧是線程安全的嘛?回答問題之前,先看一下進程和線程的關系。
進程和線程的關系
線程是進程的一個實體,是CPU調度和分派的基本單位。一個進程可以擁有多個線程。線程本身是不配擁有系統資源的,只擁有很少的,運行中必不可少的資源(如程序計數器、寄存器、棧)。但是線程可以與同屬於一個進程的其他線程,共享進程所擁有的資源。一個進程中所有的線程共享該進程的地址空間,但是
每個線程有自己獨立的棧,iOS系統中,每個線程棧的大小是1M。而堆則不同。堆是進程所獨有的,通常一個進程有一個堆,這個堆為本進程中的所有線程所共享。
棧的線程安全
其實通過上面的介紹,該問題答案已經很明顯了:
棧是線程安全的。
堆是多個線程所共有的空間,操作系統在對進程進行初始化的時候,會對堆進行分配; 棧是每個線程所獨有的,保存線程的運行狀態和局部變量。棧在線程開始的時化,每個線程的棧是互相獨立的,因此棧是線程安全的。
copy、strong、mutableCopy
屬性修飾符中,還有一個經常被問到的面試題是copy和strong。什么時候用copy,為什么?什么時候用strong,為什么?以及mutableCopy又是什么?這一節介紹一下這些內容。
copy和strong
首先看一下copy和strong,copy和strong的區別也是面試中出現頻率最高的。之前舉得例子中其實已經出現了copy和strong:
1 @property (nonatomic, copy) NSString *sex; 2 3 @property (nonatomic, strong) NSMutableArray *books;
通常情況下,不可變對象屬性修飾符使用copy,可變對象屬性修飾符使用strong。
可變對象和不可變對象
Objective-C中存在可變對象和不可變對象的概念。像NSArray、NSDictionary、NSString這些都是不可變對象,像NSMutableArray、NSMutableDictionary、NSMutableString這些是可變對象。可變對象和不可變對象的區別是,不可變對象的值一旦確定就不能再修改。下面看個例子來說明。
1 - (void)testNotChange 2 { 3 NSString *str = @"123"; 4 NSLog(@"str = %p",str); 5 str = @"234"; 6 NSLog(@"after str = %p",str); 7 }
復制代碼
NSString是不可變對象。雖然在程序中修改了str的值,但是此處的修改實際上是系統重新分配了空間,定義了字符串,然后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
再來看可變對象的例子:
1 - (void)testChangeAble 2 { 3 NSMutableString *mutStr = [NSMutableString stringWithString:@"abc"]; 4 NSLog(@"mutStr = %p",mutStr); 5 [mutStr appendString:@"def"]; 6 NSLog(@"after mutStr = %p",mutStr); 7 }
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的子類,對繼承了解的應該都知道,子類是可以用來初始化父類的。
介紹完之后,來看一段代碼。
1 - (void)testStrongStr 2 { 3 NSString *tempStr = @"123"; 4 NSMutableString *mutString = [NSMutableString stringWithString:tempStr]; 5 self.strongStr = mutString; // 子類初始化父類 6 NSLog(@"self str = %p mutStr = %p",self.strongStr,mutString); // 兩者指向的地址是一樣的 7 [mutString insertString:@"456" atIndex:0]; 8 NSLog(@"self str = %@ mutStr = %@",self.strongStr,mutString); // 兩者的值都會改變,不可變對象的值被改變 9 }
注意:**我們定義的不可變對象strongStr,在開發者無感知的情況下被篡改了。**所謂無感知,是因為開發者沒有顯示的修改strongStr的值,而是再修改其他變量的值時,strongStr被意外的改變。這顯然不是我們想得到的,而且也是危險的。項目中出現類似的bug時,通常都很難定位。這就是不可變對象使用strong修飾所帶來的風險。
可變對象用copy
上面說了不可變對象使用strong的問題,那么可變對象使用copy有什么問題呢?還是寫代碼來驗證一下:
@property (nonatomic, copy) NSMutableString *mutString;
這里還是強調一下,既然屬性類型是可變類型,說明我們期望再程序中能夠改變mutString的值,否則直接使用NSString了。
看一下測試代碼:
1 - (void)testStrCopy 2 { 3 NSString *str = @"123"; 4 self.mutString = [NSMutableString stringWithString:str]; 5 NSLog(@"str = %p self.mutString = %p",str,self.mutString); // 兩者的地址不一樣 6 [self.mutString appendString:@"456"]; // 會崩潰,因為此時self.mutArray是NSString類型,是不可變對象 7 }
執行程序后,會崩潰,崩潰原因是:
[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xed877425eeef9883
即 self.mutString沒有appendString方法。self.mutString是NSMutableString類型,為何沒有appendString方法呢?這就是使用copy造成的。看一下
self.mutString = [NSMutableString stringWithString:str];
這行代碼到底發生了什么。這行代碼實際上完成了兩件事:
// 首先聲明一個臨時變量 NSMutableString *tempString = [NSMutableString stringWithString:str]; // 將該臨時變量copy,賦值給self.mutString self.mutString = [tempString copy];
注意,通過[tempString copy]得到的self.mutString是一個不可變對象,不可變對象自然沒有appendString方法,這也是為何會崩潰的原因。
copy和mutableCopy
另外常用來做對比的是copy和mutableCopy。copy和mutableCopy之間的差異主要和深拷貝和淺拷貝有關,先看一下深拷貝、淺拷貝的概念。
深拷貝、淺拷貝
所謂淺拷貝,在Objective-C中可以理解為引用計數加1,並沒有申請新的內存區域,只是另外一個指針指向了該區域。深拷貝正好相反,深拷貝會申請新的內存區域,原內存區域的引用計數不變。看圖來說明深拷貝和淺拷貝的區別。
首先A指向一塊內存區域,現在設置B = A
現在B和A指向了同一塊內存區域,即為淺拷貝。
再來看深考貝
首先A指向一塊內存區域,現在設置B = A
A和B指向的不是同一塊內存區域,只是這兩塊內存區域中的內容是一樣的,即為深拷貝。
可變對象的copy、mutableCopy
可變對象的copy和mutableCopy都是深拷貝。以可變對象NSMutableString和NSMutableArray為例,測試代碼:
1 - (void)testMutableCopy 2 { 3 NSMutableString *str1 = [NSMutableString stringWithString:@"abc"]; 4 NSString *str2 = [str1 copy]; 5 NSMutableString *str3 = [str1 mutableCopy]; 6 NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3); 7 NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"a",@"b", nil]; 8 NSArray *array2 = [array1 copy]; 9 NSMutableArray *array3 = [array1 mutableCopy]; 10 NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3); 11 }
輸出結果:
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為例,測試代碼如下:
1 - (void)testCopy 2 { 3 NSString *str1 = @"123"; 4 NSString *str2 = [str1 copy]; 5 NSMutableString *str3 = [str1 mutableCopy]; 6 NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3); 7 NSArray *array1 = @[@"1",@"2"]; 8 NSArray *array2 = [array1 copy]; 9 NSMutableArray *array3 = [array1 mutableCopy]; 10 NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3); 11 }
輸出結果:
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方法
項目開發中經常會有自定義對象的需求,那么自定義對象是否可以copy呢?如何支持copy?
自定義對象可以支持copy方法,我們所需要做的是:自定義對象遵守NSCopying協議,且實現copyWithZone方法。NSCopying協議是系統提供的,直接使用即可。
遵守NSCopying協議:
1 @interface Student : NSObject <NSCopying> 2 { 3 NSString *_sex; 4 } 5 6 @property (atomic, copy) NSString *name; 7 8 @property (nonatomic, copy) NSString *sex; 9 10 @property (nonatomic, assign) int age; 11 12 @end
實現CopyWithZone方法:
1 - (instancetype)initWithName:(NSString *)name age:(int)age sex:(NSString *)sex 2 { 3 if(self = [super init]){ 4 self.name = name; 5 _sex = sex; 6 self.age = age; 7 } 8 return self; 9 } 10 11 - (instancetype)copyWithZone:(NSZone *)zone 12 { 13 // 注意,copy的是自己,因此使用自己的屬性 14 Student *stu = [[Student allocWithZone:zone] initWithName:self.name age:self.age sex:_sex]; 15 return stu; 16 }
測試代碼:
1 - (void)testStudent 2 { 3 Student *stu1 = [[Student alloc] initWithName:@"Wang" age:18 sex:@"male"]; 4 Student *stu2 = [stu1 copy]; 5 NSLog(@"stu1 = %p stu2 = %p",stu1,stu2); 6 }
輸出結果:
stu1 = 0x600003a41e60 stu2 = 0x600003a41fc0
這里是一個深拷貝,根據copyWithZone方法的實現,應該很容易明白為何是深拷貝。
除了NSCopying協議和copyWithZone方法,對應的還有NSMutableCopying協議和mutableCopyWithZone方法,實現都是類似的,不做過多介紹。