數(shù)據(jù)封裝 - Encapsulating Data

2018-08-12 21:19 更新

數(shù)據(jù)封裝 - Encapsulating Data

除了前面一章提到的消息行為( messaging behavior )外,對(duì)象還可以通過屬性封裝數(shù)據(jù)。

這一章節(jié)描述了 ObjC 中用于聲明對(duì)象屬性的語法,闡明了屬性如何通過默認(rèn)的訪問方法和實(shí)例變量的生成實(shí)現(xiàn)。如果實(shí)例變量支持該屬性,那么該變量必須在所有初始化方法中正確的設(shè)定。

如果一個(gè)對(duì)象需要通過屬性包含一個(gè)連向其他對(duì)象的鏈接,那么考慮這兩個(gè)對(duì)象之間的關(guān)系性質(zhì)將會(huì)變得很重要。盡管ObjC的內(nèi)存管理將會(huì)通過自動(dòng)引用計(jì)數(shù)(?Automatic Reference Counting (ARC))為你處理大部分類似情況,但你仍需要了解這些,以避免類似強(qiáng)引用環(huán)( strong reference cycle )導(dǎo)致內(nèi)存溢出等問題的出現(xiàn)。這一章還介紹了對(duì)象的生命周期,以及如何從利用關(guān)系來管理對(duì)象圖的角度思考。

用屬性封裝對(duì)象的值

大部分對(duì)象都會(huì)通過跟蹤信息來執(zhí)行任務(wù)。一些對(duì)象被設(shè)計(jì)為一個(gè)以上具體值的模型,如Cocoa 中的用來保存數(shù)值的NSNumber類 或用來代表一個(gè)有姓、名的人的自定義類 XYZperson 。還有些對(duì)象作用域更為廣泛,甚至可能用來處理用戶界面和其顯示信息之間的交互,但即使這樣的對(duì)象,仍需要跟蹤用戶界面元素或相關(guān)模式對(duì)象的信息。

為外部數(shù)據(jù)聲明公共屬性

ObjC的屬性提供了一種定義信息的方式,而這些信息正是類中將要封裝的。正如你在?Properties Control Access to an Object’s Values一節(jié)中看到的,類的接口( interface )包含了屬性聲明,例如:

    @interface XYZPerson : NSObject
    @property NSString *firstName;
    @property NSString *lastName;
    @end

在這個(gè)例子中XYZPerson類聲明了string 屬性來保存一個(gè)人的名和姓。 這是面向?qū)ο缶幊讨幸粋€(gè)非?;镜脑瓌t——將對(duì)象的內(nèi)部運(yùn)作隱藏在它的公共接口之后,所以我們總是使用一個(gè)對(duì)象的外部特性來訪問它的屬性,而不是直接嘗試去訪問它內(nèi)部的值。

通過訪問器( accessor ) 方法來獲得或設(shè)置屬性的值

你通過訪問器( accessor )方法來訪問或設(shè)定對(duì)象的屬性:

    NSString *firstName = [somePerson firstName];
    [somePerson setFirstName:@"Johnny"];

默認(rèn)地編譯器自動(dòng)為你生成訪問器方法,除了在類接口中用 @propertys 聲明屬性外你不需要做任何事情。 生成中遵循的特定命名慣例:

  • 用于訪問值的方法( getter 方法)擁有與屬性一樣的名字 對(duì)于上例中,名為firstName的屬性它的 gette r方法名也為firstName 。
  • 用于設(shè)置值的方法( setter 方法)用“ set ”開頭的單詞命名,并且其中的屬性名首字母要大寫。

對(duì)于上例中,名為 firstName 的屬性它的setter方法可以取名 setFirstName 。 如果你不想屬性通過setter方法改變值,可以在屬性聲明中增加一個(gè)特征( attribute ) readonly 表明其不能修改只能讀?。?/p>

    @property (readonly) NSString *fullName;

除了告訴其他的對(duì)象他們應(yīng)該怎樣和一個(gè)屬性交互以外,特征還告訴了編譯器如何合成相應(yīng)的訪問方法。在這種情況下編譯器將會(huì)合成一個(gè)名為fullName getter方法,而不是一個(gè)setFullName方法。

注: 與 readonly 相對(duì)的是 readwrite ,因?yàn)?readwrite 特征是默認(rèn)設(shè)置的,所以沒有必要再去指明它。 如果你想給訪問器方法另外命名,通過給相應(yīng)屬性添加特征,來確定自定義名稱是可行的。對(duì)于布爾屬性(屬性值只有 YES 或 NO ),它的 getter 方法按照慣例應(yīng)該以“ is ”開頭,例如,對(duì)于一個(gè)名為 finished 屬性,它的getter方法,應(yīng)該命名為“ isFinished ”。 同樣的,給屬性添加一個(gè)特征是也是可行的:

    @property (getter=isFinished) BOOL finished;

如果你需要指明多個(gè)特征,只需把他們排成一排并用逗號(hào)分隔開,像下面這樣:

    @property (readonly, getter=isFinished) BOOL finished;

