除了前面一章提到的消息行為( messaging behavior )外,對象還可以通過屬性封裝數(shù)據(jù)。
這一章節(jié)描述了 ObjC 中用于聲明對象屬性的語法,闡明了屬性如何通過默認(rèn)的訪問方法和實例變量的生成實現(xiàn)。如果實例變量支持該屬性,那么該變量必須在所有初始化方法中正確的設(shè)定。
如果一個對象需要通過屬性包含一個連向其他對象的鏈接,那么考慮這兩個對象之間的關(guān)系性質(zhì)將會變得很重要。盡管ObjC的內(nèi)存管理將會通過自動引用計數(shù)(?Automatic Reference Counting (ARC))為你處理大部分類似情況,但你仍需要了解這些,以避免類似強(qiáng)引用環(huán)( strong reference cycle )導(dǎo)致內(nèi)存溢出等問題的出現(xiàn)。這一章還介紹了對象的生命周期,以及如何從利用關(guān)系來管理對象圖的角度思考。
大部分對象都會通過跟蹤信息來執(zhí)行任務(wù)。一些對象被設(shè)計為一個以上具體值的模型,如Cocoa 中的用來保存數(shù)值的NSNumber類 或用來代表一個有姓、名的人的自定義類 XYZperson 。還有些對象作用域更為廣泛,甚至可能用來處理用戶界面和其顯示信息之間的交互,但即使這樣的對象,仍需要跟蹤用戶界面元素或相關(guān)模式對象的信息。
ObjC的屬性提供了一種定義信息的方式,而這些信息正是類中將要封裝的。正如你在?Properties Control Access to an Object’s Values一節(jié)中看到的,類的接口( interface )包含了屬性聲明,例如:
@interface XYZPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在這個例子中XYZPerson類聲明了string 屬性來保存一個人的名和姓。 這是面向?qū)ο缶幊讨幸粋€非?;镜脑瓌t——將對象的內(nèi)部運作隱藏在它的公共接口之后,所以我們總是使用一個對象的外部特性來訪問它的屬性,而不是直接嘗試去訪問它內(nèi)部的值。
你通過訪問器( accessor )方法來訪問或設(shè)定對象的屬性:
NSString *firstName = [somePerson firstName];
[somePerson setFirstName:@"Johnny"];
默認(rèn)地編譯器自動為你生成訪問器方法,除了在類接口中用 @propertys 聲明屬性外你不需要做任何事情。 生成中遵循的特定命名慣例:
對于上例中,名為 firstName 的屬性它的setter方法可以取名 setFirstName 。 如果你不想屬性通過setter方法改變值,可以在屬性聲明中增加一個特征( attribute ) readonly 表明其不能修改只能讀?。?/p>
@property (readonly) NSString *fullName;
除了告訴其他的對象他們應(yīng)該怎樣和一個屬性交互以外,特征還告訴了編譯器如何合成相應(yīng)的訪問方法。在這種情況下編譯器將會合成一個名為fullName getter方法,而不是一個setFullName方法。
注: 與 readonly 相對的是 readwrite ,因為 readwrite 特征是默認(rèn)設(shè)置的,所以沒有必要再去指明它。 如果你想給訪問器方法另外命名,通過給相應(yīng)屬性添加特征,來確定自定義名稱是可行的。對于布爾屬性(屬性值只有 YES 或 NO ),它的 getter 方法按照慣例應(yīng)該以“ is ”開頭,例如,對于一個名為 finished 屬性,它的getter方法,應(yīng)該命名為“ isFinished ”。 同樣的,給屬性添加一個特征是也是可行的:
@property (getter=isFinished) BOOL finished;
如果你需要指明多個特征,只需把他們排成一排并用逗號分隔開,像下面這樣:
@property (readonly, getter=isFinished) BOOL finished;
在這種情況下,編譯器僅會合成一個 isFinished 方法,而不是 setFinished 方法。
注:一般來說屬性訪問方法遵循鍵/值編碼,也就是說它的命名是遵循一定的命名慣例的。參看Key-Value Coding Programming Guide?獲得更多信息
除了明確的訪問器(accessor)方法調(diào)用,ObjC 還提供了另外一種選擇來訪問對象屬性——點語法 你可以這樣使用點語法訪問屬性:
NSString *firstName = somePerson.firstName;
somePerson.firstName = @"Johnny";
點語法僅是對訪問器方法做了一個單純的包裝,當(dāng)你使用它的時候,你仍是在使用前面提到的 getter 和 setter 方法,來對屬性進(jìn)行訪問或變更。
默認(rèn)情況下,一個可讀寫屬性會獲得實例變量的支持,這些會由編譯器自動生成。 實例變量是一種在對象的生命周期中一直存在并保存著值的變量。實例變量的存儲空間是在初次創(chuàng)建時被分配(通過 alloc ),在 dealloc 時被釋放。 你可以另外指定其他的名稱,或者實例變量將會生成與屬性一樣的名字,但實例變量的名字之前還會有一個下劃線前綴。例如,實例變量的一個可能的名字 _firstName 盡管通過訪問器方法或點語法來訪問對象自身的屬性是最好的實踐,但直接通過類實現(xiàn)中的任意實例方法來訪問實例變量也是可行的。下劃線前綴表明了你訪問的是一個實例變量而不是其他的,諸如局部變量之類的變量
- (void)someMethod {
NSString *myString = @"An interesting string";
_someString = myString;
}
在這個例子中很明顯 myString 是一個局部變量 _someString 是一個實例變量。一般來說,你會使用點語法或訪問方法進(jìn)行屬性訪問,即使是在對象自身的實現(xiàn)里進(jìn)行也是這樣,這時將會用到 self :
- (void)someMethod {
NSString *myString = @"An interesting string";
self.someString = myString;
// or
[self setSomeString:myString];
}
對于這條規(guī)則使用的特例是,初始化、釋放空間或自定義訪問器(accessor)方法,這些情況將會在后面的部分講到。
正如前面提到的,一個可寫屬性的默認(rèn)行為是使用一個名為 _propertyname 的實例變量。 如果你想為實例變量取另外的名字,你需要指導(dǎo)編譯器在你的實現(xiàn)中生成一個使用下列語法的變量:
@implementation YourClass
@synthesize propertyName = instanceVariableName;
...
@end
例如
@synthesize firstName = ivar_firstName;
在這種情況下,這個屬性仍然要被命名為 firstName ,并仍可以通過訪問器(accessor)方法—— firstName 和 setFirstName 或點語法訪問。但這種情況下,屬性是通過一個名為 ivar_firstName 的實例變量獲得支持的。 重要: 如果你使用 @synthesize 關(guān)鍵字時,不去指明實例變量的名字的話,像下面這樣:
@synthesize firstName;
那么實例變量將會被命成與屬性一樣的名字。 在這個例子中,實例變量將會被命名為 firstName ,并且不會加上下劃線前綴。
當(dāng)你需要跟蹤一個值或者對象時,最好的實現(xiàn)是通過使用另一個對象的屬性。 如果你確實需要定義一個你自己單獨的實例變量,即不通過聲明屬性獲得,你可以將他們聲明在,類接口或?qū)崿F(xiàn)的那個大括號的最開始,例如:
@interface SomeClass : NSObject {
NSString *_myNonPropertyInstanceVariable;
}
...
@end
@implementation SomeClass {
NSString *_anotherCustomInstanceVariable;
}
...
@end
注意: 你還需要將這樣的實例變量聲明在拓展類的最開始,參見 Class Extensions Extend the Internal Implementation 。
Setter 方法可能會產(chǎn)生額外的副作用,它可能會觸發(fā) KVC 通知,或在你編寫了自定義方法時執(zhí)行進(jìn)一步的任務(wù)。 你應(yīng)該總是從一個初始化方法中直接訪問實例變量,因為當(dāng)屬性被設(shè)置好時,一個對象的其余部分可能還未初始化完全。即使你不提供自定義訪問器( accesso )方法,或?qū)ψ远x類中可能產(chǎn)生的副作用有一定了解,之后產(chǎn)生的子類還是很可能會覆寫行為( behavior )。 一個典型的init方法會像下面這樣:
- (id)init {
self = [super init];
if (self) {
// initialize instance variables here
}
return self;
}
一個 init 方法應(yīng)該在開始它自己的初始化之前,首先用 self 響應(yīng)父類的初始化方法請求 。一個父類在初始化對象失敗之后會返回一個 nil ,所以應(yīng)當(dāng)總是在執(zhí)行你自己類的初始化之前,檢查 self 的返回值。
圖 3-1 初始化過程
正如你在前面的章節(jié)所看到的,對象的初始化有兩種方式,通過調(diào)用 init ,或調(diào)用為對象初始化具體值的方法。 在 XYNPerson例子中,提供一個可以設(shè)置初始名和姓的初始化方法是行的通的。
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;
你可以這樣實現(xiàn)這個方法:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
self = [super init];
if (self) {
_firstName = aFirstName;
_lastName = aLastName;
}
return self;
}
如果一個對象聲明了一個以上的初始化方法,你應(yīng)該確定一個方法作為指定初始器方法,它通常是為初始化提供最多選擇的方法(像是擁有最多參數(shù)的方法),并會被其他的便利方法調(diào)用。你通常還需要覆寫 init 方法來通過合適的默認(rèn)值調(diào)用指定初始器。
如果一個XYZPerson 類還包含一個用來表示生日的屬性,那么它的指定初始器方法應(yīng)該是這樣:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName
dateOfBirth:(NSDate *)aDOB;
這個方法還會設(shè)置相應(yīng)的實例變量值,像上面展示的一樣。如果仍希望提供一個只包含姓、名的指定初始器,你需要像下面這樣調(diào)用指定初始器,來實現(xiàn)這個方法。
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil];
}
你或許還需要實現(xiàn)一個標(biāo)準(zhǔn)的 init 方法來提供合適的默認(rèn)值,例如:
- (id)init {
return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil];
}
如果你需要在一個使用多個 init 方法的子類創(chuàng)建時,編寫初始化方法,那么,你可以覆寫父類的指定初始器來實現(xiàn)你自己的初始化,或者選擇再另外添加一個你自己的初始器。無論哪種方法,你都需要在開始你自己的初始化之前,調(diào)用父類的指定初始器(在此處 [super init] 調(diào)用)。
屬性也不總是被他們自己的實例變量支持的。舉一個例子,XYZPerson 類可以為一個人的全名定義一個只讀屬性:
@property (readonly) NSString *fullName;
比起每次姓或名一變更就不得不更新 fullName 屬性 ,只使用自定義訪問器(accessor)方法來建立一個要求的全名字符串就顯得容易許多。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
這個簡單的例子使用格式字符串和標(biāo)識符,建立了一個包含了以空格隔開的姓名的字符串。
注意: 盡管這是一個很實用的例子,但認(rèn)識到它的語境特殊性是很重要的,并且它只適用于那些將名放在姓之前的國家。 如果你為一個確實需要實例變量的屬性自定義了一個訪問器方法,你必須直接從這個方法的內(nèi)部來訪問這個屬性的實例變量。 例如,將屬性的初始化推延到它第一次被調(diào)用時是普遍的做法,這種做法叫做“ lazy訪問器”:
- (XYZObject *)someImportantObject {
if (!_someImportantObject) {
_someImportantObject = [[XYZObject alloc] init];
}
return _someImportantObject;
}
在返回值之前這個方法會首先檢查 _someImportantObject 實例變量的值是不是 nil ,如果是,它將會分配一個對象。
注意: 當(dāng)編譯器生成了至少一個訪問器(accessor)方法時,它都會再自動生成一個實例變量。但當(dāng)你為一個讀寫屬性實現(xiàn)了 getter , setter 方法,或為只讀屬性實現(xiàn)了 getter 方法,編譯器都會假設(shè)你想要控制屬性的實現(xiàn),并不再會自動生成一個實例變量。此時,如果你仍需要一個實例變量,你需要手動請求生成:
@synthesize property = _property;
屬性是 ObjC 中默認(rèn)的原子單元。
@interface XYZObject : NSObject
@property NSObject *implicitAtomicObject; // atomic by default
@property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic
@end
這意味著生成的訪問器( accessor )會確保值總是被 getter 方法取出,或被 setter 方法設(shè)置,即使它此時正在被不同的線程調(diào)用。由于原子訪問器的內(nèi)部實現(xiàn)和同步是私有的,所以將自己實現(xiàn)的訪問器方法,同編譯器生成的結(jié)合到一起是不太可能的。如果你這樣嘗試了,你將會得到編譯器警告,例如這種情況:為一個原子讀寫屬性提供一個自定義的 setter 方法,卻將 getter 方法留給編譯器生成。
你可以使用非原子( nonatomic )屬性特征來為生成的訪問器方法指明,它是要直接設(shè)置一個值還是返回一個值,但當(dāng)同樣的一個值被不同的線程訪問時,不保證會發(fā)生什么情況。正是由于這個原因,訪問一個非原子屬性比訪問原子屬性快,并且將生成的 setter 方法與其他方法結(jié)合是可行的,例如,與你自己實現(xiàn)的 getter 方法。
@interface XYZObject : NSObject
@property (nonatomic) NSObject *nonatomicObject;
@end
@implementation XYZObject
- (NSObject *)nonatomicObject {
return _nonatomicObject;
}
// setter will be synthesized automatically
@end
注意: 屬性原子性與對象線程安全性( thread safety )并不是同義詞。 考慮 XYZPerson 對象的例子,如果出現(xiàn)一個人的名和姓被一個線程的原子訪問器方法修改的情況。此時,另一個線程也在訪問這兩個字符串,原子的 getter 方法會返回完整的字符串,但并不保證這時的這兩個值彼此之間是正確對應(yīng)的。比如名是在另一個線程訪問前變更的,而姓則是在之后,那么你最終得到的將會是一對前后不一,錯誤匹配的姓和名。
這個例子很簡單,但如果考慮了關(guān)聯(lián)對象的關(guān)系網(wǎng)后,線程安全性問題將會變得更加復(fù)雜。更多細(xì)節(jié)參看Concurrency Programming Guide。
正如你所看到的, ObjC 為對象劃分的內(nèi)存是動態(tài)分配的(通過堆),這意味著你需要通過指針來跟蹤對象的地址。與標(biāo)量值不同,通過指針變量作用域來確定對象生命周期不一定可行。相反,一個對象只要是被其他對象調(diào)用了,就要始終在內(nèi)存中保持活躍。
相比起去關(guān)心如何手動管理每個對象的生命周期,你更應(yīng)該去認(rèn)真考慮對象之間的關(guān)系。
就拿 XYZPerson 的例子來說, firstName 和 lastName 這兩個字符串屬性可以說是有效的被 XYZPerson 實例所“擁有”,這意味著只要 XYZPerson 對象存在在內(nèi)存中, 這兩個屬性就同樣存在。
當(dāng)一個對象,通過有效地掌握其他對象的所有權(quán)的方式,依賴于其他對象時,這個對象就被稱為強(qiáng)引用( strong reference )其他對象。
在 ObjC 中,只要有至少一個對象強(qiáng)引用了另一個對象,那么后面這個對象都會一直保持活躍。 XYZPerson 實例與兩個 NSString 對象的關(guān)系圖見圖 3-2:
圖 3-2 強(qiáng)參考
當(dāng)一個 XYZPerson 對象從內(nèi)存中被釋放時,假設(shè)這時不再有其他任何對象強(qiáng)引用他們,那么這兩個字符串對象也會同樣被釋放。再為這個例子增加一點復(fù)雜性,考慮一下下面展示的這個應(yīng)用的對象圖:
圖 3-3 姓名標(biāo)志制作 應(yīng)用
當(dāng)用戶點擊了更新按鈕時,這個標(biāo)志的預(yù)覽圖就會根據(jù)相應(yīng)的姓名信息更新。當(dāng)一個 person 對象的信息第一次輸入并點擊更新按鈕時,簡化的的對象圖可能會是圖3-4中的樣子,
圖 3-4 初始 XYZPerson 創(chuàng)建時的關(guān)系圖
當(dāng)用戶調(diào)整了輸入的名時,關(guān)系圖會變成圖3-5 中的樣子
圖 3-5 當(dāng)名改變時的簡化關(guān)系圖
盡管此時 XYZPerson 對象已經(jīng)有了一個不同的 firstName ,標(biāo)志視圖仍然還包含著一個跟最開始的 @”John” 字符對象的強(qiáng)引用。這意味著 @”John” 對象仍在內(nèi)存中,并且還被標(biāo)志視圖用來輸出名字。
一旦用戶第二次點擊了更新按鈕,標(biāo)志視圖界面將會被告知更新它的內(nèi)部屬性以達(dá)到與 person 對象匹配。這時關(guān)系圖會是圖3-6中的樣子:
圖 3-6 更新了標(biāo)志視圖后的簡化對象圖
這時不再會有任何強(qiáng)引用與最開始的 @”John” 對象關(guān)聯(lián),它將會從內(nèi)存中移出。
盡管對于對象之間的單向關(guān)系,強(qiáng)引用表現(xiàn)的很好,但當(dāng)你在處理一組互相聯(lián)系的對象時你就需要小心了。當(dāng)一組對象是通過強(qiáng)引用環(huán)聯(lián)系時,那么即使外接已經(jīng)不存在任何跟他們的強(qiáng)引用,他們還是因為對彼此的強(qiáng)引用而保持活躍。 一個明顯的例子就是,視圖表對象與其授權(quán)對象( delegate )之間可能隱含的引用環(huán)( iOS 中的UITableView? 和 OS X 中的? NSTableView?)。為了使通用視圖表類在大多數(shù)情況下都可用,它會將一些決定授權(quán)給其他外部對象處理。這意味著視圖表對象依賴于其他對象來決定顯示什么樣的內(nèi)容,或者當(dāng)用戶與視圖表中的特定項發(fā)生交互時應(yīng)該做什么。 一個常見的情況是視圖表引用了他的授權(quán)對象,相應(yīng)的授權(quán)對象也會發(fā)出一個關(guān)聯(lián)視圖表的引用。
圖 3-7 視圖表與授權(quán)對象之間的強(qiáng)引用
但是當(dāng)其他對象撤銷了他們與視圖表與其授權(quán)對象之間的強(qiáng)引用時,問題就出現(xiàn)了
圖 3-8 一個強(qiáng)引用環(huán)
即使現(xiàn)在這兩個對象已經(jīng)沒有任何在內(nèi)存中存在的必要了——除了他倆之間還存在關(guān)聯(lián)之外,已經(jīng)沒有任何對象與他們有強(qiáng)引用了,但他們卻因為彼此之間存在的強(qiáng)引用而一直保持活躍。 當(dāng)視圖表將它與它授權(quán)對象之間的關(guān)聯(lián)修改為弱關(guān)聯(lián)( weak relationship )的時候(這也是 UITableView ?和 NSTableView? 如何解決這個問題的),最初的關(guān)系圖將會變成圖3-9。
圖 3-9 視圖表與其授權(quán)對象之間的正確關(guān)系
如果此時其他對象再撤銷他們與視圖表和其授權(quán)之間的強(qiáng)引用,視圖表就不會再與它的授權(quán)有強(qiáng)引用了。如圖 3-10
圖 3-10 規(guī)避了強(qiáng)引用的環(huán)
這意味著授權(quán)對象會被釋放,因此它對視圖表的強(qiáng)引用也會解除,如圖3-11
圖 3-11 釋放授權(quán)對象
一旦授權(quán)對象被釋放,也就不再有關(guān)聯(lián)于視圖表的強(qiáng)引用了,因此它也會被釋放。
對象屬性的默認(rèn)聲明一般是下面這樣:
@property id delegate;
為它生成的實例變量使用了強(qiáng)引用。你可以為屬性添加一個特征,來聲明弱關(guān)聯(lián),像這樣:
@property (weak) id delegate;
注: 與弱( weak )相反的是強(qiáng)( strong ),由于強(qiáng)是默認(rèn)的,所以不需要特別指明出強(qiáng)特征。 局部變量(還有非屬性實例變量)同樣也默認(rèn)包含與對象的強(qiáng)引用,這意味著下面這段代碼將會如你所期待的那樣準(zhǔn)確運行:
NSDate *originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];
NSLog(@"Last modification date changed from %@ to %@",
originalDate, self.lastModificationDate);
在這個例子中,局部變量 originalDate 包含了一個與初始對象 lastModificationDate 的強(qiáng)引用。當(dāng) lastModificationDate 屬性變更時,它不會再強(qiáng)引用原來的日期值,但這個日期仍然會被 originalDate 強(qiáng)變量保存著。
注: 只有當(dāng)變量在作用域內(nèi)、或還未分配給其他對象前、或值為 nil 的時候,它才會包含一個與對象的強(qiáng)引用。 如果你不想一個對象包含強(qiáng)引用,你可以把它聲明為 _weak ,像是這樣:
NSObject * __weak weakVariable;
因為弱關(guān)聯(lián)不會保證對象的活躍,所以有可能在關(guān)聯(lián)還在使用的時候關(guān)聯(lián)對象就被釋放了。為避免出現(xiàn)懸空指針的情況,即指針指向的內(nèi)存開始有對象后來卻被釋放了,當(dāng)對象被釋放時弱關(guān)聯(lián)會自動被設(shè)置為 nil。
這意味著你在前面的例子中使用一個弱變量時,
NSDate * __weak originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];
originalDate 變量存在被設(shè)置成 nil 的可能性。當(dāng) ?self.lastModificationDate 被重新分配時,屬性將不再會保有一個與原日期值相關(guān)的強(qiáng)引用。如果此時沒有其他變量強(qiáng)引用該日期值,那么原日期將被釋放, originalDate 也會被設(shè)置成 nil 。 弱變量可能會是混亂的來源,特別是在下面的編碼中:
NSObject * __weak someObject = [[NSObject alloc] init];
在上面的例子中,新分配的對象沒有任何與之相關(guān)的強(qiáng)引用,所以它會立即被釋放, someObject 也會被置為nil。
注意: 與 _weak 相對的是 _strong ,再次重申,你不需要特地指明 _strong ,因為它是默認(rèn)設(shè)置的。 同時去思考一個需要多次訪問弱屬性的方法含義也是很重要的,就像下面這樣:
- (void)someMethod {
[self.weakProperty doSomething];
...
[self.weakProperty doSomethingElse];
}
在這種情況下,你可能需要將弱屬性存放在一個強(qiáng)變量中,從而確保它在你需要使用的過程中一直保存在內(nèi)存中。
- (void)someMethod {
NSObject *cachedObject = self.weakProperty;
[cachedObject doSomething];
...
[cachedObject doSomethingElse];
}
在上面的例子中, cachedObject 包含了一個與初始弱屬性值關(guān)聯(lián)的強(qiáng)引用,所以只要 cachedObject 變量還在它的作用域中(同時沒有被重新賦予其他值),弱屬性就不會被釋放。 你需要特別記住的是,如果想確保弱屬性在使用前它的值不是 nil ,去測試它還遠(yuǎn)遠(yuǎn)不夠,像下面這樣:
if (self.someWeakProperty) {
[someObject doSomethingImportantWith:self.someWeakProperty];
}
因為在多線程應(yīng)用中,屬性可能會在測試與方法調(diào)用之間被釋放掉,這樣測試就無效了。因此你需要聲明一個強(qiáng)局部變量來保存值,像是這樣:
NSObject *cachedObject = self.someWeakProperty; // 1
if (cachedObject) { // 2
[someObject doSomethingImportantWith:cachedObject]; // 3
} // 4
cachedObject = nil; // 5
在這個例子中,強(qiáng)引用在第一行被創(chuàng)建,意味著將會在測試和方法調(diào)用的過程中,保證對象活躍。在第5行,cachedObject 被設(shè)置成 nil ,這樣強(qiáng)引用就被解除了,如果此時初始對象并沒有其他與之相關(guān)的強(qiáng)引用,它將會被釋放 ?someWeakProperty? 也會被設(shè)置成 nil 。
在Cocoa 和 Coacoa Touch 中還存在一些類,至今還不支持弱關(guān)聯(lián),這意味著你不能通過聲明弱屬性或弱變量來跟蹤他們。這些類包括? NSTextView ,? NSFont ?和? NSColorSpace ,想獲得完整的相關(guān)類列表,參看Transitioning to ARC Release Notes. 如果你想對這些類使用弱關(guān)聯(lián),你必須使用不安全引用。對于一個屬性,這意味著將要使用 unsafe_unretained 特征:
@property (unsafe_unretained) NSObject *unsafeProperty;
對于變量,你需要使用 __unsafe_unretained:
NSObject * __unsafe_unretained unsafeReference;
一個不安全引用與弱關(guān)聯(lián)之間的相同點在于,它們都不會保證相應(yīng)對象的活動。但當(dāng)目標(biāo)對象被釋放時,不安全引用不會被設(shè)置成 nil 。這意味著將會留下一個懸空指針,指向內(nèi)存中一塊開始存有對象之后被釋放的區(qū)域,這就是所謂的“不安全”。對一個懸空指針發(fā)送消息將會引起崩潰。
在一些情況下,一個對象可能會希望保存一份,為它的屬性設(shè)置的其他所有對象的備份。 舉個例子,早前在圖3-4中提到的 XYZBadgeView 類的接口可能會是這樣:
@interface XYZBadgeView : NSView
@property NSString *firstName;
@property NSString *lastName;
@end
上例聲明了兩個 ?NSString? 屬性,他們都包含了與自己對象的不明確強(qiáng)引用。 當(dāng)有另一個對象也創(chuàng)建了一個字符串來設(shè)置標(biāo)志視圖中的一個屬性,考慮會發(fā)生的情況,
NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;
這是完全有效的,因為? NSMutableString? 是 NSString 的一個子類。盡管標(biāo)志視圖認(rèn)為它正在處理的是 NSString 實例,但實際上它處理的是 NSMutableString 。 這意味著字符串可以通過這樣的方式變更:
[nameString appendString:@"ny"];
在這個例子中,盡管最開始時為標(biāo)志視圖 firstName? 屬性設(shè)置的名字值是“ John ”,但因為可變字符串( mutable string )值被改變了,所以它現(xiàn)在變成了“Johnny ”。 你可能會選擇為標(biāo)志視圖保存一份他自己的,包含所有為它的 firstName 屬性和 lastName 屬性設(shè)置的字符串值的備份,這樣就可以有效的捕捉屬性被設(shè)置時字符串的值。通過為這兩個屬性聲明一個 copy 特征可以達(dá)到這樣的目的:
@interface XYZBadgeView : NSView
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
@end
現(xiàn)在標(biāo)志視圖包含了自己關(guān)于這兩個字符串的備份了。即使可變字符串后來改變了,標(biāo)志視圖獲取的都還是這兩個字符串最初被設(shè)定的值。例如:
NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;
[nameString appendString:@"ny"];
這次,被標(biāo)志視圖保存的 firstName 值將會是來自于一份保有初始“ John ”字符串的抗影響備份。 Copy 特征表明,屬性因為需要與新創(chuàng)建的對象保持一致,而使用了強(qiáng)引用。
注: 任何你希望設(shè)置備份屬性的對象都需要支持 NSCopying ,這意味著它需要遵守?NSCopying協(xié)議。協(xié)議的描述在?Protocols Define Messaging Contracts?,獲取更多關(guān)于 NSCopying 的信息可以參看 NSCopying? 或者?Advanced Memory Management Programming Guide 如果你需要直接設(shè)置一個備份屬性的實例變量,例如在初始器方法中,不要忘記為原始的對象設(shè)置一個備份:
- (id)initWithSomeOriginalString:(NSString *)aString {
self = [super init];
if (self) {
_instanceVariableForCopyProperty = [aString copy];
}
return self;
}
1、為 XYZPerson 添加一個 sayHello 方法,使用人的名和姓來加載一句打招呼的話。 2、聲明并實現(xiàn)一個新的指定初始器( designated initializer ),用來創(chuàng)建一個XYZPerson類,該類使用指定姓、名、和生日日期, 同時還包含合適的類制造方法( class factory method ) 3、測試當(dāng)你設(shè)置可變字符串( mutable string )做為人名,然后在調(diào)用你添加的 sayHello 方法之前,改變?nèi)嗣址闹禃霈F(xiàn)的情況。為 NSString 屬性聲明添加備份特征,再測試一次。 4、嘗試使用main() 函數(shù)中各種強(qiáng)、弱變量來創(chuàng)建 XYZPerson 對象。驗證強(qiáng)變量會按照你期望的那樣,保持XYZPerson對象活動。 為了驗證一個 XYZPerson 對象是在何時被釋放的,你可能會想通過在? XYZPerson 實現(xiàn)中添加一個 dealloc 方法,來將它與對象的生命周期關(guān)聯(lián)起來。當(dāng)一個ObjC 對象從內(nèi)存中釋放時這個方法會被自動調(diào)用,同時該方法也可以用于釋放你手動分配的內(nèi)存,就像C中的 malloc() 函數(shù)一樣,參看Advanced Memory Management Programming Guide
為了這種目的的練習(xí),覆寫? XYZPerson 中的 dealloc 方法來加載消息,像這樣:
- (void)dealloc {
NSLog(@"XYZPerson is being deallocated");
}
嘗試通過設(shè)置 XYZPerson 中的每一個指針變量為 nil ,來驗證對象如你所期待的那樣被 釋放了。
注: 在 Xcode 工程中,為命令行提供的樣板,在 main() 函數(shù)內(nèi)使用 @autoreleasepool { } 塊,以達(dá)到使用編譯器的自動保留計數(shù)功能,來為你處理內(nèi)存管理。你在main() 函數(shù)中寫的任何代碼都會進(jìn)入自動釋放池( autoreleasepool ),這點是非常重要的。
自動釋放池在本文檔中沒有涉及,更多細(xì)節(jié)參看Advanced Memory Management Programming Guide
當(dāng)你正在編寫 Cocoa 或 Cocoa Touch 應(yīng)用而不是命令行時,你不需要過多的擔(dān)心關(guān)于創(chuàng)建你自己的自動釋放池的問題,因為那樣你就在嘗試進(jìn)入對象的構(gòu)架中,而這個構(gòu)架卻可以保證這個池的正確存在。 5、更改類的描述,使你可以跟蹤配偶(spouse)或伙伴(partner)。你需要考慮怎樣才能最好的模擬這種關(guān)系——你可以認(rèn)真思考一下對象圖管理。
更多建議: