Effective Object-C 第10條:在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)
- Effective Object-C 2.0 編寫高質(zhì)量iOS與OSX的52個有效方法-學(xué)習(xí)筆記
有時需要在對象中存放相關(guān)信息。這時我們通常會從對象所屬的類中繼承一個子類,然后改用這個子類對象。然而并非所有情況下都能這么做,有時候類的實(shí)例可能是由某種機(jī)制所創(chuàng)建的,而開發(fā)者無法令這種機(jī)制創(chuàng)建出自己縮寫的子類實(shí)例。OC中有一項(xiàng)強(qiáng)大的特性可以解決此問題,這就是關(guān)聯(lián)對象。
可以給某對象關(guān)聯(lián)許多其他對象,這些對象通過鍵來區(qū)分。存儲對象值的時候,可以指明存儲策略,用以維護(hù)響應(yīng)的內(nèi)存管理語義。存儲策略由名為objc_AssociationPolicy的枚舉所定義,同時還列出了與之等效的@property屬性:假如關(guān)聯(lián)對象成為了屬性,那么它就會具備對應(yīng)的語義。
| 關(guān)聯(lián)類型 | 等效的@property語義 |
|---|---|
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | retain, nonatomic |
| OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
| OBJC_ASSOCIATION_RETAIN | retain |
| OBJC_ASSOCIATION_COPY | copy |
下列方法可以管理關(guān)聯(lián)對象:
//以給定的鍵和策略為某對象設(shè)置關(guān)聯(lián)對象值
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
//根據(jù)給定的鍵從某對象中國區(qū)響應(yīng)的關(guān)聯(lián)對象值
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//移除指定對象的全部關(guān)聯(lián)對象
void objc_removeAssociatedObjects(id _Nonnull object)
我們可以把某對象想象成NSDictionary,把關(guān)聯(lián)到該對象的值理解為字典中的條目,于是,存取關(guān)聯(lián)對象的值就相當(dāng)于在NSDictionary對象上調(diào)用[dictionary setValue:value forKey:key]與[object objectForKey:key]方法。然而兩者之間有個重要的差別:設(shè)置關(guān)聯(lián)對象時用的鍵是個不透明的指針。如果在兩個鍵上調(diào)用isEqual:方法的返回值是YES,那么NSDictionary就認(rèn)為兩者相等;然而在設(shè)置關(guān)聯(lián)對象值時,若想另兩個鍵匹配到同一個值,則二者必須是完全相同的指針才行。鑒于此,在設(shè)置關(guān)聯(lián)對象值時,通常使用靜態(tài)全局變量做鍵。
關(guān)聯(lián)對象用法舉例
拿UIAlertView類舉例,當(dāng)用戶按下按鈕關(guān)閉該視圖時,需要用委托協(xié)議來處理此動作,但是,要想設(shè)置好這個委托機(jī)制,就得把創(chuàng)建警告視圖和處理按鈕動作代碼分開。由于代碼分作兩塊,所以讀起來有點(diǎn)亂。
- (void)askUserQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"what do you want to do?"
delegate:self
cancelButtonTitle:@"cancel"
otherButtonTitles:@"continue", nil];
[alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
}
如果想在同一個類里處理多個警告信息視圖,那么代碼就會變得更為復(fù)雜,我們必須在delegate方法中檢查傳入的alertView參數(shù),并據(jù)此選用相應(yīng)的邏輯。要是能在創(chuàng)建警告視圖的時候直接把處理每個按鈕的邏輯都寫好,那就簡單多了。這可以通過關(guān)聯(lián)對象來做。創(chuàng)建完警告視圖之后,設(shè)定一個與之關(guān)聯(lián)的塊(block),等到執(zhí)行delegate方法時再將其讀出來。代碼如下:
#import <objc/runtime.h>
static void *MyAlertViewKey = "MyAlertViewKey";
- (void)askUserQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"what do you want to do?"
delegate:self
cancelButtonTitle:@"cancel"
otherButtonTitles:@"continue", nil];
__weak typeof(self) weakSelf = self;
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
__strong typeof(weakSelf) strongSelf = weakSelf;
if (buttonIndex == 0) {
[strongSelf doCancel];
} else {
[strongSelf doContinue];
}
};
objc_setAssociatedObject(alert,
MyAlertViewKey,
block,
OBJC_ASSOCIATION_COPY);
[alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, MyAlertViewKey);
block(buttonIndex);
}
以這種方式改寫之后,創(chuàng)建警告視圖與處理操作結(jié)果的代碼都放在一起了,這樣比原來更易讀懂,因?yàn)槲覀儫o須在兩部分代碼之間來回游走,即可明白警告視圖的用處。block也容易造成“循環(huán)引用”。
這種做法很有用,但是只應(yīng)該在其他辦法行不通時才去考慮用它。若是濫用,則很快就會令代碼失控,使其難于調(diào)試?!把h(huán)引用”產(chǎn)生的原因很難查明,因?yàn)殛P(guān)聯(lián)對象之間的關(guān)系并沒有正式定義,其內(nèi)存管理語義是在關(guān)聯(lián)的時候才定義的,而不是在接口中預(yù)先定好的。使用這種寫法時要小心,不能僅僅因?yàn)槟程幙梢杂迷搶懛ň鸵欢ㄒ盟?。想?chuàng)建這種UIAlertView還有個辦法,那就是從中繼承子類,把block保存為子類中的屬性。若是需要多次用到alert視圖,那么這種做法比使用關(guān)聯(lián)對象要好。
要點(diǎn)
- 可以通過“關(guān)聯(lián)對象”機(jī)制來把兩個對象連起來。
- 定義關(guān)聯(lián)對象時可指定內(nèi)存管理語義,用以模仿定義屬性時所采用的“擁有關(guān)系”與“非擁有關(guān)系”。
- 只有在其他做法不可行時才應(yīng)選擇用關(guān)聯(lián)對象,因?yàn)檫@種做法通常會引入難于查找的bug。