標籤:

閱讀Objective-C編程全解

面向對象的編程

具備以下特徵的東西可稱為對象:

  1. 可認為分辨出這是一個對象
  2. 擁有屬性
  3. 能夠向其他對象發送消息
  4. 能夠接收消息,並作出相應的處理
  5. 消息的處理是通過對象的方法完成的

抽象化(abstraction)指的是儘可能地不考慮相關細節,只關注對象的核心和本質。

對象擁有屬性(也可以說是狀態),但屬性是怎麼被定義的呢?

對象的屬性一般被定義為指向其他對象的指針,這個指針叫做實例變數(instance variable, 簡稱為ivar)或變數。

對象和對象之間一般是通過一個對象的某個屬性是另外一個對象的變數來建立關係的。

沒有引用關係的兩個對象之間無法發送消息。Objective-C中把連接對象的變數稱為輸出口(outlet)。

我們可以把具備相同變數和方法的對象提煉出來,做成「模版」。這樣以後就可以使用「模版」來創建各個具體的對象。

這種「模版」就是類(Class)。

用類創建對象的過程叫做實例化(instantiation),生成的對象叫做實例對象(instance object),或簡稱為實例(instance)。

模塊獨立性的劃分原則是只對外提供最小限度的介面信息,內部實現不對外公開。也就是把模塊做成一個黑盒(black box)。

這個原則叫做信息隱蔽(information hiding)或封裝(encapsulation)。

類公開給外部的,關於如何使用這個類的信息叫做介面(interface)。介面中定義了這個類所包含的實例變數

Objective-C程序設計

函數是通過函數名來區分的,消息則是通過消息名來區分的。消息名又稱為消息選擇器(message selector),選擇器(selector)或方法(method)。

在Objective-C裡面,類介面的聲明以編譯指令@interface開頭並以@end結束。

所有的Objective-C編譯指令(compiler directive)都是以@字元開頭,以便和C語言的字元串區分。

下面幾種情況,建議在Objective-C中使用C語言的函數:

  1. 想使用成熟的C語言函數模塊時
  2. 想使用以C語言聲明的介面時,例如Unix的系統調用等
  3. 和面向對象沒有關係,用於數學,計算等時
  4. 類定義時
  5. 對速度有較高要求時

類和繼承

通過擴展或者修改即有類來定義新類的方法叫做繼承(inheritance)。

在繼承關係中,被繼承的類稱為父類(superclass),通過繼承關係新建的類稱為子類(subclass)。

繼承意味著子類繼承了父類的所有特性,父類的數據成員和成員函數自動成為了子類的數據成員和成員函數。

除此之外,子類還可以:

  1. 追加新的方法
  2. 追加新的實例變數
  3. 重新定義父類中的方法(override)

self指的是收到當前消息的實例變數,因此,就算是同一個程序,根據實例的類的不同,實際調用的方法也可能不相同。

super調用的是父類的方法,而至於到底調用了哪個方法則是由編譯時類的繼承關係決定的。

制定初始化方法(designated initializer)就是指能確保所有實例變數都能被初始化的方法。

對象的類型和動態綁定

動態綁定(dynamic binding)指的就是在程序執行時才確定對象的屬性和需要響應的消息。

在面向對象的程序設計理論中,多態(polymor phism)是指,同一操作作用於不同的類的實例時,將產生不同的執行結果。

即不同的類的對象收到相同的消息時,也能得到不同的結果。

靜態類型檢查的總結:

對於id類型的變數,調用任何方法都能夠通過編譯(當然調用不恰當的方法會出現運行時錯誤)

id類型的變數和被定義為特定類的變數之間是可以相互賦值的

被定義為特定類對象的變數(靜態變數),如果調用了類或父類中未定義的方法,編譯器就會提示警告

若是靜態類型的變數,子類類型的實例變數可以賦值給父類類型的實例變數

若是靜態類型的變數,父類類型的實例變數不可以賦值給子類類型的實例變數

若是判斷到底是哪個類的方法被執行了,不要看變數所聲明的類型,而要看實際執行時這個變數的類型

id類型並不是(NSObject*)類型

面向對象的語言中Smalltalk是一種弱類型語言,程序中不做變數類型說明,系統也不做類型檢查。

C++和Java是強類型語言,編譯時會進行類型檢查,以保證類型兼容。

C++和Java的多態是基於類層次結構的,可以通過子類重寫父類中的方法來實現多態。

這種類型的語言無法實現沒有繼承關係的多態。

消息選擇器(message selector)中並不包含參數和返回值的類型的信息。

消息選擇器和這些類型信息結合起來構成簽名(signature),簽名被用於在運行時標記一個方法。

介面文件中方法的定義也叫做簽名。

Objective-C中選擇器相同的消息,參數和返回值的類型也應該是相同的。

通過編譯指令@class告知編譯器Volume是一個類名。這種寫法被叫做類的前置聲明(forward declaration)。

通過使用@class可以提升程序整體的編譯速度。

但要注意的是,如果新定義的類中要使用原有類的具體成員或方法,就一定要引入原有類的頭文件。

用於讀取,修改實例對象屬性的方法稱為訪問器或訪問方法。讀取屬性值的方法稱為getter方法,修改屬性值的方法稱為setter方法。

能否從外部訪問實例變數決定了訪問的可見性(visibility)。

@private

@protected

@public

@package

讓一個變數對外不可見有兩種辦法,一種是把變數的可見性屬性設為@private,另一種就是把變數定義在實現文件中。

相比之下第二種方法封裝性更好一點,同時也可以清楚地表明這些變數對外時不可見的。

面向對象的語言中對類有兩種認識,一種是認為類只作為類型的定義,程序運行時不作為實體存在;

另外一種是認為類本省也作為一個對象存在。我們把另一種中定義中的類的對象叫做類對象(class object)。

這種情況下類定義就分成了兩部分,一部分定義所生成的實例的類型,另外一部分則定義類自身的行為。

Objective-C和SmallTalk都把類作為對象看待,而在C++中,類只被作為類型定義使用。

類對象有自己的方法和變數,分別被稱為類方法和類變數。

至今為止我們一直把類的實例變數和方法稱為實例變數和實例方法,這樣可以和類變數和類方法予以區分。

通過向類發送消息可以生成實例對象,那麼類對象自身時什麼時候生成的呢?

類對象是程序執行時自動生成的,每個類只有一個類對象,不需要手動生成。

而類方法alloc定義在哪個類裡面呢?它定義在根類NSObject中,所有的類都繼承了NSObject,所以不用擔心使用alloc生成類對象的問題。

也可以通過為類對象定義新的方法來完成實例變數的生成和初始化。

每個類的所有實例對象都可以使用類方法。類方法可以訪問類對象管理的變數。

Objective-C中還專門定義了一個Class類型用來表示類對象,所有的類對象都是Class類型。

Class和id一樣都是指針類型,只是一個地址,並不需要了解實際指向的內容。

Nil被用來表示空指針(是Class,而不是對象),實際的值是0。

類方法在執行時用self代表了類對象自身,因此可以通過給self發送消息的方式來調用類中的其他類方法。

同實例方法一樣,也要注意self實際指向的類。

調用父類的類方法時,可以使用super。

Objective-C通過在實現文件中定義靜態變數(static variables)的方法來代替類變數。

