這是Objective-C系列的第2篇。objective-c
屬性是Objective-C的一項特性,用於封裝對象中的數據;多線程
@interface Person:NSObject {
@public
NSString *_firstName;
NSString *_lastName;
}
@end
複製代碼
在Java或C++中,咱們能夠定義實例變量的做用域。然而編寫Objective-C卻不多這麼作。這種寫法的問題是:對象佈局在編譯期就已經固定了。只要碰到訪問_firstName
變量的代碼,編譯器就把其替換成偏移量(offset)
,這個偏移量是硬編碼
,表示該變量距離存放對象的內存區域的起始地址有多遠。框架
這樣作,是不能應對增長一個實例變量這種狀況帶來的麻煩的,好比:ide
@interface Person:NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
}
@end
複製代碼
能夠看到,實例變量中在內存中的地址偏移量的改變(假設指針爲4個字節)。
因此,若是代碼使用編譯期計算出來的偏移量,那麼在修改類定義以後必須從新編譯,不然就會出錯。
在Objective-C中,爲了應對這種問題,把實例變量當作一種存儲偏移量所用的「特殊變量」,交由「類對象」保管。偏移量會在運行期查找,若是類的定義變了,那麼存儲的偏移量也就變了。這樣,不管什麼時候訪問實例變量,總能使用正確的偏移量。甚至能夠在運行期向類中新增實例變量。
這個問題還有一種解決方法就是——儘可能不要直接訪問實例變量,而應該經過存取方法來作。雖然說屬性最終仍是得經過實例變量來實現,但它卻提供了一種簡潔的抽象機制。
@interface Person:NSObject
@property NSString *firstName;
@peoperty NSString *lastName;
@end
複製代碼
上面代碼,編譯器替咱們作了兩件事:
其中,關聯的實例變量,是屬性名前加「_」,即firstName屬性對應添加的實例變量是「_firstName」。
固然,你也能夠經過@synthesize指定關聯的實例變量,但通常不推薦這麼作!
@implementation Person
@synthesize firstName = _myFirstName;
@end
複製代碼
最後,存取方法,也能夠本身實現。還有一種阻止編譯器自動合成存取方法,就是使用@dynamic
關鍵字,它告訴編譯器:不要自動建立實現屬性所用的實例變量的存取方法。並且,在編譯訪問屬性的代碼是,即便編譯器發現沒有定義存取方法,也不會報錯,由於它相信能在運行期找到。
好比,從CoreData框架中的NSManagedObject類裏繼承給一個子類,那麼就須要在運行期動態建立存取方法,由於子類的某些屬性不是實例變量,其數據來自於後端的數據庫。
atomic
、nonatomic
具有atomic
特質的獲取方法會經過鎖定機制來確保操做的原子性。若是兩個線程讀寫同一屬性,那麼不論什麼時候,總能看到有效的屬性值。如果不加鎖的話,那麼當其中一個線程正在改寫某屬性值時,另一個線程也許會忽然闖入,把還沒有修改好的屬性值讀取出來。
在iOS程序中,全部的屬性都會聲明爲nonatomic
。這樣作的歷史緣由是:在iOS中使用同步鎖的開銷很大(Mac OS程序中,不會遇到性能瓶頸),將會帶來性能問題。
通常狀況下並不要求屬性必須是「原子的」,由於這並不能保證「線程安全」,若要實現線程安全,還需採用更爲深層的鎖定機制才行。例如,一個線程在連續屢次讀取某屬性值的過程當中有別的線程在同時改寫該值,那麼即使將屬性聲明爲atomic
,也仍是會讀取到不一樣的屬性值。
readwirte
、readonly
屬性用於封裝數據,而數據則要有「具體的全部權語義」。
經過點語法與直接訪問內部實例變量的區別在於:
Objective-C
的「方法派發」(method dispatch)步驟,因此直接訪問實例變量的速度會比較快在這種狀況下,編譯器所生成的代碼會直接訪問對象實例變量的那塊內存;copy
的屬性,那麼並不會拷貝該屬性,只會保留新值並釋放舊值;因此,合理的方案是:寫入實例變量時,經過其「設置方法」來作,而在讀取實例變量時,則直接訪問之。
讀取直接訪問,是爲了提升訪問速度,而寫入則爲了保留內存管理語義。
這樣作須要注意下面兩點:
根據等同性(equality)
來比較對象是一個很是有用的功能。不過按照==操做符比較出來的結果未必是咱們想要的,由於該操做符比較的是兩個指針自己,而不是其所指的對象。應該使用NSObject
協議中聲明的isEqual:
方法來判斷兩個對象的等同性。某些對象還提供了特別的等同性判斷方法,好比NSStirng
類提供的isEqualToString:
。
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
BOOL equalA = (foo == bar); //NO
BOOL equalB = [foo isEqual:bar]; //YES
BOOL equalC = [foo isEqualToString:bar]; //YES
複製代碼
針對NSString
能夠看出:
isEqualToString
方法在這裏和isEqual
是等效的,可是isEqualToString
更快,由於後者因爲不知道受測對象的類型,還須要執行額外的步驟。
在NSObject
協議中有兩個用於判斷等同性的關鍵方法:
- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
複製代碼
NSObject
對這兩個方法的默認實現是:當前僅當其「指針值」徹底相等時,這兩個對象才相等。若想在自定義的對象中重寫這些方法,就必須理解其約定;
若是isEqual:
方法判斷兩個對象相等,那麼其hash
值一定相等。
反過來,若是其hash
值相等,那麼isEqual:
方法未必會認爲二者相等。
下面有個類,咱們認爲其全部字段相等,那麼這兩個對象就相等。重寫其isEqual:
以下:
@interface JSDog : NSObject
@property (nonatomic ,assign)NSInteger age;
@property (nonatomic ,copy)NSString *name;
@end
複製代碼
在@implementation JSDog
中:
- (BOOL)isEqual:(id)object
{
if (self == object) {
return YES;
}
if ([self class] != [object class]) {
return NO;
}
JSDog *otherDog = (JSDog*)object;
if (_age != otherDog.age) {
return NO;
}
if (![_name isEqualToString:otherDog.name]) {
return NO;
}
return YES;
}
複製代碼
這是一種比較典型的寫法,分析一下:
接下來,看一下hash
方法,首先,假如咱們認爲isEqual:
相等了,那麼hash
必相等,咱們簡單的能夠以下:
-(NSUInteger)hash
{
return 12345;
}
複製代碼
這麼寫,一看就知道有問題,由於無論什麼狀況,都返回相等的hash值。那麼是什麼問題呢?
在collection中檢索對象是依靠哈希表(hash table)時,會用對象的哈希碼作索引。假如某個collection使用set來實現的,那麼set可能會根據哈希碼將對象分裝到不一樣的數組(也成爲箱子)中。在向set中添加新對象時,要根據其哈希碼找到與之相關的那個數組,依次檢查其中各個元素,判斷其是否相等。若是相等,就說明要添加的對象已經在set裏面了。
問題來了,假如哈希碼都同樣,咱們不是要每依次判斷對象是否相等,假如如今數組中已經有10000個對象,那麼我再加入一個對象時,因爲哈希碼一致,咱們要作10000次的對象是否相等的判斷,效率低下,性能堪憂。因此,咱們要改進咱們的hash方法,至少要根據不一樣的屬性返回不一樣的hash值,下面是改進的版本:
-(NSUInteger)hash
{
NSString *stringToHash = [NSString stringWithFormat:@"%@:%ld",_name,(long)_age];
return [stringToHash hash];
}
複製代碼
上面的hash方法能夠根據不一樣的屬性返回不一樣的hash值,可是該hash方法,仍然要負擔建立字符串的開銷,因此比返回單一值要慢,因爲計算哈希碼的開銷過大,也許在collection中仍然會出現性能問題。
-(NSUInteger)hash
{
NSUInteger ageHash = _age;
NSUInteger nameHash = [_name hash];
return ageHash ^ nameHash;
}
複製代碼
這裏,避免了建立字符串的開銷,又能使生成的哈希碼至少位於必定的範圍內,而不會過於頻繁的重複。固然,這種算法生成的哈希碼避免不了徹底不碰撞。因此在設計哈希算法是要在碰撞頻度與下降運算複雜程度之間取捨。
除了NSString
中,具備isEqualToString:
這種特定類的斷定方法,NSArray
具備isEqualToArray:
,以及NSDictionary
具備isEqualToDictionary:
。
本身來寫特定類斷定方法時:
- (BOOL)isEqual:(id)object
{
if ([self class] == [object class]) {
return [self isEqualToDog:(JSDog *)object];
}else{
return [super isEqual:object];
}
}
- (BOOL)isEqualToDog:(JSDog*)otherDog
{
if (self == otherDog) {
return YES;
}
if ([self class] != [otherDog class]) {
return NO;
}
if (_age != otherDog.age) {
return NO;
}
if (![_name isEqualToString:otherDog.name]) {
return NO;
}
return YES;
}
複製代碼
建立等同性斷定方法時,須要決定是根據整個對象來判斷等同性,仍是僅根據其中幾個字段來判斷。
NSArray的檢測方式是先看兩個數組所含對象個數是否一致,若相同,則在每一個對應位置的兩個對象身上調用「isEqual:」方法。若是對應位置上的對象都相等,那麼則兩個數組相等,這叫作「深度等同性斷定」。
不過,有時候咱們仍然能夠根據業務來將深度維度降下來,只根據其中某一個屬性來斷定。好比JSDog
存儲在數據庫表中有個identifier
的惟一標識符,假如此標識符相同,咱們就認爲這是同一條🐶,無需多作其餘判斷,這種降維的工做通常由業務驅動,而不是憑空構想的。
看一個實例:
NSMutableSet *set = [NSMutableSet set];
//1.
NSMutableArray *arrayA = [@[@1,@2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@",set);
//2.
NSMutableArray *arrayB = [@[@1,@2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set = %@",set);
//3.
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@",set);
//4.
[arrayC addObject:@2];
NSLog(@"set = %@",set);
//5.
NSSet *setB = [set copy];
NSLog(@"setB = %@",setB);
複製代碼
打印出來的log:
set = {( (1,2) )}
set = {( (1,2) )}
set = {( (1), (1,2) )}
set = {( (1,2), (1,2) )}
setB = {( (1,2) )}
複製代碼
第一步,添加arrayA是正常的,第二步添加arrayB,因爲set中arrayA與arrayB相等,因此set仍然只有一個對象也是正常的。第三步,添加了一個set中沒有的arrayC,正常的。
第四步,將arrayC中數組,添加一個元素2,使得arrayC此時與arrayA相等了,可奇怪的是,竟然同時存在於set中;
第五步,將此時包含了兩個相等的數組arrayA,arrayC的set進行copy,獲得setB,又發生了奇怪的事,兩個相同的數組只剩下一個。
把某個對象放入了collection中,就不該該改變其哈希碼,像上面的狀況就是在講arrayC加入到set後,又更改了arrayC的哈希碼。collection會根據哈希碼將不一樣的對象放入到不一樣的「箱子數組」中,若是某對象在放入「箱子」以後,哈希碼改變,那麼如今所處的這個箱子對它來講就是錯誤的。
因此,要麼確保哈希碼不是根據對象的「可變部分」計算出來的,要麼保證放入collection中的對象是再也不可變的。
isEqual:
與hash
方法;指令@synchronized()經過對一段代碼的使用進行加鎖。其餘試圖執行該段代碼的線程都會被阻塞,直到加鎖線程退出執行該段被保護的代碼段,也就是說@synchronized()代碼塊中的最後一條語句已經被執行完畢的時候。 通常在公用變量的時候使用,如單例模式或者操做類的static變量中使用。
指令@synchronized()須要一個參數。該參數可使任何的Objective-C對象,包括self。這個對象就是互斥信號量。他可以讓一個線程對一段代碼進行保護,避免別的線程執行該段代碼。針對程序中的不一樣的關鍵代碼段,咱們應該分別使用不一樣的信號量。只有在應用程序編程執行多線程以前就建立好全部須要的互斥信號量對象來避免線程間的競爭纔是最安全的。