在這種情況下,編譯器僅會(huì)合成一個(gè) isFinished 方法,而不是 setFinished 方法。

注:一般來說屬性訪問方法遵循鍵/值編碼,也就是說它的命名是遵循一定的命名慣例的。參看Key-Value Coding Programming Guide?獲得更多信息

點(diǎn)語法對(duì)于訪問器方法調(diào)用是簡(jiǎn)潔的選擇

除了明確的訪問器(accessor)方法調(diào)用,ObjC 還提供了另外一種選擇來訪問對(duì)象屬性——點(diǎn)語法 你可以這樣使用點(diǎn)語法訪問屬性:

    NSString *firstName = somePerson.firstName;
    somePerson.firstName = @"Johnny";

點(diǎn)語法僅是對(duì)訪問器方法做了一個(gè)單純的包裝,當(dāng)你使用它的時(shí)候,你仍是在使用前面提到的 getter 和 setter 方法,來對(duì)屬性進(jìn)行訪問或變更。

  • 通過somePerson.firstName? 獲得一個(gè)值與使用 [somePerson firstName]是相同的
  • 通過 somePerson.firstName = @"Johnny" 賦值與使用 [somePerson setFirstName:@"Johnny"] 是相同的 這意味著通過點(diǎn)語法訪問屬性也是由屬性特征控制的。比如一個(gè)屬性標(biāo)記為readonly ,那么當(dāng)你試圖通過點(diǎn)語法對(duì)該屬性進(jìn)行設(shè)置時(shí),將會(huì)出現(xiàn)編譯錯(cuò)誤。

實(shí)例變量支持大多數(shù)的屬性

默認(rèn)情況下,一個(gè)可讀寫屬性會(huì)獲得實(shí)例變量的支持,這些會(huì)由編譯器自動(dòng)生成。 實(shí)例變量是一種在對(duì)象的生命周期中一直存在并保存著值的變量。實(shí)例變量的存儲(chǔ)空間是在初次創(chuàng)建時(shí)被分配(通過 alloc ),在 dealloc 時(shí)被釋放。 你可以另外指定其他的名稱,或者實(shí)例變量將會(huì)生成與屬性一樣的名字,但實(shí)例變量的名字之前還會(huì)有一個(gè)下劃線前綴。例如,實(shí)例變量的一個(gè)可能的名字 _firstName 盡管通過訪問器方法或點(diǎn)語法來訪問對(duì)象自身的屬性是最好的實(shí)踐,但直接通過類實(shí)現(xiàn)中的任意實(shí)例方法來訪問實(shí)例變量也是可行的。下劃線前綴表明了你訪問的是一個(gè)實(shí)例變量而不是其他的,諸如局部變量之類的變量

    - (void)someMethod {
        NSString *myString = @"An interesting string";

        _someString = myString;
    }

在這個(gè)例子中很明顯 myString 是一個(gè)局部變量 _someString 是一個(gè)實(shí)例變量。一般來說,你會(huì)使用點(diǎn)語法或訪問方法進(jìn)行屬性訪問,即使是在對(duì)象自身的實(shí)現(xiàn)里進(jìn)行也是這樣,這時(shí)將會(huì)用到 self :

    - (void)someMethod {
    NSString *myString = @"An interesting string";

    self.someString = myString;
    // or
    [self setSomeString:myString];
    }

對(duì)于這條規(guī)則使用的特例是,初始化、釋放空間或自定義訪問器(accessor)方法,這些情況將會(huì)在后面的部分講到。

你可以自定義生成的實(shí)例變量的名字

正如前面提到的,一個(gè)可寫屬性的默認(rèn)行為是使用一個(gè)名為 _propertyname 的實(shí)例變量。 如果你想為實(shí)例變量取另外的名字,你需要指導(dǎo)編譯器在你的實(shí)現(xiàn)中生成一個(gè)使用下列語法的變量:

    @implementation YourClass
    @synthesize propertyName = instanceVariableName;
    ...
    @end

例如

    @synthesize firstName = ivar_firstName;

在這種情況下,這個(gè)屬性仍然要被命名為 firstName ,并仍可以通過訪問器(accessor)方法—— firstName 和 setFirstName 或點(diǎn)語法訪問。但這種情況下,屬性是通過一個(gè)名為 ivar_firstName 的實(shí)例變量獲得支持的。 重要: 如果你使用 @synthesize 關(guān)鍵字時(shí),不去指明實(shí)例變量的名字的話,像下面這樣:

    @synthesize firstName;

那么實(shí)例變量將會(huì)被命成與屬性一樣的名字。 在這個(gè)例子中,實(shí)例變量將會(huì)被命名為 firstName ,并且不會(huì)加上下劃線前綴。

你可以不通過屬性來定義實(shí)例變量