Objective-C中類變數原則上只在類的內部實現中使用,在進行設計時要充分考慮到這一點。

在繼承存在的情況下,我們要儘可能地避免使用靜態類型把代碼寫「死」。

比如下面是調用類方法初始化的一行語句:

[[Volume alloc] initWithMin:a max:b setp:s];

比起改寫法,一種更好的寫法是:

[[[self class] alloc] initWithMin:a max:b setp:s];

新寫法中使用[self class]替代了具體的類名Volume。class是類方法,返回self所屬的類對象。

這樣修改之後,子類也可以原封不動地使用這行代碼。

基於引用計數的內存管理

Cocoa環境的Objective-C提供了一種動態的內存管理方式,稱為引用計數(reference counter)。

這種方式會跟蹤每個對象被引用的次數,當對象的引用次數為0時,系統就會釋放這個對象所佔用的內存。

本書把這種內存管理方式稱為基於引用計數的內存管理。

比引用計數內存管理更高級一點的就是自動引用計數(Automatic Reference Counting,簡寫為ARC)的內存管理。

除了ARC外,Objective-C2.0還引入了一種自動內存管理機制--垃圾回收。

使用垃圾回收時,就不再需要通過引用計數來管理創建的對象,系統會自動識別哪些對象仍在使用,哪些對象可以回收。

Cocoa環境的Objective-C提供了一種對象自動釋放(autorelease)的機制。

這種機制的基本思想是把所有需要發送release消息的對象都記錄下來,

等到需要釋放這些對象時,會給這些對象一起發送release消息。

其中,類NSAutoreleasePool(自動釋放池)就起到了記錄的作用。

- (id) autorelease

其返回值是接受消息的對象。

本書中把自動釋放池中登錄的實際上相當於放棄了所有權的對象稱為臨時對象。

自動釋放池的典型用法如下所示。

id pool = [[NSAutoreleasePool alloc] init];

/*

在此進行一系列操作。

給臨時對象發送autorelease消息。

*/

[pool release]; /* 銷毀自動釋放池,自動釋放池中所有的對象也被銷毀 */

Objective-C中的很多類都提供了生成臨時對象的類方法。

這種類方法的命名規則是,不以init開頭,而以要生成的對象的類型作為開頭。

同綜合使用alloc,init創建對象的方法相比,通過這種方法生成的對象的所有者不是調用類方法的對象。

這種生成臨時對象的類方法,在Objective-C中稱為便利構造函數(convenience constructor)或便利構造器。

一些面向對象的語言中會把生成對象的函數叫做構造函數,以和普通的函數進行區分。

把在內部調用別的構造函數而生成的構造函數叫做便利構造函數。

在Objective-C語言中便利構造函數指的就是這種利用alloc,init和autorelease生成臨時對象的類方法。

典型的圖形界面應用程序(GUI程序)往往會在休眠中等待用戶操作,例如,操作滑鼠或鍵盤點擊菜單,按鈕等。

在用戶發出動作之前,程序將一直處於空閑狀態。當點擊事件發生後,程序會被喚醒並開始工作,

執行某些必要的操作以響應這一事件。處理完這一事件後,程序又會返回到休眠狀態並等待下一個事件的到來。

這個過程被叫做運行迴路(runloop)。

Cocoa在程序開始事件處理之前會隱式創建一個自動釋放池,並在事件處理結束後銷毀該自動釋放池。

所以,程序員在進行Cocoa的GUI編程時,就算不手動創建自動釋放池,也可以使用臨時對象。

內存中常量對象(類對象,常量字元串對象等)的空間分配與其他對象不同,他們沒有引用計數機制,用於不能釋放這些對象。

給這些對象發送消息retainCount後,返回的是NSUIntegerMax(其值為0xffffffff,被定義為最大的無符號整數)。

有的時候,我們可能會需要某個類僅能生成一個實例,程序中訪問到這個類的對象時使用的都是同一個實例對象。

在設計模式中這種情況稱為單例模式(singleton)。

使用常量修飾符const能夠表明指針所指向的內容是否可以修改,在一定程度上可以提高程序的安全性和可靠性。

因此,在Objective-C中也請盡量使用const。

採用引用計數方式管理內存時需要程序員管理所有生成對象的所有權(object ownership)。

程序員需要清楚地了解獲得/放棄對象所有權的時機,並在適當的位置插入retain,release和autorelease函數。

ARC(Automatic Referece Counting,自動引用計數)是一個編譯期技術,利用此技術可以簡化Objective-C在內存管理方面的工作量。

s = w;

=>

[w retain];

id _old = s;

s = w;

[_old release];

採用引用計數方法管理內存時,創建對象時就會擁有這個對象的所有權。

例如,使用以alloc開頭的類方法生成對象,並且使用以init開頭的類方法來初始化對象的時候,就會獲得這個對象的所有權。

另外,使用名稱中包含new,copy或mutableCopy的方法複製對象的時候,也會獲得這個對象的所有權。

採用引用計數方法管理內存時,如果不使用alloc/init/new/copy/mutableCopy這些方法,或者不使用retain來保留一個對象,就不能成為對象的所有者。

另外,只有使用release或者autorelease,才能夠放棄這個對象的所有權。

這些規定被叫做所有權策略(ownership policy)。換句話說,對象實例該有誰來釋放,並不取決於編程語言方面的要求,而是編程邏輯決定的。

另一方面,由於ARC允許混合鏈接手動內存管理和自動內存管理的代碼,所以針對到底哪個方法同對象的生成和複製相關這一問題,

不能使用"init ...方法名"這種命名方式來區分,而需要定義能夠讓編譯器明確區分的方法。

同對象生成相關的方法集合叫做方法族(method family)。

目前為止一共定義了5個方法族,可被保持的對象(retainable object)指的是Objective-C的對象或者block對象。

  1. alloc方法族:以alloc開頭的方法表示調用者對被創建的對象擁有所有權,返回的對象必須是可以被retain的。
  2. copy方法族:以copy開頭的方法表示調用者對被創建的對象擁有所有權,返回的對象必須是可以被retain的。
  3. mutableCopy方法族:以mutableCopy開頭的方法表示調用者被創建的對象擁有所有權,返回到對象必須是可以被retain的。
  4. new方法族:以new開頭的方法表示調用者對被創建的對象擁有所有權,返回的對象必須是可以被retain的。
  5. init方法族:以init開頭的方法必須被定義為實例方法,它一定返回id類型或父類,子類的指針。

除了以init開頭的方法之外,以alloc/new/copy/mutableCopy開頭的方法都既可以是類方法也可以是實例方法。

依照Objective-C中的命名慣例,調用以alloc/new/copy/mutableCopy/init開頭的方法時,需要將對象所有權返回給調用端,由調用端release生成的對象。

另一方面,如果你想由調用端來release一個方法返回的對象,而這個方法的名字又不是以上述關鍵字開頭的話,

ARC就有可能不會釋放這個對象,從而造成內存泄漏。

給方法命名時必須遵循命名規則。大家可能會在無意中使用new或copy開頭的名字作為方法名,這是不正確的,有可能會照成誤釋放,

也有可能在編譯時提示警告或錯誤。因此要切記嚴守內存管理相關的函數命名規則。

