iOS開發讀書筆記:Effective Objective-C 2.0 52個有效方法 - 篇1/4

iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇1/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇2/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇3/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇4/4

  • 第一章 熟悉Objective-C
    • 第1條:了解Objective-C語言的起源
    • 第2條:在類的頭文件中盡量少引入其他頭文件
    • 第3條:多用字面量語法,少用閾值等價的方法
    • 第4條:多用類型常量,少用#define預處理指令
    • 第5條:用枚舉表示狀態、選項、狀態碼
  • 第二章 對象、消息、運行期
    • 第6條:理解“屬性”這一概念
    • 第7條:在對象內部盡量直接訪問實例變量
    • 第8條:理解“對象等同性”這一概念
    • 第9條:以“類族模式”隱藏實現細節
    • 第10條:在既有類中使用關聯對象存放自定義數據

第一章 熟悉Objective-C

Objective-C是在C語言基礎上添加了面向對象特性。

第1條:了解Objective-C語言的起源

Objective-C與C++、Java等面向對象語言類似,有所差別是因為該語言使用“消息結構”(messaging structure)而非“函數調用”(Unction calling)。消息與函數調用之間的區別看上去就像這樣:

// Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];

// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1,parameter2);

關鍵區別在于:

  • 使用消息結構的語言,其運行時所應執行的代碼由運行環境來決定;
  • 使用函數調用的語言,則由編譯器決定;

如果范例代碼中調用的函數是多態的,那么在運行時就要按照“虛方法表”(virtual table是編程語言為實現“動態派發”(dynamic dispatch)或“運行時方法綁定”(runtime method binding)而采用的一種機制)來查出到底應該執行哪個函數實現。而采用消息結構的語言,不論是否多態,總是在運行時才會去査找所要執行的方法。實際上,編譯器甚至不關心接收消息的對象是何種類型。接收消息的對象問題也要在運行時處理,其過程叫做“動態綁定"(dynamic binding)。

Objective-C的重要工作都由“運行期組件”(runtime component)而非編譯器來完成。使用Objective-C的面向對象特性所需的全部數據結構及函數都在運行期組件里面。舉例來說, 運行期組件中含有全部內存管理方法。運行期組件本質上就是一種與開發者所編代碼相鏈接的“動態庫”(dynamic library),其代碼能把開發者編寫的所有程序粘合起來。這樣的話,只需更新運行期組件,即可提升應用程序性能。而那種許多工作都在“編譯期”(compile time) 完成的語言,若想獲得類似的性能提升,則要重新編譯應用程序代碼。

Objective-C是C的“超集”(superset),所以C語言中的所有功能在編寫Objective-C代碼時依然適用。其中尤為重要的是要理解C語言的內存模型(memory model),這有助于理解Objective-C的內存模型及其“引用計數”(reference counting)機制的工作原理。 若要理解內存模型,則需明白:Objective-C語言中的指針是用來指示對象的。想要聲明一個變量,令其指代某個對象,可用如下語法:

NSString *someString = @"The string";

這種語法基本上是照搬C語言的,它聲明了一個名為someString的變量,其類型是NSString *。也就是說,此變量為指向NSString的指針。所有Objective-C語言的對象都必須這樣聲明,因為對象所占內存總是分配在“堆空間”(heap space)中,而絕不會分配在“棧” (stack)上。
如果在棧中分配Objective-C對象,編譯報錯:

NSString stackstring;
//error: interface type cannot be statically allocated

someString變量指向分配在堆里的某塊內存,其中含有一個NSString對象。也就是說, 如果再創建一個變量,令其指向同一地址,那么并不拷貝該對象,只是這兩個變量會同時指向此對象:

NSString *someString = @"The string";
NSString *anotherString = someString;

只有一個NSString實例,然而有兩個變量指向此實例。兩個變量都是NSString *型,這說明當前“棧幀”(stack frame)里分配了兩塊內存,每塊內存的大小都能容下一枚指針(在32位架構的計算機上是4字節,64位計算機上是8字節)。這兩塊內存里的值都一樣,就是NSString實例的內存地址。
下圖描述了此時的內存布局。存放在NSString實例中的數據含有代表字符串實際內容的字節。

內存布局.png

分配在堆中的內存必須直接管理,而分配在棧上用于保存變量的內存則會在其棧幀彈出時自動清理。

Objective-C將堆內存管理抽象出來了。不需要用malloc及free來分配或釋放對象所占內存。Objective-C運行期環境把這部分工作抽象為一套內存管理架構,名叫“引用計數”。

在Objective-C代碼中,有時會遇到定義里不含*的變量,它們可能會使用“棧空間” (stack space)。這些變量所保存的不是Objective-C對象。比如Core Graphics框架中的CGRect就是個例子:

CGRect frame;
frame.origin.x = 0.0f;

CGRect是C結構體,其定義是:

struct CGRect {
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

整個系統框架都在使用這種結構體,因為如果改用Objective-C對象來做的話,性能會受影響。與創建結構體相比,創建對象還需要額外開銷,例如分配及釋放堆內存等。如果只需保存int、float、double、char等“非對象類型”(nonobject type),那么通常使用CGRect這種結構體就可以了。

要點:

  1. Objective-C為C語言添加了面向對象特性,是其超集。Objective-C使用動態綁定的消息結構,也就是說,在運行時才會檢查對象類型。接收一條消息之后,究竟應執行何種代碼,由運行期環境而非編譯器來決定。
  2. 理解C語言的核心概念有助于寫好Objective-C程序。尤其要掌握內存模型與指針。

第2條:在類的頭文件中盡量少引入其他頭文件

與C和C++一樣,Objective-C也使用“頭文件”(header file)與“實現文件” (implementation file)來區隔代碼。用Objective-C語言編寫“類”(class)的標準方式為:以類名做文件名,分別創建兩個文件,頭文件后綴用.h,實現文件后綴用.m。

創建連個類:EOCPerson和EOCEmployer。為EOCPerson類添加一項屬性,在EOCPerson.h中加入下面這行:

@property (nonatomic, strong) EOCEmployer *employer; 

在編譯一個使用了EOCPerson類的文件時,不需要知道 EOCEmployer類的全部細節,只需要知道有一個類名叫EOCEmployer就好。可以使用“向前聲明"(forward declaring)該類能把這一情況告訴編譯器:

@class EOCEmployer;

EOCPerson類的實現文件則需引入EOCEmployer類的頭文件,因為若要使用后者,則必須知道其所有接口細節。

將引入頭文件的時機盡量延后,只在確有需要時才引入,這樣就可以減少類的使用者所需引入的頭文件數量,減少編譯時間。

向前聲明也解決了兩個類互相引用的問題。此時,若要編譯EOCEmployer,則編譯器必須知道EOCPerson這個類,而要編譯EOCPerson,則又必須知道EOCEmployer。如果在各自頭文件中引入對方的頭文件,則會導致“循環引用”(chicken-and-egg situation)。當解析其中一個頭文件時,編譯器會發現它引入了另一個頭文件,而那個頭文件又回過頭來引用第一個頭文件。使用#import而非#include指令雖然不會導致死循環,但卻這意味著兩個類里有一個無法被正確編譯。

但是有時候必須要在頭文件中引入其他頭文件:

  1. 如果你寫的類繼承自某個超類,則必須引入定義那個超類的頭文件;
  2. 同理,如果要聲明你寫的類遵從某個協議(protocol),那么該協議必須有完整定義,且不能使用向前聲明。向前聲明只能告訴編譯器有某個協議,而此時編譯器卻要知道該協議中定義的方法。鑒于此,最好是把協議單獨放在一個頭文件中,那么只要引入此協議,就必定會引入那個頭文件中的全部內容。

然而有些協議,例如“委托協議”(delegate protocol),就不用單獨寫一個頭文件了。在那種情況下,協議只有與接受協議委托的類放在一起定義才有意義。此時最好能在實現文件中聲明此類實現了該委托協議。

要點:

  1. 除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明來提及別的類,并在實現文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合(coupling)。
  2. 有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況下,盡量把“該類遵循某協議”的這條聲明移至“class-cominuation分類”中。如果不行的話,就把協議單獨放在一個頭文件中,然后將其引入。

第3條:多用字面量語法,少用與之等價的方法

Objective-C以語法繁雜而著稱。不過,從Objective-C 1.0起,有一種非常簡單的方式能創建NSString對象。這就是“字符串字面量” (string literal),其語法如下:

NSString *someString = @"Effective Objective-C 2.0";

如果不用這種語法的話,就要以常見的alloc及init方法來分配并初始化NSString對象了。

字面數值

有時需要把整數、浮點數、布爾值封入Objective-C對象中。這種情況下可以用NSNumber類,該類可處理多種類型的數值。

字面量數組

用字面量語法創建數組時要注意,若數組元素對象中有nil,則會拋出異常,因為字面量語法實際上只是一種“語法糖"(syntactic sugar:也稱“糖衣語法”,是指計算機語言中與另外一套語法等效但是開發者用起來卻更加方便的語法。語法糖 可令程序更易讀,減少代碼出錯機率。),其效果等于是先創建了一個數組,然后把方括號內的所有對象都加到這個數組中。

在改用字面量語法來創建數組時就會遇到這個問題。下面這段代碼分別以兩種語法創建數組:

id object1 = /* ... */;
id object2 = /* ... */;
id object3 = /* ... */;

NSArray *arrayA = [NSArray array WithObjects:object1,object2, object3, nil];
NSArray *arrayB ? @[object1,object2,object3];

如果object1與object3都指向了有效的Objective-C對象,而object2是nil, 那么會出現什么情況呢?按字面量語法創建數組arrayB時會拋出異常。arrayA雖然能創建出來,但是其中卻只含有object1 一個對象。原因在于,“arrayWithObjects:”方法會依次處理各個參數,直到發現nil為止,由于object2是nil,所以該方法會提前結束。

這個微妙的差別表明,使用字面量語法更為安全。拋出異常令應用程序終止執行,這比創建好數組之后才發現元素個數少了要好。向數組中插入nil通常說明程序有錯,而通過異常可以更快地發現這個錯誤。

字面量字典

“字典”(Dictionary)是一種映射型數據結構,可向其中添加鍵值對,字典中的對象和鍵必須都是Objective-C對象。

與數組一樣,用字面量語法創建字典時也有個問題,那就是一旦有值為nil,便會拋出異常。不過基于同樣的原因,這也是個好事。假如在創建字典時不小心用了空值對象,那 么“dictionaryWithObjectsAndKeys:”方法就會在首個nil之前停下,并拋出異常,這有助于査錯。

可變數組與字典

如果數組與字典對象是可變的(mutable),那么也能通過下標修改其中的元素值。修改可變數組與字典內容的標準做法是:

[mutableArray replaceObjectAtlndex: 1 withObject :@"dog"];
[mutableDictionary setObject: @"Galloway" forKey: @"lastName"];

若換用取下標操作來寫,則是:

mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";

局限性

字面量語法有個小小的限制,就是除了字符串以外,所創建出來的對象必須屬于Foundation框架才行。如果自定義了這些類的子類,則無法用字面量語法創建其對象。很少有人會從其中自定義子類。

使用字面量語法創建出來的字符串、數組、字典對象都是不可變的(immutable)。若想要可變版本的對象,則需復制一份。這么做會多調用一個方法,而且還要再創建一個對象,不過使用字面量語法所帶來的好處還是多于上述缺點的。

要點:

  1. 應該使用字面雖語法來創建字符串、數值、數組、字典。與創建此類對象的常規方法相比,這么做更加簡明扼要。
  2. 應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
  3. 用字面童語法創建數組或字典時,若值中有nil,則會拋出異常。因此,務必確保值里不含nil。

第4條:多用類型常量,少用#define預處理指令

用宏定義來定義一個常量:

#define ANIMATION_DURATION 0.3

上述預處理指令會把源代碼中的ANIMATION_DURATION字符串替換為0.3,有如下缺點:

  1. 定義出來的常量沒有類型信息;
  2. 預處理過程會把碰到的所有ANIMATION_DURATION一律替換成0.3,這樣的話,假設此指令聲明在某個頭文件中,那么所有引入了這個頭文件的代碼,其ANIMATION_DURATION都會被替換。

要想解決此問題,應該設法利用編譯器的某些特性才對。有個辦法比用預處理指令來定義常量更好。下面就定義了一個類型為NSTimelnterval的常量:(請注意,用此方式定義的常量包含類型信息,其好處是清楚地描述了常量的含義。)

static const NSTimelnterval kAnimationDuration = 0.3;

注意常量名稱:常用的命名法是:若常量局限于某“編譯單元"(translation unit,也就是“實現文件”,implementation file)之內,則在前面加字母k;若常量在類之外可見,則通常以類名為前綴。

注意定義常量的位置:我們總喜歡在頭文件里聲明預處理指令,這樣做真的很糟糕,當常量名稱有可能互相沖突時更是如此。例如,ANIMATION_DURATION這個常量名就不該用在頭文件中,因為所有引入了這份頭文件的其他文件中都會出現這個名字。其實就連用static const定義的那個常量也不應出現在頭文件里。因為Objective-C沒有“名稱空間” (namespace)這一概念,所以那樣做等于聲明了一個名叫kAnimationDuration的全局變量。 此名稱應該加上前綴,以表明其所屬的類,例如可改為EOCViewClassAnimationDuration。

若不打算公開某個常量,則應將其定義在使用該常量的實現文件里。

變量一定要同時用static與const來聲明:如果試圖修改由const修飾符所聲明的變量, 那么編譯器就會報錯。而static修飾符則意味著該變量僅在定義此變量的編譯單元中可見。編譯器每收到一個編譯單元,就會輸出一份“目標文件”(object file)。在Objective-C的語境下,“編譯單元”一詞通常指每個類的實現文件(以.m為后綴名)。因此,在上述代碼中聲明的kAnimationDuration變量,其作用域僅限于由EOCAnimatedView.m所生成的目標文件中。假如聲明此變量時不加static,則編譯器會為它創建一個“外部符號”(external symbol)。此時若是另一個編譯單元中也聲明了同名變量,那么編譯器就拋出一條錯誤消息:

duplicate symbol _kAnimationDuration in:
    EOCAnimatedView.o 
    EOCOtherView.o

實際上,如果一個變量既聲明為static,又聲明為const,那么編譯器根本不會創建符號, 而是會像執行預處理指令一樣,把所有遇到的變量都替換為常值。不過還是要記住:用這種方式定義的常量帶有類型信息。

有時候需要對外公開某個常值變量(constant variable)。此類常量需放在“全局符號表”(global symbol table)中,以便可以在定義該常量的編譯單元之外使用。因此,其定義方式與上例演示的static const有所不同。應該這樣來定義:

//in the header file
extern NSString *const EOCStringConstant;

// In the implementation file
NSString *const EOCStringConstant = @"VALUE";

這個常量在頭文件中“聲明”,且在實現文件中“定義”。注意const修飾符在常量類型中的位置。常量定義應從右至左解讀,所以在本例中,EOCStringConstant就是“一個常量, 而這個常量是指針,指向NSString對象”。這與需求相符:我們不希望有人改變此指針常量, 使其指向另一個NSString對象。

編譯器看到頭文件中的extern關鍵字,就能明白如何在引入此頭文件的代碼中處理該常量了。這個關鍵字是要告訴編譯器,在全局符號表中將會有一個名叫EOCStringConstant的符號。也就是說,編譯器無須查看其定義,即允許代碼使用此常量。因為它知道,當鏈接成二進制文件之后,肯定能找到這個常量。

此類常量必須要定義,而且只能定義一次。通常將其定義在與聲明該常量的頭文件相關的實現文件里。由實現文件生成目標文件時,編譯器會在“數據段”(data section)為字符串分配存儲空間。鏈接器會把此目標文件與其他目標文件相鏈接,以生成最終的二進制文件。 凡是用到EOCStringConstant這個全局符號的地方,鏈接器都能將其解析。

因為符號要放在全局符號表里,所以命名常量時需謹慎。為避免名稱沖突,最好是用與之相關的類名做前綴。系統框架中一般都這樣做。例如UIKit就按照這種方式來聲明用作通知名稱的全局常量。其中有類似UIApplicationDidEnterBackgroundNotification 與UIApplicationWillEnterForegroundNotification 這樣的常量名。 其他類型的常量也是如此。

要點:

  1. 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量值不一致。
  2. 在實現文件中使用static const來定義“只在編譯單元內可見的常量”(translation-unit- specific constant)。由于此類常量不在全局符號表中,所以無須為其名稱加前綴。
  3. 在頭文件中使用extern來聲明全局常量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴。

第5條:用枚舉表示狀態、選項、狀態碼

由于Objective-C基于C語言,所以C語言有的功能它都有。其中之一就是枚舉類型:enum

枚舉只是一種常量命名方式。某個對象所經歷的各種狀態就可以定義為一個簡單的枚舉集(enumeration set)。

除了正常使用枚舉之外,還有一種情況應該使用枚舉類型,那就是定義選項且選項可以彼此組合。只要枚舉定義的對,各選項之間就可通過“按位或操作符”(bitwise OR operator)來組合。

每個枚舉值(UIVievvAutoresizingNone除外,它的值是0,對應的二進制值也是0,其中沒有值為1的二進制位)所對應的二進制表示中,只有1個二進制位的值是1。用‘按位或操作符’可組合多個選項。下圖中列出了每個枚舉成員的二進制值,并演示了其中兩個枚舉組合之后的值。


枚舉.png
enum UIViewAutoresizing resizing = UIViewAutoresizingFlexiblGWidth | UIViewAutoresizingFlexibleHeight; 
if (resizing & UIViewAutoresizingFlexibleWidth) { 
    // UIViewAutoresizingFlexibleWidth is set
};

要點:

  1. 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
  2. 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將各選項值定義為2的冪,以便通過按位或操作將其組合起來。
  3. 用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,可以指定用于保存枚舉值的底層數據類型。
  4. 在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后, 編譯器就會提示開發者:switch語句并未處理所有枚舉。

第二章 對象、消息、運行期

用Objective-C等面向對象語言編程時,“對象”(object)就是“基本構造單元"(building block),開發者可以通過對象來存儲并傳遞數據。在對象之間傳遞數據并執行任務的過程叫做“消息傳遞”(Messaging)。若想編寫出髙效且易維護的代碼,就一定要熟悉這兩個特性的工作原理。

當應用程序運行起來以后,為其提供相關支持的代碼叫做“Objective-C運行期環境” (Objective-C runtime),它提供了一些使得對象之間能夠傳遞消息的重要函數,并且包含創建類實例所用的全部邏輯。

第6條:理解“屬性”這一概念

“屬性”(property)用于封裝對象中的數據。Objective-C對象通常會把其所需要的數據保存為各種實例變量。實例變量一般通過“存取方法”(access method)來訪問。其中,“獲取方法"(getter)用于讀取變量值,而設置方法(setter)用于寫入變量值。 開發者可以令編譯器自動編寫與屬性相關的存取方法。此特性引入了一種新的“點語法”(dot syntax),使開發者可以更為容易地依照類對象來訪問存放于其中的數據。

在描述個人信息的類中,也許會存放人名等內容。可以在類接口的public區段中聲明一些實例變量:

@interface EOCPerson : NSObject {

  @public
  NSString *_firstName;                            .
  NSString *_lastName;

  @private
  NSString *_someInternalData;
}
@end

原來編過Java或C++程序的人應該比較熟悉這種寫法,在這些語言中,可以定義實例變量的作用域。然而編寫Objective-C代碼時卻很少這么做。這種寫法的問題是:對象布局在編譯期(compile time)就已經固定了。只要碰到訪問firstName變量的代碼,編譯器就把其替換為“偏移量”(offset),這個偏移量是“硬編碼”(hardcode),表示該變量距離存放對象的內存區域的起始地址有多遠。這樣做目前來看沒問題,但是如果又加了一個實例變量,那就麻煩了。比如說,假設在_firstName之前又多了一個實例變量:

@interface EOCPerson : NSObject {

  @public
  NSDate *_dataOfBirth;                            .
  NSString *_firstName;                            .
  NSString *_lastName;

  @private
  NSString *_someInternalData;
}
@end

原來表示_firstName的偏移量現在卻指向dateOfBirth 了。把偏移量硬編碼于其中的那些代碼都會讀取到錯誤的值。

如果代碼使用了編譯期計算出來的偏移量,那么在修改類定義之后必須重新編譯,否則就會出錯。Objective-C的做法是,把實例變量當做一種存儲偏移量所用的“特殊變量” (special variable),交由“類對象”(class object)保管。偏移量會在運行期査找,如果類的定義變了,那么存儲的偏移量也就變了,這樣的話,無論何時訪問實例變量,總能使用正確的偏移量。甚至可以在運行期向類中新增實例變量,這就是穩固的“應用程序二進制接口”(Application Binary Interface,ABI)。ABI定義了許多內容,其中一項就是生成代碼時所應遵循的規范。有了這種“穩固的”(nonfragile)的ABI,我們就可以在 “class-continuation分類”或實現文件中定義實例變量了。所以說,不一定要在接口中把全部實例變量都聲明好,可以將某些變量從接口的public區段里移走,以便保護與類實現有關的內部信息。

這個問題還有一種解決辦法,就是盡量不要直接訪問實例變量,而應該通過存取方法來做。雖說屬性最終還是得通過實例變量來實現。在正規的Objective-C編碼風格中,存取方法有著嚴格的命名規范。 所以Objective-C這門語言才能根據名稱自動創建出存取方法。這時@property語法就派上用場了,編譯器會自動寫出一套存取方法, 用以訪問給定類型中具有給定名稱的變量。

要訪問屬性,可以使用“點語法”,編譯器會把“點語法”轉換為對存取方法的調用,使用“點 語法”的效果與直接調用存取方法相同。

然而屬性還有更多優勢。如果使用了屬性的話,那么編譯器就會自動編寫訪問這些屬性所需的方法,此過程叫做“自動合成”(autosynthesis)。需要強調的是,這個過程由編譯器在編譯期執行,所以編輯器里看不到這些“合成方法"(synthesized method)的源代碼。除了生成方法代碼之外,編譯器還要自動向類中添加適當類型的實例變量,并且在屬性名前面加下劃線,以此作為實例變量的名字。在前例中,會生成兩個實例變量,其名稱分別為_ firstName與_lastName。也可以在類的實現代碼里通過@synthesize語法來指定實例變量的名字:

@implementation EOCPerson 
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName; 
@end

前述語法會將生成的實例變量命名為_myFirstName與_myLastName,而不再使用默認的名字。一般情況下無須修改默認的實例變置名。

若不想令編譯器自動合成存取方法,可以自己實現。也可以使用@dynamic關鍵字,它會告訴編譯器:不要自動創建實現屬性所用的實例變量,也不要為其創建存取方法。而且,在編譯訪問屬性的代碼時,即使編譯器發現沒有定義存取方法,也不會報錯,它相信這些方法能在運行期找到。

@dynamic data;

屬性特質

使用屬性時還有一個問題要注意,就是其各種特質(attribute)設定也會影響編譯器所生成的存取方法。比如下面這個屬性就指定了三項特質:

@property (nonatomic,readwrite,copy) NSString *firstName;

屬性可以擁有的特質分為四類:

原子性

在默認情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性(atomicity:在并發編程中,如果某操作具備整體性,也就是說,系統其他部分無法觀察到其中間步驟所生成的臨時結果,而只能看到操作前與操作后的結果,那么該操作就是“原子的"(atomic),或者說,該操作具備“原子性”) 。如果屬性具備nonatomic特質,則不使用同步鎖。請注意若是自己定義存取方法,那么就應該遵從與屬性特質相符的原子性。

讀/寫權限

  1. 具備readwrite(讀寫)特質的屬性擁有“獲取方法”(getter)與“設置方法"(setter) 。若該屬性由@synthesize實現,則編譯器會自動生成這兩個方法。
  2. 具備readonly (只讀)特質的屬性僅擁有獲取方法,只有當該屬性由@synthesize實現時,編譯器才會為其合成獲取方法。你可以用此特質把某個屬性對外公開為只讀屬性,然后在“dass-cominuaticm分類”中將其重新定義為讀寫屬性。

內存管理語義

屬性用于封裝數據,而數據則要有“具體的所有權語義”(concrete ownership semantic)。 下面這一組特質僅會影響“設置方法”。例如,用“設置方法”設定一個新值時,它是應該 “保留"(retain)此值呢,還是只將其賦給底層實例變量就好?編譯器在合成存取方法時,要根據此特質來決定所生成的代碼。如果自己編寫存取方法,那么就必須同有關屬性所具備的特質相符。

  1. assign:“設置方法”只會執行針對“純量類型”(scalar type,例如CGFloat或 NSInteger等)的簡單賦值操作。
  2. strong:此特質表明該屬性定義了一種“擁有關系”(owning relationship)。為這種屬性設置新值時,設置方法會先保留新值,并釋放舊值,然后再將新值設置上去。
  3. weak:此特質表明該屬性定義了一種“非擁有關系”(nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。
  4. __unsafe_unretained:此特質的語義和assign相同,但是它適用于“對象類型”(object type),該特質表達一種“非擁有關系”(“不保留”,unretained),當目標對象遭到摧毀時,屬性值不會自動清空(“不安全”,unsafe),這一點與weak有區別。
  5. copy:此特質所表達的所屬關系與strong類似。然而設置方法并不保留新值,而是將其“拷貝”(copy)。當屬性類型為NSString *時,經常用此特質來保護其封裝性, 因為傳遞給設置方法的新值有可能指向一個NSMutableString類的實例。這個類是NSString的子類,表示一種可以修改其值的字符串,此時若是不拷貝字符串,那么設置完屬性之后,字符串的值就可能會在對象不知情的情況下遭人更改。所以,這時就要拷貝一份“不可變”(immutable)的字符串,確保對象中的字符串值不會無意間變動。只要實現屬性所用的對象是“可變的”(mutable),就應該在設置新屬性值時拷貝一份。

方法名

可通過如下特質來指定存取方法的方法名:

  1. getter=<name>指定“獲取方法”的方法名。如果某屬性是Boolean型,而你想為其獲取方法加上“is”前綴,那么就可以用這個辦法來指定。
@property (nonatomic, getter=isOn) BOOL on;
  1. setter=<name>指定“設置方法”的方法名。這種用法不太常見。

通過上述特質,可以微調由編譯器所合成的存取方法。不過需要注意:若是自己來實現這些存取方法,那么應該保證其具備相關屬性所聲明的特質。比方說,如果將某個屬性聲明 為copy,那么就應該在“設置方法”中拷貝相關對象,否則會誤導該屬性的使用者。

如果想在其他方法里設置屬性值,那么同樣要遵守屬性定義中所宣稱的語義。

@interface EOCPerson : NSManagedObject
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
- (id)initWithFirstName: (NSString *)firstName lastName:(NSString *)lastName;
@end

在實現這個自定義的初始化方法時,一定要遵循屬性定義中宣稱的“copy”語義,因為 “屬性定義”就相當于“類”和“待設置的屬性值”之間所達成的契約。

- (id)initWithFirstName: (NSString*) firstName lastName:(NSString*)lastName {
  if ((self = [super init])) {
    _firstName = [firstName copy];
    _lastName = [lastName copy];
  }
  return self;
)

為何不調用屬性所對應的“設置方法”呢?如果用了“設置方法”的話,不是總能保證準確的語義嗎?筆者在第7條中將會詳細解釋為什么決不應該在init (或dealloc)方法中調用存取方法。

atomic與nonatomic的區別是什么呢?前面說過,具備atomic特質的獲取方法會通過鎖定機制來確保其操作的原子性。這也就是說,如果兩個線程讀寫同一屬性,那么不論何時, 總能看到有效的屬性值。若是不加鎖的話(或者說使用nonatomic語義),那么當其中一個線程正在改寫某屬性值時,另外一個線程也許會突然闖入,把尚未修改好的屬性值讀取出來。 發生這種情況時,線程讀到的屬性值可能不對。

一般來說所有屬性都聲明為nonatomic。這樣做的歷史原因是:在iOS中使用同步鎖的開銷較大,這會帶來性能問題。一般情況下并不要求屬性必須是“原子的”,因為這并不能保證“線程安全"(thread safety),若要實現“線程安全”的操作,還需采用更為深層的鎖定機制才行。例如,一個線程在連續多次讀取某屬性值的過程中有別的線程在同時改寫該值,那么即便將屬性聲明為atomic,也還是會讀到不同的屬性值。 因此,開發iOS程序時一般都會使用nonatomic屬性。但是在開發Mac OS X程序時,使用atomic屬性通常都不會有性能瓶頸。

要點:

  1. 可以語法來定義對象中所封裝的數據。
  2. 通過“特質”來指定存儲數據所需的正確語義。
  3. 在設置屬性所對應的實例變量時,一定要遵從該屬性所聲明的語義。
  4. 開發iOS程序時應該使用nonatomic屬性,因為atomic屬性會嚴重影響性能。

第7條:在對象內部盡量直接訪問實例變量

在對象之外訪問實例變量時,總是應該通過屬性來做。

使用“點語法”通過存取方法來訪問相關實例變量和不經由存取方法,而是直接訪問實例變量,這兩種寫法有幾個區別:

  1. 由于不經過Objective-C的“方法派發” (method dispatch,參見第11條)步驟,所以直接訪問實例變量的速度當然比較快。在這種情況下,編譯器所生成的代碼會直接訪問保存對象實例變量的那塊內存。
  2. 直接訪問實例變童時,不會調用其“設置方法”,這就繞過了為相關屬性所定義的“內存管理語義”。比方說,如果在ARC下直接訪問一個聲明為copy的屬性,那么并不會拷貝該屬性,只會保留新值并釋放舊值。
  3. 如果直接訪問實例變量,那么不會觸發‘鍵值觀測’(Key-Value Observing,KVO:一種通知機制,當某對象屬性改變時,可通知其他對象)通知。這樣做是否會產生問題,還取決于具體的對象行為。
  4. 通過屬性來訪問有助于排査與之相關的錯誤,因為可以給“獲取方法”和/或“設置方法”中新增“斷點"(breakpoint),監控該屬性的調用者及其訪問時機。

有一種合理的折中方案,那就是:在寫入實例變量時,通過其“設置方法”來做,而在讀取實例變量時,則直接訪問之。此辦法既能提高讀取操作的速度,又能控制對屬性的寫入操作。之所以要通過“設置方法”來寫入實例變量,其首要原因在于,這樣做能夠確保相關屬性的“內存管理語義”得以貫徹。但是,選用這種做法時,需注意幾個問題。

第一個要注意的地方就是,在初始化方法中應該如何設置屬性值。這種情況下總是應該直接訪問實例變量,因為子類可能會“覆寫”(override)設置方法。假設EOCPerson有一 個子類叫做EOCSmithPerscm,這個子類專門表示那些姓“Smith”的人。該子類可能會覆寫lastName屬性所對應的設置方法:

- (void)setLastName:(NSString^)lastName {
  if (![lastName isEqualToString:@"Smith"]) {
    [NSException raise:NSInvalidArgumentExceptionformat:@"Last name must be Smith"];
  }
  self.lastName = lastname;
}

但是,某些情況下卻又必須在初始化方法中調用設置方法:如果待初始化的實例變量聲明在超類中,而我們又無法在子類中直接訪問此實例變量的話,那么就需要調用“設置方法”了。

另外一個要注意的問題是‘惰性初始化’(lazy initialization:由于此屬性不常用, 而且該屬性所指代的對象相當復雜,創建成本較高)。在這種情況下,必須通過 “獲取方法”來訪問屬性,否則,實例變量就永遠不會初始化。

要點:

  1. 在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,則應通過屬性來寫。
  2. 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據。
  3. 有時會使用惰性初始化技術配置某份數據,這種情況下,需要通過屬性來讀取數據。

第8條:理解“對象等同性”這一概念

根據“等同性”(equality)來比較對象是一個非常有用的功能。不過,按照==操作符比較出來的結果未必是我們想要的,因為該操作比較的是兩個指針本身,而不是其所指的對象。應該使用NSObject協議中聲明的isEqual:方法來判斷兩個對象的等同性。一般來說, 兩個類型不同的對象總是不相等的(unequal)。某些對象提供了特殊的“等同性判定方法” (equality-checking method),如果已經知道兩個受測對象都屬于同一個類,那么就可以使用這種方法。以下述代碼為例:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat: @"Badger 123"];
BOOL equalA = (foo == bar); //NO
BOOL equalB = [foo isEqual: bar] ; // YES
BOOL equalC = [foo isEqualToString:bar]; // YES

大家可以看到==與等同性判斷方法之間的差別。NSString類實現了一個自己獨有的等同性判斷方法,名叫isEqualToString:。傳遞給該方法的對象必須是NSString,否則結果未定義(undefined)。調用該方法比調用isEquah方法快,后者還要執行額外的步驟,因為它不知道受測對象的類型。

NSObject協議中有兩個用于判斷等同性的關鍵方法:

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject類對這兩個方法的默認實現是:當且僅當其“指針值"(pointer value:理解為內存地址)完全相等時,這兩個對象才相等。若想在自定義的對象中正確覆寫這些方法,就必須先理解其約定(contract)。如果isEqual:方法判定兩個對象相等,那么其hash方法也必須返回同一個值。但是,如果兩個對象的hash方法返回同一個值,那么isEqual:方法未必會認為兩者相等。

比如有下面這個類:

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

我們認為,如果兩個EOCPerson的所有字段均相等,那么這兩個對象就相等。于是isEqual:方法可以寫成:

- (BOOL)isEqual:(id)object {
  if (self == object) return YES;
  if ([self class] != [object class]) return NO;
  EOCPerson *otherPerson = (EOCPerson *)object;
  if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
  if (_age != otherPerson.age) return NO; 
  return YES;
}

首先,直接判斷兩個指針是否相等。若相等,則其均指向同一對象,所以受測的對象也必定相等。接下來,比較兩對象所屬的類。若不屬于同一個類,則兩對象不相等。EOCPerson對象當然不可能與EOCDog對象相等。不過,有時我們可能認為:一個EOCPerson實例可以與其子類(比如EOCSmithPerson)實例相等。在繼承體系(inheritance hierarchy)isEqual:情況。最后,檢測每個屬性是否相等。只要其中有不相等的屬性,就判定兩對象不等,否則兩對象相等。

接下來該實現hash方法了。回想一下,根據等同性約定:若兩對象相等,則其哈希碼(hash:也叫做“散列”)也相等,但是兩個哈希碼相同的對象卻未必相等。這是能否正確覆寫isEqual:方法的關鍵所在。下面這種寫法完全可行:

- (NSUInteger)hash {
  return 1337;
}

不過若是這么寫的話,在collection中使用這種對象將產生性能問題,因為collection在檢索哈希表(hash table)時,會用對象的哈希碼做索引。假如某個collection是用set(collectionset在中文里都叫做“集合”,前者是Array、Dictionary、Set等數據結構的總稱。為避免混淆,保留這兩個詞的英文寫法。) 實現的, 那么set可能會根據哈希碼把對象分裝到不同的數組(這種數組在后文中也稱為“箱子"(bin))中。在向set中添加新對象時,要根據其哈希碼找到與之相關的那個數組,依次檢査其中各個元素,看數組中已有的對象是否和將要添加的新對象相等。如果相等,那就說明要添加的對象已經在set里面了。
再來看最后一種計算哈希碼的辦法:

- (NSUInteger)hash {
  NSUInteger firstNameHash =[_firstName hash】;
  NSUInteger lastNameHash = [_lastName hash];
  NSUInteger ageHash = _age;
  return firstNameHash ^ lastNameHash ^ ageHash;
}

這種做法既能保持較高效率,又能使生成的哈希碼至少位于一定范圍之內,而不會過于頻繁地重復。當然,此算法生成的哈希碼還是會碰撞(collision),不過至少可以保證哈希碼有多種可能的取值。編寫hash方法時,應該用當前的對象做做實驗,以便在減少碰撞頻度與降低運算復雜程度之間取舍。

特定類所具有的等同性判定方法

NSArray與NSDictionary類也具有特殊的等同性判定方法,前者名為“isEqualToArray:”,后者名為“isEqualToDictionary:”。如果和其相比較的對象不是數組或字典,那么這兩個方法會各自拋出異常。由于Objective-C在編譯期不做強類型檢査(strong type checking),這樣容易不小心傳入類型錯誤的對象,因此開發者應該保證所傳對象的類型是正確的。

如果經常需要判斷等同性,那么可能會自己來創建等同性判定方法,因為無須檢測參數類型,所以能大大提升檢測速度。自己來編寫判定方法的另一個原因是,我們想令代碼看上去更美觀、更易讀,此動機與NSString類“isEqualToString:”方法的創建緣由相似,純粹為了裝點門面。使用此種判定方法編出來的代碼更容易讀懂,而且不用再檢査兩個受測對象的類型了。

在編寫判定方法時,也應一并覆寫“isEqual:”方法。后者的常見實現方式為:如果受測的參數與接收該消息的對象都屬于同一個類,那么就調用自已編寫的判定方法,否則就交由超類來判斷。例如,在EOCPerson類中可以實現如下兩個方法:

- (BOOL)isEqualToPerson:(EOCPerson^)otherPerson { 
  if (self == object) return YES;
  if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO; 
  if (_age != otherPerson.age) return NO; 
  return YES;
}

- (BOOL)isEqual:(id)object {
  if ([self class] == [object class]) {
    return [self isEqualToPerson:(EOCPerson*)object];
  } else {
    return [super isEqual:object];
}

等同性判定的執行深度

創建等同性判定方法時,需要決定是根據整個對象來判斷等同性,還是僅根據其中幾個字段來判斷。NSArray的檢測方式為先看兩個數組所含對象個數是否相同,若相同,則在每個對應位置的兩個對象身上調用其“isEquth”方法。如果對應位置上的對象均相等,那么這兩個數組就相等,這叫做“深度等同性判定”(deep equality)。不過有時候無須將所有數據逐個比較,只根據其中部分數據即可判明二者是否等同(比方說唯一標識符)。
是否需要在等同性判定方法中檢測全部字段取決于受測對象。只有類的編寫者才可以確 定兩個對象實例在何種情況下應判定為相等。

容器中可變類的等同性

還有一種情況一定要注意,就是在容器中放入可變類對象的時候。把某個對象放入collection之后,就不應再改變其哈希碼了。前面解釋過,collection會把各個對象按照其哈希碼分裝到不同的“箱子數組”中。如果某對象在放入“箱子”之后哈希碼又變了,那么其現在所處的這個箱子對它來說就是“錯誤”的。要想解決這個問題,需要確保哈希碼不是根據對象的“可變部分”(mutable portion)計算出來的,或是保證放入collection之后就不再改變對象內容了。筆者將在第18條中解釋為何要將對象做成“不可變的"(immutable)。這里先舉個例子,此例能很好地說明其中緣由。

用一個NSMutableSet與幾個NSMutableArray對象測試一下,就能發現這個問題了。
set中居然可以包含兩個彼此相等的數組!根據set的語義是不允許出現這種情況的,然而現在卻無法保證這一點了,因為我們修改了set中已有的對象。若是拷貝此set,那就更糟糕了,復制過的set中又只剩一個對象了,此set看上去好像是由一個空set開始、通過逐個向其中添加新對象而創建出來的。這可能符合你的需求,也可能不符合。有的開發者也許想要忽略set中的錯誤,“照原樣”(verbatim)復制一個新的出來,還有的開發者則會認為這樣做挺合適的。這兩種拷貝算法都說得通,于是就進一步印證了剛才提到的那個問題:如果把某對象放入set之后又修改其內容,那么后面的行為將很難預料。

要點:

  1. 若想檢測對象的等同性,請提供“isEqual:”與hash方法。
  2. 相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同。
  3. 不要盲目地逐個檢測每條屬性,而是應該依照具體需求來制定檢測方案。
  4. 編寫hash方法時,應該使用計算速度快而且哈希碼碰撞幾率低的箅法。

第9條 以“類族模式”隱藏實現細節

“類族”(class cluster:也叫做類簇)是一種很有用的模式(pattern),可以隱藏“抽象基類”(abstract base class)背后的實現細節。Objective-C的系統框架中普遍使用此模式。比如UIButton:

+ (UIButton*)buttonWithType:(UIButtonType)type;

該方法所返回的對象,其類型取決于傳入的按鈕類型(button type)。然而,不管返回什么類型的對象,它們都繼承自同一個基類:UIButton。這么做的意義在于:UIButton類的使用者無須關心創建出來的按鈕具體屬于哪個子類,也不用考慮按鈕的繪制方式等實現細節。

類族模式可以靈活應對多個類,將它們的實現細節隱藏在抽象基類后面,以保持接口簡潔。用戶無須自己創建子類實例,只需調用基類方法來創建即可。

創建類族

現在舉例來演示如何創建類族。假設有一個處理雇員的類,每個雇員都有“名字”和 “薪水”這兩個屬性,管理者可以命令其執行日常工作。但是,各種雇員的工作內容卻不同。 經理在帶領雇員做項目時,無須關心每個人如何完成其工作,僅需指示其開工即可。

首先要定義抽象基類:

typedef NS_ENUM(NSUInteger EOCEmployeeType) { 
  EOCEmployeeTypeDeveloper, 
  EOCEmployeeTypeDesigner, 
  EOCEmployeeTypeFinance
};

@interface EOCEmployee : NSObject
@property (copy) NSString *name;
@property NSUInteger salary;
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
@end

@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type { 
  switch (type) {
  case EOCEmployeeTypeDeveloper:
    return [EOCEmployeeDeveloper new];
    break;

  case EOCEmployeeTypeDesigner:
    return (EOCEmployeeDesigner new]; 
    break;

  case EOCEmployeeTypeFinance:
    return (EOCEmployeeFinance new]; 
    break;
  }
}

- (void)doADaysWork {
  // Subclasses implement this.
}
@end

每個“實體子類"(concrete subclass) 都從基類繼承而來。例如:

@interface EOCEmployeeDeveloper : EOCEmployee 
@end

@implementation EOCEmployeeDeveloper
@end  

在本例中,基類實現了一個“類方法”,該方法根據待創建的雇員類別分配好對應的雇員類實例。這種“工廠模式”(Factory pattern)是創建類族的辦法之一。

如果對象所屬的類位于某個類族中,那么在査詢其類型信息(introspection:是某些面向對象語言可以在運行期檢視對象類型與屬性的一種功能。中文譯作 “內省”或“類型內省”)時就要當心了。你可能覺得自己創建了某個類的實例,然而實際上創建的卻是其子類的實例。在Employee這個例子中,[employee isMemberOfClass:[EOCEmployee class]]返回的卻是NO,因為employee并非Employee類的實例,而是其某個子類的實例。

Cocoa里的類族

系統框架中有許多類族。大部分collection類都是類族(作者有時把“類族中的抽象基類”(the abstract base class of a class cluster)直接稱為“類族”。這句話實際上是說,大部分collection類都是某個類族中的抽象基類),例如NSArray與其可變版本NSMutableArray。這樣看來,實際上有兩個抽象基類,一個用于不可變數組,另一個用于可變數組。盡管具備公共接口的類有兩個,但仍然可以合起來算作一個類族(在傳統的類族模式中,通常只有一個類具備“公共接口”(public imerface),這個類就是類族中的抽象基類)。不可變的類定義了對所有數組都通用的方法,而可變的類則定義了那些只適用于可變數組的方法。兩個類共屬同一類族,這意味著二者在實現各自類型的數組時可以共用實現代碼,此外,還能夠把可變數組復制為不可變數組,反之亦然。

在使用NSArray的alloc方法來獲取實例時,該方法首先會分配一個屬于某類的實例, 此實例充當“占位數組”(placeholder array)。該數組稍后會轉為另一個類的實例,而那個類則是NSArray的實體子類。這個過程稍顯復雜,其完整的解釋已經超出本書范圍。

像NSArray這樣的類的背后其實是個類族(對于大部分collection類而言都是這樣),明白這一點很重要,否則就可能會寫出下面這種代碼:

id maybeAnArray = /* ??? */;
if ([maybeAnArray class) == [NSArray class]) {
  //Will never be hit
}

你要是知道NSArray是個類族,那就會明白上述代碼錯在哪里:其中的if語句永遠不可能為真。[maybeAnArray class]所返回的類絕不可能是NSArray類本身,因為由NSArray的初始化方法所返回的那個實例其類型是隱藏在類族公共接口(public facade)后面的某個內部類型(internal type)。

若想判斷出某個實例所屬的類是否位于類族之中或者判斷某對象是否位于類族中,不要直接檢測兩個“類對象”是否等同,而應該改用類型信息查詢方法(introspectionmethod):

id maybeAnArray = /*??? */;
if([maybeAnArray isKindOfClass:[NSArray class])) {
  //Will be hit
}

我們經常需要向類族中新增實體子類,不過這么做的時候得留心。在Employee這個例子中,若是沒有“工廠方法”(factory method)的源代碼,那就無法向其中新增雇員類別了。 然而對于Cocoa中NSArray這樣的類族來說,還是有辦法新增子類的,但是需要遵守幾條規則。這幾條規則如下。

  1. 子類應該繼承自類族中的抽象基類;
  2. 子類應該定義自己的數據存儲方式:因為NSArray本身只不過是包在其他隱藏對象外面的殼,它僅僅定義了所有數組都需具備的一些接口。對于這個自定義的數組子類來說, 可以用NSArray來保存其實例。
  3. 子類應當覆寫超類文檔中指明需要覆寫的方法:在每個抽象基類中,都有一些子類必須覆寫的方法。比如說,想要編寫NSArray的子 類,就需要實現countobjectAtlndex:方法。像lastObject這種方法則無須實現,因為基類可以根據前兩個方法實現出這個方法。

在類族中實現子類時所需遵循的規范一般都會定義于基類的文檔之中,編碼前應該先看看。

要點:

  1. 類族模式可以把實現細節隱藏在一套簡單的公共接口后面。
  2. 系統框架中經常使用類族。
  3. 從類族的公共抽象基類中繼承子類時要當心,若有開發文檔,則應首先閱讀。

第10條:在既有類中使用關聯對象存放自定義數據

有時需要在對象中存放相關信息。這時我們通常會從對象所屬的類中繼承一個子類,然后改用這個子類對象。還有一種方法,這就是“關聯對象"(Associated Object)。

可以給某對象關聯許多其他對象,這些對象通過“鍵”來區分。存儲對象值的時候,可 以指明“存儲策略”(storage policy),用以維護相應的“內存管理語義”。存儲策略由名為objc_AssociationPolicy的枚舉所定義,表2-1列出了該枚舉的取值,同時還列出了與之等效的屬性:假如關聯對象成為了屬性,那么它就會具備對應的語義。


對象關聯類型.png

下列方法可以管理關聯對象:

  1. objc_setAssociatedObject(id object, void *key, id value, objc AssociationPolicy policy):此方法以給定的鍵和策略為某對象設置關聯對象值。
  2. objc_getAssociatedObject(id object, void *key):此方法根據給定的鍵從某對象中獲取相應的關聯對象值。
  3. objc_removeAssociatedObjects(id object):此方法移除指定對象的全部關聯對象。

我們可以把某對象想象成NSDictionary,把關聯到該對象的值理解為字典中的條目,于是,存取關聯對象的值就相當于在NSDictionary對象上調用[object setObject:value forKey:key][object objectForKey:key]方法。然而兩者之間有個重要差別:設置關聯對象時用的鍵(key)是個‘不透明的指針’(opaque pointer) ”。如果在兩個鍵上調用isEqual:方法的返回值是YES,那么NSDictionary就認為二者相等;然而在設置關聯對象值時,若想令兩個鍵匹配到同一個值,則二者必須是完全相同的指針才行。鑒于此,在設置關聯對象值時, 通常使用靜態全局變量做鍵。

關聯對象用法舉例

開發iOS時經常用到UIAlertView類,如果想在同一個類里處理多個警告信息視圖,那么代碼就會變得更為復雜,我們必須在delegate方法中檢査傳入的alertView參數,并據此選用相應的邏輯。要是能在創建警告視圖的時候直接把處理每個按鈕的邏輯都寫好,那就簡單多了。這可以通過關聯對象來做。創建完警告視圖之后,設定一個與之關聯的‘塊"(block)。以這種方式改寫之后,創建警告視圖與處理操作結果的代碼都放在一起了,這樣比原來更易讀。但是, 采用該方案時需注意:塊可能要捕獲(capture)某些變量,這也許會造成“保留環”(retain cycle)。

正如大家所見,這種做法很有用,但是只應該在其他辦法行不通時才去考慮用它。若是濫用,則很快就會令代碼失控,使其難于調試。“保留環”產生的原因很難査明,因為關聯對象之間的關系并沒有正式的定義(formal definition),其內存管理語義是在關聯的時候才定義的,而不是在接口中預先定好的。使用這種寫法時要小心,不能僅僅因為某處可以用該寫 就一定要用它。想創建這種UIAlertView還有個辦法,那就是從中繼承子類,把塊保存為子類中的屬性。筆者認為:若是需要多次用到alert視圖,那么這種做法比使用關聯對象要好。

要點:

  1. 可以通過“關聯對象”機制來把兩個對象連起來。
  2. 定義關聯對象時可指定內存管理語義,用以模仿定義屬性時所采用的“擁有關系”與“非擁有關系”。
  3. 只有在其他做法不可行時才應選用關聯對象,因為這種做法通常會引入難于査找的bug。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370