當(dāng)你需要跟蹤一個(gè)值或者對(duì)象時(shí),最好的實(shí)現(xiàn)是通過使用另一個(gè)對(duì)象的屬性。 如果你確實(shí)需要定義一個(gè)你自己?jiǎn)为?dú)的實(shí)例變量,即不通過聲明屬性獲得,你可以將他們聲明在,類接口或?qū)崿F(xiàn)的那個(gè)大括號(hào)的最開始,例如:

    @interface SomeClass : NSObject {
    NSString *_myNonPropertyInstanceVariable;
    }
    ...
    @end

    @implementation SomeClass {
    NSString *_anotherCustomInstanceVariable;
    }
    ...
    @end

注意: 你還需要將這樣的實(shí)例變量聲明在拓展類的最開始,參見 Class Extensions Extend the Internal Implementation 。

可以直接通過初始化方法來訪問實(shí)例變量

Setter 方法可能會(huì)產(chǎn)生額外的副作用,它可能會(huì)觸發(fā) KVC 通知,或在你編寫了自定義方法時(shí)執(zhí)行進(jìn)一步的任務(wù)。 你應(yīng)該總是從一個(gè)初始化方法中直接訪問實(shí)例變量,因?yàn)楫?dāng)屬性被設(shè)置好時(shí),一個(gè)對(duì)象的其余部分可能還未初始化完全。即使你不提供自定義訪問器( accesso )方法,或?qū)ψ远x類中可能產(chǎn)生的副作用有一定了解,之后產(chǎn)生的子類還是很可能會(huì)覆寫行為( behavior )。 一個(gè)典型的init方法會(huì)像下面這樣:

    - (id)init {
    self = [super init];

    if (self) {
        // initialize instance variables here
    }

    return self;
    }

一個(gè) init 方法應(yīng)該在開始它自己的初始化之前,首先用 self 響應(yīng)父類的初始化方法請(qǐng)求 。一個(gè)父類在初始化對(duì)象失敗之后會(huì)返回一個(gè) nil ,所以應(yīng)當(dāng)總是在執(zhí)行你自己類的初始化之前,檢查 self 的返回值。

圖 3-1 初始化過程

初始化過程

正如你在前面的章節(jié)所看到的,對(duì)象的初始化有兩種方式,通過調(diào)用 init ,或調(diào)用為對(duì)象初始化具體值的方法。 在 XYNPerson例子中,提供一個(gè)可以設(shè)置初始名和姓的初始化方法是行的通的。

    - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;

你可以這樣實(shí)現(xiàn)這個(gè)方法:

    - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
    self = [super init];

    if (self) {
        _firstName = aFirstName;
        _lastName = aLastName;
    }

    return self;
    }

指定初始器是最主要的初始化方法

如果一個(gè)對(duì)象聲明了一個(gè)以上的初始化方法,你應(yīng)該確定一個(gè)方法作為指定初始器方法,它通常是為初始化提供最多選擇的方法(像是擁有最多參數(shù)的方法),并會(huì)被其他的便利方法調(diào)用。你通常還需要覆寫 init 方法來通過合適的默認(rèn)值調(diào)用指定初始器。

如果一個(gè)XYZPerson 類還包含一個(gè)用來表示生日的屬性,那么它的指定初始器方法應(yīng)該是這樣:

    - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName
                                            dateOfBirth:(NSDate *)aDOB;

這個(gè)方法還會(huì)設(shè)置相應(yīng)的實(shí)例變量值,像上面展示的一樣。如果仍希望提供一個(gè)只包含姓、名的指定初始器,你需要像下面這樣調(diào)用指定初始器,來實(shí)現(xiàn)這個(gè)方法。

    - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
    return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil];
    }

你或許還需要實(shí)現(xiàn)一個(gè)標(biāo)準(zhǔn)的 init 方法來提供合適的默認(rèn)值,例如:

    - (id)init {
    return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil];
    }

如果你需要在一個(gè)使用多個(gè) init 方法的子類創(chuàng)建時(shí),編寫初始化方法,那么,你可以覆寫父類的指定初始器來實(shí)現(xiàn)你自己的初始化,或者選擇再另外添加一個(gè)你自己的初始器。無論哪種方法,你都需要在開始你自己的初始化之前,調(diào)用父類的指定初始器(在此處 [super init] 調(diào)用)。

你可以實(shí)現(xiàn)你自定義的訪問器方法

屬性也不總是被他們自己的實(shí)例變量支持的。舉一個(gè)例子,XYZPerson 類可以為一個(gè)人的全名定義一個(gè)只讀屬性:

    @property (readonly) NSString *fullName;

比起每次姓或名一變更就不得不更新 fullName 屬性 ,只使用自定義訪問器(accessor)方法來建立一個(gè)要求的全名字符串就顯得容易許多。

    - (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
    }

這個(gè)簡(jiǎn)單的例子使用格式字符串和標(biāo)識(shí)符,建立了一個(gè)包含了以空格隔開的姓名的字符串。

注意: 盡管這是一個(gè)很實(shí)用的例子,但認(rèn)識(shí)到它的語境特殊性是很重要的,并且它只適用于那些將名放在姓之前的國(guó)家。 如果你為一個(gè)確實(shí)需要實(shí)例變量的屬性自定義了一個(gè)訪問器方法,你必須直接從這個(gè)方法的內(nèi)部來訪問這個(gè)屬性的實(shí)例變量。 例如,將屬性的初始化推延到它第一次被調(diào)用時(shí)是普遍的做法,這種做法叫做“ lazy訪問器”:

    - (XYZObject *)someImportantObject {
    if (!_someImportantObject) {
        _someImportantObject = [[XYZObject alloc] init];
    }

    return _someImportantObject;
    }

在返回值之前這個(gè)方法會(huì)首先檢查 _someImportantObject 實(shí)例變量的值是不是 nil ,如果是,它將會(huì)分配一個(gè)對(duì)象。

注意: 當(dāng)編譯器生成了至少一個(gè)訪問器(accessor)方法時(shí),它都會(huì)再自動(dòng)生成一個(gè)實(shí)例變量。但當(dāng)你為一個(gè)讀寫屬性實(shí)現(xiàn)了 getter , setter 方法,或?yàn)橹蛔x屬性實(shí)現(xiàn)了 getter 方法,編譯器都會(huì)假設(shè)你想要控制屬性的實(shí)現(xiàn),并不再會(huì)自動(dòng)生成一個(gè)實(shí)例變量。此時(shí),如果你仍需要一個(gè)實(shí)例變量,你需要手動(dòng)請(qǐng)求生成:

    @synthesize property = _property;

屬性是默認(rèn)的原子單元

屬性是 ObjC 中默認(rèn)的原子單元。

    @interface XYZObject : NSObject
    @property NSObject *implicitAtomicObject;          // atomic by default
    @property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic
    @end

這意味著生成的訪問器( accessor )會(huì)確保值總是被 getter 方法取出,或被 setter 方法設(shè)置,即使它此時(shí)正在被不同的線程調(diào)用。由于原子訪問器的內(nèi)部實(shí)現(xiàn)和同步是私有的,所以將自己實(shí)現(xiàn)的訪問器方法,同編譯器生成的結(jié)合到一起是不太可能的。如果你這樣嘗試了,你將會(huì)得到編譯器警告,例如這種情況:為一個(gè)原子讀寫屬性提供一個(gè)自定義的 setter 方法,卻將 getter 方法留給編譯器生成。

你可以使用非原子( nonatomic )屬性特征來為生成的訪問器方法指明,它是要直接設(shè)置一個(gè)值還是返回一個(gè)值,但當(dāng)同樣的一個(gè)值被不同的線程訪問時(shí),不保證會(huì)發(fā)生什么情況。正是由于這個(gè)原因,訪問一個(gè)非原子屬性比訪問原子屬性快,并且將生成的 setter 方法與其他方法結(jié)合是可行的,例如,與你自己實(shí)現(xiàn)的 getter 方法。

    @interface XYZObject : NSObject
    @property (nonatomic) NSObject *nonatomicObject;
    @end

    @implementation XYZObject
    - (NSObject *)nonatomicObject {
    return _nonatomicObject;
    }
    // setter will be synthesized automatically
    @end

注意: 屬性原子性與對(duì)象線程安全性( thread safety )并不是同義詞。 考慮 XYZPerson 對(duì)象的例子,如果出現(xiàn)一個(gè)人的名和姓被一個(gè)線程的原子訪問器方法修改的情況。此時(shí),另一個(gè)線程也在訪問這兩個(gè)字符串,原子的 getter 方法會(huì)返回完整的字符串,但并不保證這時(shí)的這兩個(gè)值彼此之間是正確對(duì)應(yīng)的。比如名是在另一個(gè)線程訪問前變更的,而姓則是在之后,那么你最終得到的將會(huì)是一對(duì)前后不一,錯(cuò)誤匹配的姓和名。

這個(gè)例子很簡(jiǎn)單,但如果考慮了關(guān)聯(lián)對(duì)象的關(guān)系網(wǎng)后,線程安全性問題將會(huì)變得更加復(fù)雜。更多細(xì)節(jié)參看Concurrency Programming Guide。

通過所有權(quán)和責(zé)任來管理對(duì)象圖

正如你所看到的, ObjC 為對(duì)象劃分的內(nèi)存是動(dòng)態(tài)分配的(通過堆),這意味著你需要通過指針來跟蹤對(duì)象的地址。與標(biāo)量值不同,通過指針變量作用域來確定對(duì)象生命周期不一定可行。相反,一個(gè)對(duì)象只要是被其他對(duì)象調(diào)用了,就要始終在內(nèi)存中保持活躍。

相比起去關(guān)心如何手動(dòng)管理每個(gè)對(duì)象的生命周期,你更應(yīng)該去認(rèn)真考慮對(duì)象之間的關(guān)系。

就拿 XYZPerson 的例子來說, firstName 和 lastName 這兩個(gè)字符串屬性可以說是有效的被 XYZPerson 實(shí)例所“擁有”,這意味著只要 XYZPerson 對(duì)象存在在內(nèi)存中, 這兩個(gè)屬性就同樣存在。

當(dāng)一個(gè)對(duì)象,通過有效地掌握其他對(duì)象的所有權(quán)的方式,依賴于其他對(duì)象時(shí),這個(gè)對(duì)象就被稱為強(qiáng)引用( strong reference )其他對(duì)象。

在 ObjC 中,只要有至少一個(gè)對(duì)象強(qiáng)引用了另一個(gè)對(duì)象,那么后面這個(gè)對(duì)象都會(huì)一直保持活躍。 XYZPerson 實(shí)例與兩個(gè) NSString 對(duì)象的關(guān)系圖見圖 3-2:

圖 3-2 強(qiáng)參考

strongpersonproperties

當(dāng)一個(gè) XYZPerson 對(duì)象從內(nèi)存中被釋放時(shí),假設(shè)這時(shí)不再有其他任何對(duì)象強(qiáng)引用他們,那么這兩個(gè)字符串對(duì)象也會(huì)同樣被釋放。再為這個(gè)例子增加一點(diǎn)復(fù)雜性,考慮一下下面展示的這個(gè)應(yīng)用的對(duì)象圖:

圖 3-3 姓名標(biāo)志制作 應(yīng)用

namebadgemaker

當(dāng)用戶點(diǎn)擊了更新按鈕時(shí),這個(gè)標(biāo)志的預(yù)覽圖就會(huì)根據(jù)相應(yīng)的姓名信息更新。當(dāng)一個(gè) person 對(duì)象的信息第一次輸入并點(diǎn)擊更新按鈕時(shí),簡(jiǎn)化的的對(duì)象圖可能會(huì)是圖3-4中的樣子,

圖 3-4 初始 XYZPerson 創(chuàng)建時(shí)的關(guān)系圖

simplifiedobjectgraph1

當(dāng)用戶調(diào)整了輸入的名時(shí),關(guān)系圖會(huì)變成圖3-5 中的樣子

圖 3-5 當(dāng)名改變時(shí)的簡(jiǎn)化關(guān)系圖

simplifiedobjectgraph2

盡管此時(shí) XYZPerson 對(duì)象已經(jīng)有了一個(gè)不同的 firstName ,標(biāo)志視圖仍然還包含著一個(gè)跟最開始的 @”John” 字符對(duì)象的強(qiáng)引用。這意味著 @”John” 對(duì)象仍在內(nèi)存中,并且還被標(biāo)志視圖用來輸出名字。

一旦用戶第二次點(diǎn)擊了更新按鈕,標(biāo)志視圖界面將會(huì)被告知更新它的內(nèi)部屬性以達(dá)到與 person 對(duì)象匹配。這時(shí)關(guān)系圖會(huì)是圖3-6中的樣子:

圖 3-6 更新了標(biāo)志視圖后的簡(jiǎn)化對(duì)象圖

simplifiedobjectgraph3

這時(shí)不再會(huì)有任何強(qiáng)引用與最開始的 @”John” 對(duì)象關(guān)聯(lián),它將會(huì)從內(nèi)存中移出。

避免強(qiáng)引用環(huán)(strong reference cycles)

盡管對(duì)于對(duì)象之間的單向關(guān)系,強(qiáng)引用表現(xiàn)的很好,但當(dāng)你在處理一組互相聯(lián)系的對(duì)象時(shí)你就需要小心了。當(dāng)一組對(duì)象是通過強(qiáng)引用環(huán)聯(lián)系時(shí),那么即使外接已經(jīng)不存在任何跟他們的強(qiáng)引用,他們還是因?yàn)閷?duì)彼此的強(qiáng)引用而保持活躍。 一個(gè)明顯的例子就是,視圖表對(duì)象與其授權(quán)對(duì)象( delegate )之間可能隱含的引用環(huán)( iOS 中的UITableView? 和 OS X 中的? NSTableView?)。為了使通用視圖表類在大多數(shù)情況下都可用,它會(huì)將一些決定授權(quán)給其他外部對(duì)象處理。這意味著視圖表對(duì)象依賴于其他對(duì)象來決定顯示什么樣的內(nèi)容,或者當(dāng)用戶與視圖表中的特定項(xiàng)發(fā)生交互時(shí)應(yīng)該做什么。 一個(gè)常見的情況是視圖表引用了他的授權(quán)對(duì)象,相應(yīng)的授權(quán)對(duì)象也會(huì)發(fā)出一個(gè)關(guān)聯(lián)視圖表的引用。

圖 3-7 視圖表與授權(quán)對(duì)象之間的強(qiáng)引用

strongreferencecycle1

但是當(dāng)其他對(duì)象撤銷了他們與視圖表與其授權(quán)對(duì)象之間的強(qiáng)引用時(shí),問題就出現(xiàn)了

圖 3-8 一個(gè)強(qiáng)引用環(huán)

strongreferencecycle2

即使現(xiàn)在這兩個(gè)對(duì)象已經(jīng)沒有任何在內(nèi)存中存在的必要了——除了他倆之間還存在關(guān)聯(lián)之外,已經(jīng)沒有任何對(duì)象與他們有強(qiáng)引用了,但他們卻因?yàn)楸舜酥g存在的強(qiáng)引用而一直保持活躍。 當(dāng)視圖表將它與它授權(quán)對(duì)象之間的關(guān)聯(lián)修改為弱關(guān)聯(lián)( weak relationship )的時(shí)候(這也是 UITableView ?和 NSTableView? 如何解決這個(gè)問題的),最初的關(guān)系圖將會(huì)變成圖3-9。

圖 3-9 視圖表與其授權(quán)對(duì)象之間的正確關(guān)系

strongreferencecycle3

如果此時(shí)其他對(duì)象再撤銷他們與視圖表和其授權(quán)之間的強(qiáng)引用,視圖表就不會(huì)再與它的授權(quán)有強(qiáng)引用了。如圖 3-10

圖 3-10 規(guī)避了強(qiáng)引用的環(huán)

strongreferencecycle4

這意味著授權(quán)對(duì)象會(huì)被釋放,因此它對(duì)視圖表的強(qiáng)引用也會(huì)解除,如圖3-11

圖 3-11 釋放授權(quán)對(duì)象

strongreferencecycle5

一旦授權(quán)對(duì)象被釋放,也就不再有關(guān)聯(lián)于視圖表的強(qiáng)引用了,因此它也會(huì)被釋放。

通過強(qiáng)、弱聲明(Strong and Weak Declaration)來管理所有權(quán)

對(duì)象屬性的默認(rèn)聲明一般是下面這樣:

    @property id delegate;

為它生成的實(shí)例變量使用了強(qiáng)引用。你可以為屬性添加一個(gè)特征,來聲明弱關(guān)聯(lián),像這樣:

    @property (weak) id delegate;

注: 與弱( weak )相反的是強(qiáng)( strong ),由于強(qiáng)是默認(rèn)的,所以不需要特別指明出強(qiáng)特征。 局部變量(還有非屬性實(shí)例變量)同樣也默認(rèn)包含與對(duì)象的強(qiáng)引用,這意味著下面這段代碼將會(huì)如你所期待的那樣準(zhǔn)確運(yùn)行:

    NSDate *originalDate = self.lastModificationDate;
    self.lastModificationDate = [NSDate date];
    NSLog(@"Last modification date changed from %@ to %@",
                        originalDate, self.lastModificationDate);

在這個(gè)例子中,局部變量 originalDate 包含了一個(gè)與初始對(duì)象 lastModificationDate 的強(qiáng)引用。當(dāng) lastModificationDate 屬性變更時(shí),它不會(huì)再?gòu)?qiáng)引用原來的日期值,但這個(gè)日期仍然會(huì)被 originalDate 強(qiáng)變量保存著。

注: 只有當(dāng)變量在作用域內(nèi)、或還未分配給其他對(duì)象前、或值為 nil 的時(shí)候,它才會(huì)包含一個(gè)與對(duì)象的強(qiáng)引用。 如果你不想一個(gè)對(duì)象包含強(qiáng)引用,你可以把它聲明為 _weak ,像是這樣:

    NSObject * __weak weakVariable;

因?yàn)槿蹶P(guān)聯(lián)不會(huì)保證對(duì)象的活躍,所以有可能在關(guān)聯(lián)還在使用的時(shí)候關(guān)聯(lián)對(duì)象就被釋放了。為避免出現(xiàn)懸空指針的情況,即指針指向的內(nèi)存開始有對(duì)象后來卻被釋放了,當(dāng)對(duì)象被釋放時(shí)弱關(guān)聯(lián)會(huì)自動(dòng)被設(shè)置為 nil。

這意味著你在前面的例子中使用一個(gè)弱變量時(shí),

    NSDate * __weak originalDate = self.lastModificationDate;
    self.lastModificationDate = [NSDate date];

originalDate 變量存在被設(shè)置成 nil 的可能性。當(dāng) ?self.lastModificationDate 被重新分配時(shí),屬性將不再會(huì)保有一個(gè)與原日期值相關(guān)的強(qiáng)引用。如果此時(shí)沒有其他變量強(qiáng)引用該日期值,那么原日期將被釋放, originalDate 也會(huì)被設(shè)置成 nil 。 弱變量可能會(huì)是混亂的來源,特別是在下面的編碼中:

    NSObject * __weak someObject = [[NSObject alloc] init];

在上面的例子中,新分配的對(duì)象沒有任何與之相關(guān)的強(qiáng)引用,所以它會(huì)立即被釋放, someObject 也會(huì)被置為nil。

注意: 與 _weak 相對(duì)的是 _strong ,再次重申,你不需要特地指明 _strong ,因?yàn)樗悄J(rèn)設(shè)置的。 同時(shí)去思考一個(gè)需要多次訪問弱屬性的方法含義也是很重要的,就像下面這樣:

    - (void)someMethod {
    [self.weakProperty doSomething];
    ...
    [self.weakProperty doSomethingElse];
    }