使用ARC編程時的注意事項:

  1. 不能在程序中定義和使用下面的這些函數:retain,release,autorelease和retainCount
  2. 使用@autoreleasepool代替NSAutoreleasePool
  3. 方法命名必須遵循命名規則,不能隨意定義以alloc/init/new/copy/mutableCopy開頭並且和所有權操作無關的方法
  4. 不用在dealloc中釋放實例變數(但可以在dealloc中釋放資源),也不需要調用[super dealloc]
  5. 編譯代碼時使用編譯器clang,並加上編譯選項 -fobjc-arc

兩個對象相互引用,或者像A持有B,B持有C,C持有A這樣多個對象的引用關係形成了環的現象,叫做循環引用或循環保持(retain cycle)。

循環引用會造成內存泄漏,只有打破循環引用關係才能夠釋放內存。

弱引用是通過存儲一個指向對象的指針創建的,且不保留對象。Objective-C中用__weak修飾符來定義弱引用。

通常聲明的未加 __weak修飾符的變數都是強引用(strong reference)類型的變數,

聲明時也可以通過加上 __strong 修飾符來明示變數是強引用類型。函數和方法的參數也是強引用類型。

對強引用變數賦值之後,編譯器會自動插入變數引用計數加1的代碼。

當強引用變數不再使用某個對象時,編譯器會自動插入變數引用計數減1的代碼。

與此相對,弱引用的情況下,無論是對變數賦值還是解除引用,變數的引用計數都不會發生變化。

強引用和弱引用都會被隱式地初始化為nil。

這種用於修飾指針類型變數的修飾符被叫做生命周期修飾符(lifetime qualifier)或所有權修飾符。

生命周期修飾符一共有四種,除了我們已經介紹的 __strong,__weak之外,還有__autoreleasing, __unsafe_unretained。

弱引用會在其指向的實例對象被釋放後自動變為nil,這就是弱引用的自動nil化功能。

也就是說,即使弱引用指向的實例對象在不知不覺中被釋放了,弱引用也不會變成野指針。

對象圖中的環路就是循環引用產生的原因。使用ARC的時候應該盡量保證對象之間的關係呈樹形結構,避免一個對象同時被兩處引用(環形成的必要條件)。

使用ARC的時候,如果既不想保持賦值的對象,也不想賦值的對象在釋放後被自動設為nil,可以使用生命周期修飾符 __unsafe_unretained。

__unsafe_unretained所修飾的變數稱為非nil化的弱指針,也就是說,如果所指向的內存區域被釋放了,這個指針就是一個野指針了。

在使用ARC的程序中,id類型和void*類型之間不能進行轉型。就算加了 __unsafe_unretained修飾符,轉型操作在編譯時也會報錯。

這是因為,iOS世界中主要有兩種對象:Ojbective-C對象和Core Foundation對象。其中,Core Foundation類型的對象不再ARC的管理範疇內。

因此,當轉換這兩種類型(一種有ARC管理,一種沒有ARC管理)時,就需要告訴編譯器怎麼處理對象的所有權。

為了解決這一問題,可以使用 __bridge修飾符來實現id類型與void*類型的相互轉換。

有一種減少野指針出現的方法是,當不再使用傳入的對象時,將其賦值為nil。典型的做法就是在dealloc方法中進行處理。

當一個函數或方法有多個返回值時,我們可以通過函數或方法的參數傳入一個指針,將返回值寫入指針所指向的空間。

C語言中把這種方法叫做按引用傳遞(pass-by-reference)。

Objective-C的ARC中也有類似的方法,但採用了和C語言不通的實現方式,叫做寫回傳(pass-by-writeback)。

垃圾回收

垃圾回收指的是在程序運行過程中,檢查是否有不再使用的對象,並自動釋放它們所佔用的內存,通常被簡稱為GC。

內存的檢查和回收都是由垃圾收集器(garbage collector)完成的。

屬性聲明

一般來說,屬性(property)指的是一個對象的屬性或特性。

本章中要說明的屬性聲明是一種聲明變數為屬性的語法,該語法同時還引入了一種更簡單的訪問屬性的方法。

本章中要介紹的屬性聲明指的就是在介面文件中聲明實例對象到底有哪些屬性。

屬性聲明的一些規則總結:

  1. 能夠生成訪問方法
  2. 自動生成實例變數
  3. 更簡單地調用訪問方法
  4. 屬性的內省(introspection)

一個類包含的方法和屬性的相關信息也可以被看作是一種元信息。在面向對象的語言中,通過程序動態訪問這些元信息的功能叫做內省(introspection)或者反射(reflection)。

respondsToSelector:就是一個自省的例子,通過它可以檢查對象是否包含某個方法。

Ojbective-C 2.0會在編譯時使用點操作符訪問屬性的過程理解為訪問方法的調用。

因為調用的是訪問方法,所以無論對應的實例變數是否存在,只要訪問方法存在,就都可以通過點操作符訪問屬性。

點操作符只能用於類類型的實例變數,不能對id類型的變數應用點操作符。因為沒指定類型的情況下,編譯器無法判斷是否存在屬性對應的訪問方法。

類NSOjbect和運行時系統

作為一門動態編程語言,Objective-C有很多動態的特性,因此,Objective-C不僅需要編譯環境,同時還需要一個運行時系統(runtime system)來執行編譯好的代碼。

運行時系統扮演的角色類似於Objective-C的操作系統,它負責完成對象生成,釋放時的內存管理,為發來的消息查找對應的處理方法等。

至今為止我們把選擇器(方法名)和消息關鍵字放在一起進行了說明。程序中的方法名(選擇器)在編譯後會被一個內部標識符所代替,這個內部標識符所對應的數據類型就是SEL類型。

Ojbective-C為了能夠在程序中操作編譯後的選擇器,定義了@selector()指令。通過使用@selector()指令,就可以直接引用編譯後的選擇器。

可以使用SEL類型的變數來發送消息,為此,NSObject中準備了如下方法。

- (id) performSelector: (SEL) aSelector

向消息的接收者發送aSelector代表的消息,返回這個消息執行的結果。

- (id) performSelector:(SEL) aSelector withObject: (id) anObject

向消息的接收者發送aSelector代表的消息,消息的參數為anObject,返回這個消息執行的結果。

例如,下面兩個消息表達式進行的處理是相同的。

[target description];

[target performSelector: @selector(description)];

下面的例子展示了如何根據條件動態決定執行哪個方法。

SEL method = (cond1) ? @selector(activate:) : @selector(hide:);

id obj = (cond2) ? myDocument : defaultDocument;

[target performSelector:method withObject:obj];

通過SEL類型來指定要執行的方法這就是Objecitve-C消息發送的方式。也正是通過這種方法,才實現了Objective-C的動態性。

對象收到一個消息後執行哪個方法是被動態決定的。

所有的實例變數都存在一個Class類型的isa變數,他就是類對象。

當收到消息後,運行時系統會檢查類內是否有和這個消息選擇器相同的方法,如果有就執行對應的方法,如果沒有就通過類對象中指向父類的指針來查找父類中是否有對應的方法。

運行時系統內部會緩存一個散列表,標中記錄著某個類擁有和什麼樣的選擇器相對應的方法,方法被定義在何處等信息。

