Objective-C(二)對象、消息、運行期

這是Objective-C系列的第2篇。objective-c

什麼是屬性

屬性是Objective-C的一項特性,用於封裝對象中的數據;多線程

1 實例變量

@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類裏繼承給一個子類,那麼就須要在運行期動態建立存取方法,由於子類的某些屬性不是實例變量,其數據來自於後端的數據庫。

2 屬性特質

原子性

atomicnonatomic

具有atomic特質的獲取方法會經過鎖定機制來確保操做的原子性。若是兩個線程讀寫同一屬性,那麼不論什麼時候,總能看到有效的屬性值。如果不加鎖的話,那麼當其中一個線程正在改寫某屬性值時,另一個線程也許會忽然闖入,把還沒有修改好的屬性值讀取出來。

在iOS程序中,全部的屬性都會聲明爲nonatomic。這樣作的歷史緣由是:在iOS中使用同步鎖的開銷很大(Mac OS程序中,不會遇到性能瓶頸),將會帶來性能問題。

通常狀況下並不要求屬性必須是「原子的」,由於這並不能保證「線程安全」,若要實現線程安全,還需採用更爲深層的鎖定機制才行。例如,一個線程在連續屢次讀取某屬性值的過程當中有別的線程在同時改寫該值,那麼即使將屬性聲明爲atomic,也仍是會讀取到不一樣的屬性值。

讀寫權限

readwirtereadonly

內存管理語義

屬性用於封裝數據,而數據則要有「具體的全部權語義」。

  • assign:只會執行鍼對「純量類型」的簡單賦值操做;
  • strong:此特質代表一種「擁有關係」,爲這種屬性設置新值時,設置方法會先保留新值,並釋放舊值,而後再將新值設置上去;
  • weak:此特質代表該屬性定義了一種「非擁有關係」。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。與assign相似,而後在屬性所指的對象遭到摧毀時,屬性值會被清空(nil out)。
  • unsafe_unretained:此特質的語義和assign相似,可是它適用於「對象類型」,表達一種「非擁有關係」。當目標對象遭到摧毀是,屬性值不會被清空。
  • copy:此特質所表達的所屬關係和strong相似。然而設置方法並不保留新值,而是將其拷貝。

在對象內部儘可能直接訪問實例變量

經過點語法與直接訪問內部實例變量的區別在於:

  • 因爲不通過Objective-C的「方法派發」(method dispatch)步驟,因此直接訪問實例變量的速度會比較快在這種狀況下,編譯器所生成的代碼會直接訪問對象實例變量的那塊內存;
  • 直接訪問實例變量,不會調用其「設置方法」,這就繞過了爲相關屬性所定義的內存管理語義。好比,在ARC下,直接訪問一個聲明爲copy的屬性,那麼並不會拷貝該屬性,只會保留新值並釋放舊值;
  • 若是直接訪問實例變量,那麼不會觸發「鍵值觀察(key-value observing)」通知,這樣作是否會產生問題,取決於具體的對象行爲;
  • 經過屬性來訪問有助於排查與之相關的錯誤,由於能夠在「獲取方法」與「設置方法」中打斷點,監控該屬性的調用者及其訪問時機;

因此,合理的方案是:寫入實例變量時,經過其「設置方法」來作,而在讀取實例變量時,則直接訪問之

讀取直接訪問,是爲了提升訪問速度,而寫入則爲了保留內存管理語義。

這樣作須要注意下面兩點:

  1. 在初始化方法中應該如何設置屬性值?
  2. 惰性初始化

理解「對象等同性」

根據等同性(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能夠看出:

  • == :比較兩個對象的指針是否一致;
  • isEqaual: :比較兩個對象的字符串是否一致;

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;
}
複製代碼

這是一種比較典型的寫法,分析一下:

  • 首先判斷兩個指針是否一致,若是一致,毫無疑問,兩個對象是相等的;
  • 其次,判斷兩個類是否一致,假如類不一致,那麼便不相等,可是此處須要注意的是,在業務中須要繼承體系上認爲相等的,好比JSDog實例能夠JSAnimal(父類)相等的狀況要考慮,即繼承體系上的父子關係是否相等須要業務來作決定。
  • 最後,判斷各個字段是否相等,只有所有相等兩個對象纔算相等,不然,就不相等。

接下來,看一下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;
}
複製代碼

這裏,避免了建立字符串的開銷,又能使生成的哈希碼至少位於必定的範圍內,而不會過於頻繁的重複。固然,這種算法生成的哈希碼避免不了徹底不碰撞。因此在設計哈希算法是要在碰撞頻度與下降運算複雜程度之間取捨。

1. 特定類所具備的等同性斷定方法

除了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;
}
複製代碼

2. 等同性斷定的執行深度

建立等同性斷定方法時,須要決定是根據整個對象來判斷等同性,仍是僅根據其中幾個字段來判斷。

NSArray的檢測方式是先看兩個數組所含對象個數是否一致,若相同,則在每一個對應位置的兩個對象身上調用「isEqual:」方法。若是對應位置上的對象都相等,那麼則兩個數組相等,這叫作「深度等同性斷定」。

不過,有時候咱們仍然能夠根據業務來將深度維度降下來,只根據其中某一個屬性來斷定。好比JSDog存儲在數據庫表中有個identifier的惟一標識符,假如此標識符相同,咱們就認爲這是同一條🐶,無需多作其餘判斷,這種降維的工做通常由業務驅動,而不是憑空構想的。

3. 容器中可變類的等同性

看一個實例:

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方法;
  • 相同的對象必須有相同的hash碼,可是hash碼相同的對象卻未必相同;
  • 不要盲目地檢測每一個屬性,而是應該按照具體需求來制定檢測方案;
  • 編寫hash方法時,應該使用計算速度快並且哈希碼碰撞率低的算法。

new與alloc/init

  • new 實際調用alloc/init方法,等效;
  • new 不支持自定義init方法;
  • alloc-init 更清晰;

@synchronized

指令@synchronized()經過對一段代碼的使用進行加鎖。其餘試圖執行該段代碼的線程都會被阻塞,直到加鎖線程退出執行該段被保護的代碼段,也就是說@synchronized()代碼塊中的最後一條語句已經被執行完畢的時候。 通常在公用變量的時候使用,如單例模式或者操做類的static變量中使用。

指令@synchronized()須要一個參數。該參數可使任何的Objective-C對象,包括self。這個對象就是互斥信號量。他可以讓一個線程對一段代碼進行保護,避免別的線程執行該段代碼。針對程序中的不一樣的關鍵代碼段,咱們應該分別使用不一樣的信號量。只有在應用程序編程執行多線程以前就建立好全部須要的互斥信號量對象來避免線程間的競爭纔是最安全的。

相關文章
相關標籤/搜索