在這種情況下,你可能需要將弱屬性存放在一個(gè)強(qiáng)變量中,從而確保它在你需要使用的過程中一直保存在內(nèi)存中。

    - (void)someMethod {
    NSObject *cachedObject = self.weakProperty;
    [cachedObject doSomething];
    ...
    [cachedObject doSomethingElse];
    }

在上面的例子中, cachedObject 包含了一個(gè)與初始弱屬性值關(guān)聯(lián)的強(qiáng)引用,所以只要 cachedObject 變量還在它的作用域中(同時(shí)沒有被重新賦予其他值),弱屬性就不會(huì)被釋放。 你需要特別記住的是,如果想確保弱屬性在使用前它的值不是 nil ,去測(cè)試它還遠(yuǎn)遠(yuǎn)不夠,像下面這樣:

    if (self.someWeakProperty) {
        [someObject doSomethingImportantWith:self.someWeakProperty];
    }

因?yàn)樵诙嗑€程應(yīng)用中,屬性可能會(huì)在測(cè)試與方法調(diào)用之間被釋放掉,這樣測(cè)試就無效了。因此你需要聲明一個(gè)強(qiáng)局部變量來保存值,像是這樣:

    NSObject *cachedObject = self.someWeakProperty;           // 1
    if (cachedObject) {                                       // 2
        [someObject doSomethingImportantWith:cachedObject];   // 3
    }                                                         // 4
    cachedObject = nil;                                       // 5

在這個(gè)例子中,強(qiáng)引用在第一行被創(chuàng)建,意味著將會(huì)在測(cè)試和方法調(diào)用的過程中,保證對(duì)象活躍。在第5行,cachedObject 被設(shè)置成 nil ,這樣強(qiáng)引用就被解除了,如果此時(shí)初始對(duì)象并沒有其他與之相關(guān)的強(qiáng)引用,它將會(huì)被釋放 ?someWeakProperty? 也會(huì)被設(shè)置成 nil 。

對(duì)一些類使用不安全、無保留的引用

在Cocoa 和 Coacoa Touch 中還存在一些類,至今還不支持弱關(guān)聯(lián),這意味著你不能通過聲明弱屬性或弱變量來跟蹤他們。這些類包括? NSTextView ,? NSFont ?和? NSColorSpace ,想獲得完整的相關(guān)類列表,參看Transitioning to ARC Release Notes. 如果你想對(duì)這些類使用弱關(guān)聯(lián),你必須使用不安全引用。對(duì)于一個(gè)屬性,這意味著將要使用 unsafe_unretained 特征:

    @property (unsafe_unretained) NSObject *unsafeProperty;

對(duì)于變量,你需要使用 __unsafe_unretained:

    NSObject * __unsafe_unretained unsafeReference;

一個(gè)不安全引用與弱關(guān)聯(lián)之間的相同點(diǎn)在于,它們都不會(huì)保證相應(yīng)對(duì)象的活動(dòng)。但當(dāng)目標(biāo)對(duì)象被釋放時(shí),不安全引用不會(huì)被設(shè)置成 nil 。這意味著將會(huì)留下一個(gè)懸空指針,指向內(nèi)存中一塊開始存有對(duì)象之后被釋放的區(qū)域,這就是所謂的“不安全”。對(duì)一個(gè)懸空指針發(fā)送消息將會(huì)引起崩潰。

備份屬性(Copy Properties?)保有他們自己的備份

在一些情況下,一個(gè)對(duì)象可能會(huì)希望保存一份,為它的屬性設(shè)置的其他所有對(duì)象的備份。 舉個(gè)例子,早前在圖3-4中提到的 XYZBadgeView 類的接口可能會(huì)是這樣:

    @interface XYZBadgeView : NSView
    @property NSString *firstName;
    @property NSString *lastName;
    @end

上例聲明了兩個(gè) ?NSString? 屬性,他們都包含了與自己對(duì)象的不明確強(qiáng)引用。 當(dāng)有另一個(gè)對(duì)象也創(chuàng)建了一個(gè)字符串來設(shè)置標(biāo)志視圖中的一個(gè)屬性,考慮會(huì)發(fā)生的情況,

    NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
    self.badgeView.firstName = nameString;

這是完全有效的,因?yàn)? NSMutableString? 是 NSString 的一個(gè)子類。盡管標(biāo)志視圖認(rèn)為它正在處理的是 NSString 實(shí)例,但實(shí)際上它處理的是 NSMutableString 。 這意味著字符串可以通過這樣的方式變更:

    [nameString appendString:@"ny"];

在這個(gè)例子中,盡管最開始時(shí)為標(biāo)志視圖 firstName? 屬性設(shè)置的名字值是“ John ”,但因?yàn)榭勺冏址?mutable string )值被改變了,所以它現(xiàn)在變成了“Johnny ”。 你可能會(huì)選擇為標(biāo)志視圖保存一份他自己的,包含所有為它的 firstName 屬性和 lastName 屬性設(shè)置的字符串值的備份,這樣就可以有效的捕捉屬性被設(shè)置時(shí)字符串的值。通過為這兩個(gè)屬性聲明一個(gè) copy 特征可以達(dá)到這樣的目的:

    @interface XYZBadgeView : NSView
    @property (copy) NSString *firstName;
    @property (copy) NSString *lastName;
    @end