這樣一來,當下次再收到同樣的消息時,直接利用上次緩存的信息即可,就不需要再重頭進行搜索了。

NSObject中定義了可以動態查詢一個對象是否能夠響應某個選擇器的方法。

- (BOOL) respondsToSelector: (SEL) aSelector

查詢消息的接收者中是否有能夠響應aSelector的方法,包括從父類繼承來的方法。如果存在的話,返回YES。

+ (BOOL)instancesRespondToSelector: (SEL) aSelector

查詢消息的接收者所屬的類中能否有能夠響應aSelector的實例方法,包括從父類繼承來的方法。如果存在的話,返回YES。

通過使用下面額方法,可以獲得某個對象持有的方法的函數指針, 這些方法都被定義在NSObject中。

- (IMP) methodForSelector: (SEL) aSelector

搜索和指定選擇器相對應的方法,並返回指向該方法實現的函數指針。實例對象和類對象都可以使用這個方法。

對實例對象使用時,會返回實例方法對應的函數,對類對象使用時,會返回類對象對應的函數。

+ (IMP) instanceMethodForSelector: (SEL) aSelector

搜索和指定選擇器相對應的實例方法,並返回指向該實例方法實現的函數指針。

IMP是"implement"的縮寫,它是一個函數指針,指向了方法實現代碼的入口。

IMP的定義為 typedef id (*IMP)(id, SEL, ...);

這個被指向的函數包括id(self指針), 調用的SEL(方法名),以及其他一些參數。

例如下面這樣一個方法。

- (id) setBox: (id) obj1 title: (id) obj2;

foo是這個方法所屬類的一個實例變數。獲取指向setBox的函數指針,並通過該指針進行函數調用的過程如下。

IMP funcp;

funcp = [foo methodForSelector:@selector(setBox:title:)];

xyz = (*funcp)(foo, @selector(setBox:title:), param1, param2);

通過這個例子能夠看出,調用方法對應的函數時,除了方法聲明時的參數外,還需要把消息接受對象和消息的選擇器作為參數。

雖然沒有明確聲明,但方法內部野可以訪問這兩個參數,因此這兩個參數也被叫做隱含參數(hidden arguments)。

第一個參數消息的接收者實際上就是self, 第二個參數選擇器可以通過 _cmd這個變數來訪問。

因為沒有明確指定IMP的方法參數的類型, 所以編程的時候可以把一個實際的函數指針賦值給IMP類型的變數(需要通過cast進行類型轉換)。

相當於類對象的類的對象是存在的,而類對象的類就被叫做元類(metaclass)。

實例對象(instance object)所屬的類是class,類對象(class object)所屬的類是metaclass。

任何一個類對象都是繼承了根類的元類對象的一個實例。也就是說,類對象可以執行根類對象的實例方法。

元類總結:

  1. 所有類的實例對象都可以執行根類的實例方法
  2. 所有類的類對象都可以執行根類的類方法
  3. 所有類的類對象都可以執行根類的實例方法

Application框架利用SEL類型能夠在運行時動態決定執行哪個方法的這種原理實現了GUI控制項對象間的通信,叫做目標-動作模式(target-action paradigm)。

Application框架的目標-動作模式在發送消息時使用了下面這種形式定義的方法,即只有一個id類型的參數,沒有返回值。這種形式的方法叫做動作方法(action method)。

- (void) XXXXXX: (id) sender;

Foundation框架中的常用的類

Objective-C中的類分為可變(mutable)類和不可變(immutable)類。可變類的實例對象稱為可變對象,指的是創建後能夠改變其內容或狀態的對象。

不可變的實例對象稱為不可變對象,指的是創建後不可更改其內容的對象。對象是否可變的屬性稱為可變性(mutablility)。

可變類時不可變類的子類。例如類NSMutableArray就是類NSArray的子類,所以可變類的實例對象可直接作為不可變類的實例對象來使用。

可以使用mutableCopy方法為不可變對象創建一個可變的副本。這個方法定義在NSObject中,

只要是成對出現的可變類和不可變類的對象,就都可以使用這個方法。

- (id) mutableCopy

給不可變對象發送mutableCopy消息的話,會生成一個和不可變對象內容一致的可變對象。

使用基於引用計數的內容管理方式時,新生成的實例對象的所有者就是mutableCopy消息的發送者。

使用@「XXX」這種方式定義的字元串時常量對象(constant object),可被作為NSString的對象使用。

字元串常量從程序的執行開始到終止一直存在,調用release方法或者垃圾回收都不能釋放字元串常量。

範疇

實現某個類的一部分方法的模塊叫做範疇或類別(category)。一個類既可以不使用任何範疇,也可以由多個範疇構成。調用範疇中定義的方法和調用普通方式定義的方法一樣。

範疇的語法如下:

範疇的聲明 =>

@interface 類名(範疇名)

方法的聲明;

...

@end

範疇的實現 =>

@implementation 類名(範疇名)

方法的定義;

...

@end

範疇的介面部分需要遵循以下幾個原則。下面提到的引用既包括引用同一個文件中的內容也包括引用其他文件中的內容。

  1. 範疇的介面部分必須引用主文件的介面文件
  2. 範疇的實現部分必須引用對應的介面文件
  3. 使用範疇中的方法時必須引用這個方法所在的頭文件

通過範疇可以為一個類追加新的方法但不能追加實例變數。

但是,利用Objective-C語言的動態性,並藉助運行時(runtime)的功能,就可以為已存在的實例增加實例變數,這個功能叫做關聯引用(associative references)。

將這個功能和範疇組合在一起使用,即使不創建子類,也能夠對類進行動態擴展。

下面我們來看一下添加關聯和檢索關聯用的兩個方法,這兩個方法的定義在頭文件objc/runtime.h中。

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

這個方法是為對象Object添加以key指定的地址作為關鍵字,以value為值的關聯引用,第四個參數policy指定關聯應用的存儲策略。

通過將value指定為nil,就可以刪除key的關聯。

id objc_getAssociatedObject(id object, void *key)

返回object以key為關鍵字關聯的對象。如果沒有關聯到任何對象,則返回nil。

Objective-C中也提供了運行時斷開關聯的函數。

void objc_remoeAssociatedObjects(id object)

斷開object的所有關聯。

抽象類和類簇

在定義子類時,在子類中只聲明那些需要具體定義的方法,這樣的類就是抽象類(abstract class),或者稱為虛類(virtual class)

在Objective-C中,即使是抽象類,只要能夠使用alloc方法,也可以生成類實例。但這麼做沒有什麼實際意義。

例如,類NSObject可以生成實例,但幾乎沒有什麼用處,只能為子類提供有用的公共方法。

從這點看來,我們可以說類NSObject具有很強的抽象類特質。

類簇(class cluster)就是定義相同的介面並提供相同功能的一組類的集合。僅公開介面的抽象類也稱為類簇的公共類(public class)。

各個具體類的介面由公共類的介面抽象化,並被隱藏在簇的內部。這些具體類不能被直接使用,一般會被作為公共類的子類來實現,

所以有時也稱他們為私有子類。

協議

大多數情況下,對象的主要作用是表示所處理的消息的類型。而表示對象的作用和行為的方法的集合就稱為協議(protocol)。

使用協議的情況下,如果類實現了該協議聲明的所有方法,我們就說類遵循(conform)該協議。

而它的子類實例因為繼承關係也擁有了這些協議方法。當類適用於某個協議時,它的實例也適用於這個協議。

協議的聲明

@protocol 協議名

聲明方法;

...

@end

協議的採用

@interface 類名: 超類名 <協議名>

{

聲明介面變數;

}

聲明方法;

...

@end

像這樣,類的介面聲明制定了某個協議的情況,我們稱為類採用(adopt)了該協議。

採用協議的類及其子類也就成為了該協議適用的類,而子類也可以同時使用別的協議。

協議的繼承

@protocol 協議名1 <協議名2>

聲明方法;

...

@end

代碼中只關注協議和抽象類,而沒有具體類名的對象稱為匿名對象(anonymous object)。

使用編譯器命令符@protocol()後,就可以獲得表示指定協議數據的指針。@protocol()參數中包含類型(Protocol*),可以帶入變數。

+ (BOOL) conformsToProtocol: (Protocol *) aProtocol

aProtocol參數指定的協議和類適用時,返回YES。

- (BOOL) conformsToProtocol: (Protocol *) aProtocol

接收器類和參數aProtocol指定的協議適用時,返回YES

if ( [obj conformsToProtocol: @protocol(NSLocking) ] ) ...

Objective-C2.0中規定了一項新的功能:協議列舉的方法中,分為必須實現的方法和可選擇實現的方法,也就是說可以指定不同實現的方法。

在協議聲明中,編譯器命令符@optional和@required可用來設定其後出現額方法時可選的還是必選的。

對象的複製及存儲

用與一個實例對象相同的內容,生成一個新的對象,這個過程一般稱為複製。

其中,只複製對象的指針稱為淺複製(shallow copy),而複製具有新的內存空間的對象則稱為深複製(deep copy)。

淺複製和深複製有時也稱為淺拷貝和深拷貝。

+ (id) allocWithZone: (NSZone *) zone;

NSZone結構體是專門用於表達區域的數據結構。現代運行時中,因為參數zone會被忽略,所以一般設置為NULL。但方法的功能和alloc一樣。

實例的複製操作是由實例方法copyWithZone:完成的。

@protocol NSCopying

- (id)copyWithZone:(NSZone *)zone;

@end

- (id)copyWithZone:(NSZone *)zone {

ImageCell *tempcopy = [[[self class] allocWithZone:zone] init];

/*重要:使用 [self class] 而非類名*/

if (tempcopy) {

tempcopy->name = [name copyWithZone: zone];

tempcopy->image = [image retain];

tempcopy->position = positon;

}

return tmpcopy;

}

不僅限於copyWithZone:方法,只要用到繼承,我們都不建議寫方法內類自身固定的類名。

從常數對象生成可變對象可以使用MutableCopy方法。

- (id) mutableCopy

然而,該方法是使用NSObject簡單生成的,實際上真正生成實例的方法是mutableCopyWithZone:

- (id)mutableCopyWithZone:(NSZone *)zone

該方法在只包含它的協議NSMutableCopying中聲明,協議本身定義在頭文件Foundation/NSObject.h中。

有時候我們會有這樣的需求,即將程序中使用的多個對象及其屬性值,以及它們的相互關係保存到文件中,或者發送給其他的進程。為了實現該功能,Foundation框架中,可以把相互關聯的多個對象歸檔為二進位文件,而且還能將對象的關係從二進位文件中還原出來。像這樣,將對象打包成二進位文件就稱為歸檔(archive)。

將對象存儲轉換為二進位序列的過程成為歸檔,打包或編碼。逆變換則稱為解檔(unarchive),解碼或對象還原。

可以使用NSKeyedArchiver和NSKeyedUnarchiver完成對象的歸檔和解檔操作,而他們都是抽象類NSCoder的子類。

所有可以歸檔的對象都必須要適用於協議NSCoding。協議NSCoding在Foundation/NSObject.h中定義,NSObject自身並不採用該協議。NSString,NSDictionary等Foundation框架的主要類都適用協議NSCoding。

協議NSCoding按照如下方式聲明。

@protocol NSCoding

-(void)encodeWithCoder:(NSCoder *)aCoder;

-(id)initWithCoder:(NSCoder *)aDecoder;

@end

屬性表(property list)是Cocoa環境中用來表示或保存各種信息的標準數據形式。它可以將數組或詞典等對象的結構表現為字元串或二進位形式來保存在文件中,或者從中讀取出對象的信息。

塊對象

塊對象(block Oject)不是Ojbective-C而是C語言的功能實現。蘋果公司的文檔中將其稱為塊對象或Block(複數為Blocks),在其他編程語言中,它與閉包(closure)功能基本相同。為了和C語言的語法相區別,本書中稱為塊對象。

聲明:

typedef int (^myBlockType)(int);

使用該類型後,聲明函數func就可以使用下面的方式。

void func(myBlockType block)

此外,定義包含3個該類型元素的數組blocks時,可使用如下方式:

int (^blocks[3]) (int);或

myBlockType blocks[3];

我們把閉包引用,讀取外部變數稱為捕獲(capture)。

塊句法主題中,除塊句法內部的局部變數和形參外,還包括塊句法當前位置處可以訪問的變數。這些變數中有外部變數,還有包含塊句法的代碼塊內可以訪問的局部變數。

從塊對象內部可以直接訪問外部變數及靜態變數(static變數),也可以直接改變變數的值。

在包含塊句法的代碼塊內可訪問外部變數中,書寫塊句法時自動變數(棧內變數)的值會被保存起來,然後再被訪問。

a. 所以,即使變數最初的值發生了變化,塊對象在使用時也不會知道。

b.變數的值可以被讀取但是不能被改變。

c.自動變數為數組的時候,會發生編譯錯誤。

消息發送模式

應用從操作系統中接受不了滑鼠點擊等事件的消息,並將其轉到相應的例行程序來處理,如此反覆,這樣的過程被稱為運行迴路(run loop)或事件循環(event loop)。

當對象需要根據用途改變或增加新功能時,為了執行新添加的處理,就需要引用一個特殊的類似於「被諮詢者「的對象。這個對象就稱為委託。委託可在運行時動態分配。

並行編程

線程(thread)是進程(process)內假想的持有CPU使用權的執行單位。一般情況下,一個進程只有一個線程,但也可以穿件多個線程並在進程中並行執行。

應用在執行某一處理的同時,還可以接收GUI的輸入。

使用多線程的程序稱為多線程(multithread)運行。從程序開始執行時就運行的線程稱為主線程,

除此之外,之後生成的線程稱為次線程(secondary thread)或子線程(subthread)。

由於被創建的線程共享進程的地址空間,所以能夠自由訪問進程的空間變數。多線程訪問的變數稱為共享變數(shard variable)。

共享變數大多是全局變數或靜態變數,但因為地址空間是共享的,所以理論上所有內存區域都可以稱為共享變數。

如果多線程胡亂訪問共享變數,那麼就不能保證變數值的正確性。所以有時就需要按照一定的規則使多線程可以協調動作。

此時就必須執行線程間互斥(或者排他控制,mutual exclusion)

各個線程都分配有棧且獨立進行管理。基本上不能訪問其他線程的棧內的變數(自動變數)。

通過遵守這樣的編程方式,就可以自由訪問方法或函數的自動變數,而且不用擔心互斥。

多個線程同時操作某個實例時,如果沒有得到錯誤結果或實例沒有包含不正確的狀態,那麼該類就稱為線程安全(thread-safe)。

結果不能保證時,則稱為非線程安全或線程不安全(thread-unsafe)。

我們將同時只可以由一個線程佔有並執行的代碼部分稱為臨界區(critical section),或稱為危險區。互斥的目的就是限制可以在臨界區執行的線程。

為了使多個線程間可以相互排斥地使用全局變數等共享資源,可以使用NSLock類。

該類的實例也就是可以調整多線程行為的信號量(semaphore)或者互斥型信號量(mutual exclusion semaphore)。Cocoa環境中也稱為鎖(lock)。

NSLock *countLock = [[NSLock alloc] init];

- (void)lock

如果鎖正被使用,則線程進入休眠狀態。

如果鎖沒有被使用,則將鎖的狀態變為正被使用,線程繼續執行。

- (void)unlock

將鎖置為沒有在被使用,此時如果有等待該鎖資源的正在休眠的線程,則將其喚醒。

有趣的問題:

- (int)inc {

[countLock lock];

++theCount;

[countLock unlock];

}

乍一看好像沒問題,但從釋放鎖到返回值期間,其他線程可能會修改變數值。正確方法如下所示:

- (int)inc {

int tmp;

[countLock lock];

tep = ++theCount;

[countLock unlock];

return tmp;

}

個線程持有獨立的棧,自動變數tmp可以在整個線程中局部利用,而且無需擔心被其他線程訪問。

如果按照這樣的方式執行,就可以得到臨界區之後的正確性。

線程和鎖的關係必須在設計之初就經過仔細考慮。如果錯誤的使用鎖,不但不能按照預期執行互斥,還可能使多個線程陷入到不能執行的狀態,即死鎖(deadlock)狀態。

死鎖就是多線程(或進程)永遠在等待一個不可能實現的條件而無法繼續執行。

NSLock類不僅能獲得鎖和釋放鎖,還有檢查是否能獲得鎖的功能。利用這些功能,就可以在不能獲得鎖時進行其他處理。

- (BOOL) tryLock

用接收器嘗試獲得某個鎖,如果可以取得該鎖則返回YES。不能獲得時,與lock處理不同,線程沒有進入休眠狀態,而是直接返回NO並繼續執行。

類NSConditionLock稱為條件鎖(condition lock)。該鎖持有整數值,根據該值可以獲得鎖或者等待。

使用NSRecursiveLock類的鎖,擁有鎖的線程即使多次獲得同一個鎖也不會進入死鎖。但是,其他線程當然也不能獲得該鎖。獲得次數和釋放次數一致時,鎖就會被釋放。

程序內的塊可以指定為不被多線程同時使用。為此可以使用@synchronized編譯符,如下所示:

@synchronized(obj) {

//記訴想要排斥地執行的內容

}

通過使用該段代碼,運行時系統就可以創建排斥地執行該代碼塊的鎖(mutex)。參數obj通常指定為該互斥鎖要保護的對象。obj自己不需要是鎖對象。

@synchronized的參數對象決定對應的塊。所以,同一個對象參數的@synchronized塊如果有多個,則不可以同時執行。

@synchronized與上述NSRecursiveLock類的鎖一樣,可以遞歸調用。例如,下述這種簡單的例子就不會死鎖。

@synchronized(obj) {

@synchronized(obj) {

...

}

}

Mac OS X 10.6及iOS 4.0 後,導入了可以使系統全體線程更高效運行,並且使並行處理應用更易開發的架構,稱為大中央調度(GCD,Grand Central Dispatch)。

GCD的核心是用C語言寫的系統服務,應用將應該執行的各種任務封裝成塊對象寫入到隊列中。

通過對比各任務啟動線程的代價,該操作可以非常輕量地運行。

為了直接使用GCD的功能,必須要使用C語言函數,然而,在Objective-C中,通過用NSOperation類來表示任務,並將其追加到NSOperationQueue類隊列中,即可實現並行處理。

在下面的說明中,我們將需要執行的任務聚合在一起,稱為抽象任務(task)。

和將NSArray實例稱為數組對象一樣, 下面的NSOperation實例也可以稱為操作對象(operation object),或簡稱為操作。

操作不僅僅可以按照等待隊列中的順序被調用,也可以像「任務A完成後調用任務B」這樣指定調用的先後關係。此時,任務B依賴於任務A,這種關係稱為依賴關係(dependency)。

還可以設定操作的優先權(priority)。

為操作提供了等待隊列功能的是NSOperationQueue類。以下將該類的實例稱為操作隊列(operation queue),或簡稱為隊列。

等待隊列使用中先入先出的數據結構,也稱為FIFO(First In, First Out)。操作一旦被加入到隊列,至於如何執行,就要任憑系統來調度。

但是,加入到隊列的操作在執行之前也可以取消。

NSOperation子類必須將要執行的任務寫到下面的mian方法中。由於在NSOperation中定義的方法不發揮任何作用,所以並沒有必要調用super。

- (void) main

NSOperation子類至少需要包含main方法,此方法因按照如下方式定義。利用垃圾回收機制是不需要自動釋放池,只需要啟動垃圾回收器即可。

另外,@catch後的括弧並不表示省略的意思,而是就應該寫成「...」。

- (void)main {

@try {

@autoreleasepool {

//在這裡書寫任務中需要進行的處理

}

}

@catch(...) {

// 再次拋出異常,不能使用@throw等向外部傳播

}

}

NSOperationQueue的使用方法也很簡單。創建實例後,將操作對象加入到隊列中即可。

可以使用下面的方法將操作對象添加到隊列。

- (void) addOperation: (NSOperation *) operation

創建的隊列即使被多個線程同時操作也不會出任何問題,因此並不需要為互斥加鎖,或者用@synchronized塊來保護。

一個程序中可以創建多個隊列。但是,一個操作對象一次只能加入到一個隊列中。而且,已執行完成或正在執行的操作對象都不可以加入到隊列中。否則就會引發異常。

操作對象一旦加入到隊列,就不能從隊列中消除。如果不想執行該任務,則可以取消該處理。

雖然無法知道任務按照什麼樣的順序執行,但在隊列中添加的操作全部完成之前的這段時間,通過使用NSOperationQueue的下述方法,就可以一直鎖住當前線程(使之等待)。

這樣以來,在聚合任務終止後就可以簡單的進行下一個任務。

- (void) waitUntilAllOperationsAreFinished

到接收器隊列中添加的操作全部執行完為止,一直加鎖調用該函數的線程。隊列為空時,沒有要執行的任務,該方法立即返回。

- (void) addOperations: (NSArray *)ops waitUnitilFinished: (BOOL) wait

將數組ops中的操作對象添加到接收器隊列中。參數wait為YES時,到指定操作執行完為止,調用該方法的線程一直處於被鎖狀態。

使用引用計數管理方式時,添加的操作對象唄保存在隊列中,而數組ops則不被保存。

在Cocoa框架中,NSOperation的子類包括 NSInvocationOperation 和 NSBlockOperation。使用這些類時,即使不定義子類也能創建操作對象。