現(xiàn)在標(biāo)志視圖包含了自己關(guān)于這兩個(gè)字符串的備份了。即使可變字符串后來改變了,標(biāo)志視圖獲取的都還是這兩個(gè)字符串最初被設(shè)定的值。例如:

    NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
    self.badgeView.firstName = nameString;
    [nameString appendString:@"ny"];

這次,被標(biāo)志視圖保存的 firstName 值將會(huì)是來自于一份保有初始“ John ”字符串的抗影響備份。 Copy 特征表明,屬性因?yàn)樾枰c新創(chuàng)建的對(duì)象保持一致,而使用了強(qiáng)引用。

注: 任何你希望設(shè)置備份屬性的對(duì)象都需要支持 NSCopying ,這意味著它需要遵守?NSCopying協(xié)議。協(xié)議的描述在?Protocols Define Messaging Contracts?,獲取更多關(guān)于 NSCopying 的信息可以參看 NSCopying? 或者?Advanced Memory Management Programming Guide 如果你需要直接設(shè)置一個(gè)備份屬性的實(shí)例變量,例如在初始器方法中,不要忘記為原始的對(duì)象設(shè)置一個(gè)備份:

    - (id)initWithSomeOriginalString:(NSString *)aString {
    self = [super init];
    if (self) {
        _instanceVariableForCopyProperty = [aString copy];
    }
    return self;
    }

練習(xí)

1、為 XYZPerson 添加一個(gè) sayHello 方法,使用人的名和姓來加載一句打招呼的話。 2、聲明并實(shí)現(xiàn)一個(gè)新的指定初始器( designated initializer ),用來創(chuàng)建一個(gè)XYZPerson類,該類使用指定姓、名、和生日日期, 同時(shí)還包含合適的類制造方法( class factory method ) 3、測(cè)試當(dāng)你設(shè)置可變字符串( mutable string )做為人名,然后在調(diào)用你添加的 sayHello 方法之前,改變?nèi)嗣址闹禃?huì)出現(xiàn)的情況。為 NSString 屬性聲明添加備份特征,再測(cè)試一次。 4、嘗試使用main() 函數(shù)中各種強(qiáng)、弱變量來創(chuàng)建 XYZPerson 對(duì)象。驗(yàn)證強(qiáng)變量會(huì)按照你期望的那樣,保持XYZPerson對(duì)象活動(dòng)。 為了驗(yàn)證一個(gè) XYZPerson 對(duì)象是在何時(shí)被釋放的,你可能會(huì)想通過在? XYZPerson 實(shí)現(xiàn)中添加一個(gè) dealloc 方法,來將它與對(duì)象的生命周期關(guān)聯(lián)起來。當(dāng)一個(gè)ObjC 對(duì)象從內(nèi)存中釋放時(shí)這個(gè)方法會(huì)被自動(dòng)調(diào)用,同時(shí)該方法也可以用于釋放你手動(dòng)分配的內(nèi)存,就像C中的 malloc() 函數(shù)一樣,參看Advanced Memory Management Programming Guide

為了這種目的的練習(xí),覆寫? XYZPerson 中的 dealloc 方法來加載消息,像這樣:

    - (void)dealloc {
    NSLog(@"XYZPerson is being deallocated");
    }

嘗試通過設(shè)置 XYZPerson 中的每一個(gè)指針變量為 nil ,來驗(yàn)證對(duì)象如你所期待的那樣被 釋放了。

注: 在 Xcode 工程中,為命令行提供的樣板,在 main() 函數(shù)內(nèi)使用 @autoreleasepool { } 塊,以達(dá)到使用編譯器的自動(dòng)保留計(jì)數(shù)功能,來為你處理內(nèi)存管理。你在main() 函數(shù)中寫的任何代碼都會(huì)進(jìn)入自動(dòng)釋放池( autoreleasepool ),這點(diǎn)是非常重要的。

自動(dòng)釋放池在本文檔中沒有涉及,更多細(xì)節(jié)參看Advanced Memory Management Programming Guide

當(dāng)你正在編寫 Cocoa 或 Cocoa Touch 應(yīng)用而不是命令行時(shí),你不需要過多的擔(dān)心關(guān)于創(chuàng)建你自己的自動(dòng)釋放池的問題,因?yàn)槟菢幽憔驮趪L試進(jìn)入對(duì)象的構(gòu)架中,而這個(gè)構(gòu)架卻可以保證這個(gè)池的正確存在。 5、更改類的描述,使你可以跟蹤配偶(spouse)或伙伴(partner)。你需要考慮怎樣才能最好的模擬這種關(guān)系——你可以認(rèn)真思考一下對(duì)象圖管理。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)