NSInvocationOperation使用如下初始化器,返迴向目標對象發送消息的任務操作對象。當任務內容已經被作為方法定義時,該方法有效。

- (id) initWithTarget: (id) target selector: (SEL) sel object: (id) arg

初始化實例,使參數選擇器sel向target發送消息。可以指定包含一個參數的選擇器,並指向參數arg。選擇器沒有包含參數時,向arg傳入nil。

使用引用計數管理方式時,target和arg會通過調用初始化器被保存,並在接收器的操作被釋放時release。而且,該操作會在創建自動釋放池後執行任務。

NSBlockOperation使用塊對象構造任務。使用下面的類方法可以創建臨時實例。

+ (id) blockOperationWithBlock: (void (^) (void)) block

返回以參數重指定的塊對象為任務的臨時操作對象。參數塊對象唄生成複製,並在操作對象內保存。

使用引用計數管理方式時,該操作會在創建自動釋放池後執行任務。

NSBlockOperation 中包含數組對象,裡面保存著多個塊對象,而且這些塊對象可以被當作任務執行。

當有多個塊對象時(如果可能的話),這些塊對象就會被並行執行。下面是與數組相關的方法。

- (void) addExecutionBlock: (void (^)(void)) block

將參數塊追加到接收器數組中。

- (NSArray *) executionBlocks

返回接收器包含的塊對象數組。

NSBlockOperation是NSOperation的子類,可以使用init初始化。初始化後,塊對象數組中沒有任何元素。

NSOperation中定義的設置依賴關係及解除關係的方法如下:

- (void) addDependency: (NSOperation *) operation

將接收器操作依賴的,也就是比接收器先執行的操作指定為參數。

- (void)removeDependency: (NSOperation *) operation

從接收器中刪除組冊的依賴操作。

- (NSArray *) dependencies

創建新數組,接收器保存依賴操作並返回。數組中也包括已經終止的操作。不包含依賴操作時,返回空數組。

在多個任務中,既有需要比其他任務優先執行的任務,也有非優先執行的任務,很多時候我們都會需要進行這樣的設定。此時可以使用 NSOperation提供的優先順序的設定方法。

但是並不能保證高優先順序的任務一定會先與低優先順序的任務執行。

- (void) setQueuePriority: (NSOperationQueuePriority) priority

為接收器的操作指定優先順序。

- (NSOperationQueuePriority) queuePriority

返回接收器的優先順序。

操作隊列可以同時啟動多個任務的情況下,可以設定最多可以並行執行的任務數。NSOperationQueue提供了下面的方法。

- (void) setMaxConcurrentOperationCount: (NSInteger) count

設定可以並行執行的最大操作數

- (NSInteger) maxConcurrentOperationCount

返回可以並行執行的最大操作數

NSOperation有如下方法取消隊列中的操作對象的任務。

- (void) cancel

使下面的方法 isCancelled返回YES。

- (BOOL) isCancelled

接收上面的cancel方法後返回YES。否則則返回NO。任務正在執行時或執行終止後也可以返回YES。

而且,NSOperationQueue類中也有如下方法。

- (void) cancelAllOperations

此時向隊列中的所有操作對象發送cancel消息。

操作對象即使接收cancel,也並不代表插入或異常這樣的特殊處理會被執行。

isCancelled在任務執行前接收cancel並返回YES時,操作對象將被解除執行。

在任務執行的過程中接收到cancel時,為了轉移到中斷處理,任務內容必須按如下方式編程實現。

具體來說,程序在main方法的開頭以在處理途中的各個重要地方檢查自己的isCancelled值,如果返回YES則進行中斷處理。

也可以臨時停止某個操作隊列的執行,來停止執行隊列中的操作。停止的隊列也可以再恢復。中斷和解除的方法在類NSOperationQueue中定義。

- (void) setSuspended: (BOOL) suspend

將接收器的隊列變為中斷狀態或者解除隊列的行為。參數指定為YES時時中斷狀態。進入中斷狀態後,停止從新的隊列中取出操作並執行。

即使為中斷狀態,也可以向隊列中添加操作。而且即使處於中斷狀態,也不會停止正在執行的任務。

- (BOOL) isSuspended

接收器隊列如果處於中斷狀態則返回YES,否則則返回NO。

在Mac OS X的Foundation框架中, 為了使不同的線程或進程可以雙工通信,提供了類 NSConnection。

NSConnection對象除了被作為線程間線程安全的通信線路使用外,也提供了創建應用間所使用的分散式對象(distributed object)的方法。

而iOS中則不提供該方法。

鍵值編碼

鍵值編碼(key-value coding)是指,將表示對象包含的信息的字元串作為鍵值使用,來間接訪問該信息的方式。

鍵值編碼提供了非常強大的功能,基本上,只要存在訪問器方法,聲明屬性或實例變數,就可以將其名字指定為字元串來訪問。

本章中,可以訪問,設定的對象狀態的值稱為屬性(property)。

之所以說鍵值編碼的訪問是間接的,是因為以下兩點:

  1. 也可以在運行中確定作為鍵的字元串
  2. 使用者無法知道實際訪問屬性的方法

鍵值編碼必須的方法在非正式協議NSKeyValueCoding中聲明。這些默認在NSObject中實現。

- (id) valueForKey: (NSString *) key

返回表示屬性的鍵字元串所對應的值。如果不能取得值,則將引起接收器調用方法valueForUndefinedKey:。

- (void) setValue: (id) value forKey: (NSString *)key

將鍵字元串key所對應的屬性的值設置為value。不能設定屬性值時,將引起接收器調用方法setValue:ForUndefinedKey:。

決定可否訪問實例變數的類方法以及取值失敗時調用的方法如下所示。

+ (BOOL) accessInstanceVariablesDirectly

通常定義為返回YES,可以在子類中改變。該類方法返回YES時,使用鍵值編碼可有訪問該類的實例變數。

返回NO時不可以訪問。只要該方法返回YES,實例變數的可視屬性即使有@private修飾,也可以訪問。

- (id) valueForUndefinedKey: (NSString *) key

不能取得鍵字元串對應的值時,從方法valueForKey:中調用該方法,默認情況下,該方法的執行會觸發異常 NSUndefinedKeyException。

不過,通過在子類中修改定義,就可以返回其他對象。

- (void) setValue: (id)value forUndefinedKey: (NSString *)key

不能設置鍵字元串key對應的屬性值時,從方法 setValue:forKey中調用該方法。默認情況下,該方法的執行會觸發異常NSUndefinedKeyException。

不過,通過在子類中修改定義,就可以返回其他對象。

在下面的討論中,我們將屬性中的單純的數值,也就是整數或實數,布爾值這樣的數據稱為標量(scalar)值。將標量值,結構體,字元串或NSNumber等常數對象稱為屬性(attribute)。

能夠使用鍵值編碼操作的屬性中,不僅有對象,也包括標量值,結構體等。上節中說明的方法valueForKey:在返回值為標量值或結構體時,會返回將其自動包裝的對象。

未來給setValue:forKey:傳入值,也需要使用適當的對象來包裝。

屬性值如果為對象,則可以將nil作為值傳遞。另一方面,使用setValue: forKey來設置標量型屬性為nil時, setNilValueForKey: 方法將被發送給接收器。

- (void) setNilValueForKey: (NSString *) key

在鍵字元串Key所對應的標量屬性中設定nil時, 會從方法setValue:forKey:中調用該方法。

默認情況下,執行該方法將產生 NSInvalidArgumentException異常,不過,通過在子類中修改定義,也可能產生其他行為。

字典類 NSDictionary 和 NSMutableDictionary 包含了協議 NSKeyValueCoding 的方法,使用它們可以進行鍵值編碼。

NSDictionary中定義了下面的方法。

- (id) valueForKey: (NSString *)key

鍵字元串開頭不是"@"時,將調用方法objectForKey:。開頭如果為"@",則將去除開頭字元後剩餘的字元串作為鍵,調用超類的方法valueForKey:。

- (void) setValue: (id) value forKey: (NSString *) key

一般會調用方法 setObject: forKey: ,參數value為nil時, 則調用方法 removeObjectForKey:刪除鍵對應的對象。

在鍵值編碼中,使用某個鍵訪問獲得某個屬性對象後,如果希望在用別的鍵來訪問該對象,可採用如下方法。

id name = [aGroup valueForKeyPath:@"leader.name"];

像這樣,用「.」號連接鍵表示的字元串稱為鍵路徑(key path)。只要能找到對象,點和鍵多長都沒有關係。

使用鍵路徑訪問屬性的方法如下所示:

- (id) valueForKeyPath: (NSString *)keyPath

以點切分鍵路徑,並使用第一個鍵向接收器發送valueForKey:方法。然後再使用鍵路徑的下一個鍵,

向得到的對象發送ValueForKey:方法,如此反覆操作,返回最後獲得的對象。

- (void) setValue: (id) value forKeyPath: (NSString *) keyPath

與valueForKeyPath:方法同樣取出對象,這裡只對路徑中的最後一個鍵調用setValue:forKey:方法,並設定屬性值為value。

使用鍵(鍵路徑)訪問時,我們將對象確定為一個的屬性稱為指定一對一關係(to-one relationship)的屬性,

將屬性值為數組或集合稱為指定一對多關係(to-many relationship)的屬性。

關於一對多關係屬性的訪問,更改,需要留意以下幾點。

使用集合元素對象持有的鍵訪問一對多關係屬性時,鍵對應的屬性被作為數組或集合返回。

使用集合元素對象持有的鍵設定一對多關係屬性時,各元素對象鍵對應的屬性全都被更改。

數組類NSArray和NSMutableArray以及集合類 NSSet 和 NSMutableSet都包含協議 NSKeyValueCoding的方法,也就是鍵值編碼。

- (id) valueForKey: (NSString *) key

以key為參數,對集合的各元素調用方法 valueForKey:後返回數組(NSSet時返回集合)。對各成員使用方法ValueForKey:,返回nil時,則包含NSNull實例。

- (void) setValue: (id) value forKey: (NSString *) key

對集合各元素調用方法setValue:forKey: 。需要注意的是,即使集合對象自身不可以改變,也能調用該方法。

如果可以使用鍵值編碼來訪問某個屬性,則稱該屬性是鍵值編碼的準則,或稱為KVC準則(compliane)。

反之,如果知道某屬性為KVC準則,那麼就可以編寫使用鍵值編碼的程序。

KVC準則和協議適用的概念不同,它不是以類為單位,而是討論以各個屬性為單位是不是準則的問題。

要使某屬性為KVC準則,就必須實現能使用valueForKey:方法的訪問器。當屬性可變時,還需要魚方法setValue:ForKey:相應的訪問器。

下面列舉一些具體的條件。

Property為屬性(標量值或單純型的對象)或一對一關係時,要想稱為KVC準則,就需要滿足如下條件。屬性名為「name」。

1.(a)實現了name或isName訪問器方法。或者

(b)包含name(或_name)實例變數。

2. 可變屬性時,還需要實現 setName:方法。需要執行鍵值驗證時,要實現驗證方法(validateName:error:)。但是,setName:方法中不能調用驗證方法。

屬性為一對多關係時,要想成為KVC準則,需滿足如下條件。屬性名為「names」。

1.(a)實現了返回數組的names方法。或者

(b)持有包含 names(或 names)數組對象的實例變數。或者

(c)實現了帶索引的訪問器模式的方法countOfNames以及objectInNamesAtIndex:。

2.當一對多關係的屬性可變時

(a)持有返回可變數組對象的names方法。或者

(b)實現了帶索引的訪問器模式的方法 insertObject: inNamesAtIndex: 以及 remoeveObjectFromNamesAtIndex:。

當然,在實現帶索引的訪問器模式的方法時,為改善執行效率,也可以添加其他方法來實現。

鍵值觀察(key-value observing),即某個對象的屬性改變時通知其他對象的機制。有時也記作 KVO。

NSOjbect中提供了鍵值觀察所必需的方法,頭文件 Foundation/NSKeyValueObserving.h中將其定義為了非正式協議的形式。

首先,需要使用下面的方法註冊鍵值觀察。

- (void) addObserver: (NSObject *) anObserver forKeyPath: (NSString *) keyPath options: (NSKeyValueObservingOptions) options context: (void *) context

從接收器的角度來看,監視鍵路徑keyPath中的某個屬性,要在接收器中註冊。觀察者為對象anObserver。屬性變化時發送的通知消息中,包含了現實變化內容的字典數據,

參數context中指定的任意指針(或對象)。options中指定字典數據中包含什麼樣的值。值可取下面的常數或他們的位或運算。

NSKyeValueObservingOptionNew---提供屬性改變後的值。

NSKyeValueObservingOptionOld---提供屬性改變前的值。

使用該方法後,當指定監視的屬性被改變時,下面的消息將被發送給觀察者。所有的觀察者都要實現如下方法。

- (void) observeValueForKeyPath: (NSSTring *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context

從參數object的角度來看,當鍵路徑keyPath的屬性發生變化時會發送通知。字典change中保存著改變的相關消息。

參數context中返回註冊觀察者時指定的值。

如果要停止已經註冊的監視,可使用下面的方法。

- (void) removeObserver: (NSObject *) anObserver forKeyPath: (NSString *) keyPath

觀察者為 anObserver,鍵路徑將刪除 keyPath內容註冊的監視這一消息傳達給接收器。

使用引用計數管理方式時需要注意一些事項。註冊屬性監視時,不需要持有觀察者及監視對象的屬性。

還有,如果在不刪除註冊信息的情況下將關聯對象釋放,那麼隨著屬性的變更,就可能會發生訪問已釋放了的對象的危險。

某屬性值隨著同一個對象的其他屬性的改變而改變時常有的事情。通過事先將這樣的依賴關係在類中註冊,

那麼即使屬性值間接地發送了改變,也會發送通知消息。為此需使用下面的類方法。

- (void) setKeys: (NSArray *) keys triggerChangeNotificationsForDependentKey: (NSString *) dependentKey

數組keys中可以保存多個鍵。註冊依賴關係,使當這些鍵中任意一個鍵對應的屬性發生改變時,都會自動引起與鍵dependentKey的屬性變化時一樣的行為(被監視時發送通知)。


推薦閱讀:

TAG:iOS開發 |