1. 程式人生 > >Objective-C 給分類新增屬性——關聯物件

Objective-C 給分類新增屬性——關聯物件

給分類新增屬性

  咱們知道,分類中可以新增方法,卻無法新增例項變數。那咱們有什麼方法在既有類中存放自定義資料呢?
先來看看下面這段程式碼:

@interface UIView (nl_Frame)
@property (nonatomic, assign) CGFloat nl_width;
@end
 @implementation UIView (nl_Frame)
- (void)setNl_width:(CGFloat)nl_width {
  CGRect frame = self.frame;
  frame.size.width = nl_width;
  self
.frame = frame; } - (CGFloat)nl_width { return CGRectGetWidth(self.frame); } @end

  在這裡給 UIView增加了一個寬度屬性:nl_width,而且為其實現了相應的 getter和 setter方法(nl_widthsetNl_width:)。這兩個方法實際上訪問的 frame屬性,如果你有注意過的話,你會發現frame也是定義在分類裡邊的:

@interface UIView(UIViewGeometry)
@property(nonatomic) CGRect            frame;
//...
@end

  可以看到,這種定義在分類裡的屬性,實際上是實現了相應的方法,並在方法裡邊通過訪問其它屬性來達到目的。這通常用來簡化某些操作,比如定義咱們這個分類後,獲取檢視的寬度只要view.nl_width就可以了,再不用CGRectGetWidth(view.frame)來得到寬度,而且可讀性也增強了很多。

  再來看看這個需求:在 sqlite中,第一個表如果在沒有指定主鍵的情況下,那預設就會定義一個主鍵rowid。咱們就把這個 rowid直接放到 NSObject裡邊,作為屬性,那麼任何物件也會有這個主鍵rowid了。但是這個rowid卻無法像上邊的nl_width一樣通過訪問其它屬性來達到目的。那該怎麼辦?

關聯物件

  本節的主角出場了:關聯物件
  在使用關聯物件之前,得先引入標頭檔案:

#import <objc/runtime.h>

  可以在該標頭檔案中找到三個允許你將任何鍵值在執行時關聯到物件上的函式:

objc_setAssociatedObject       // 設定關聯物件
objc_getAssociatedObject       // 獲取關聯物件
objc_removeAssociatedObjects   // 移除關聯物件

既然有了這個工具,那麼咱們再來看看:

@interface NSObject (nl_sqlite)
@property (nonatomic, assign) NSUInteger rowid;
@end
@implementation NSObject (nl_sqlite)
static void *nl_sqlite_rowid_key = &nl_sqlite_rowid_key;
- (void)setRowid:(NSUInteger)rowid {
  objc_setAssociatedObject(self, nl_sqlite_rowid_key, @(rowid), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSUInteger)rowid {
  return [objc_getAssociatedObject(self, nl_sqlite_rowid_key) unsignedLongValue];
}
@end

  上面的程式碼,就是通過關聯物件給NSObject增加了一個rowid“屬性”。關聯物件在使用時,需要咱們提供一個指標,即key,用來識別被關聯的物件。咱們這裡的key就是一個空指標:nl_sqlite_rowid_key。當然,你也可以@selector(rowid)來作為 key(常用)。
於是,就可以這麼來用了:

id person = [NSObject new];
person.rowid = 1;

  很爽吧!以後就可以給已有類新增“屬性”了。這可是一個很強大的功能喲,如果你檢視過一些強大的第三方庫的話,就會發現,這是一個常用的技巧。

關聯型別 等效的@property屬性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

為什麼分類無法新增例項變數

  這個問題牽扯到記憶體佈局。有一個個人資訊的類,定義的例項變數如下:

@interface Person : NSObject {
  NSString *_firstName;
  NSString *_lastName;
  NSString *_someInternalData;
}

  如果你對 C++或 Java比較熟悉的話,那你也應該比較熟悉這種寫法,在這些語言中,可以定義例項變數的作用域。然而編寫 OC程式碼時,卻很少這麼做。這種寫法的問題是:物件佈局在編譯期就已經固定了。只要碰到訪問_firstName變數的程式碼,編譯器就把其替換為“偏移量”(offset),這個偏移量是“硬編碼”(hardcode),表示該變數距離存放物件的記憶體區域的起始地址有多遠。這樣做目前來看沒問題,但是如果又加了一個例項變數,那就麻煩了。比如說,假設在_firstName之前又多了一個例項變數:

@interface Person : NSObject {
  NSDate *_dateOfBirth;
  NSString *_firstName;
  NSString *_lastName;
  NSString *_someInternalData;
}

  原來表示_firstName的偏移量現在卻指向_dateOfBirth了。把偏移量硬編碼於其中的那麼程式碼都會讀取到錯誤的值。
  如果程式碼使用了編譯期計算出來的偏移量,那麼在修改類定義之後必須重新編譯,否則就會出錯。例如,某個程式碼庫中的程式碼使用了一份舊的類定義。如果和其相連結的程式碼使用了新的定義,那麼執行時就會出現不相容現象。當然,真實的情況遠比這複雜。
  所以在分類中,自然也就無法新增例項變量了。不然會打亂類中的偏移量。
  

關聯物件的基本原理

  OC已開源:http://opensource.apple.com/tarballs/objc4/objc4-646.tar.gz
  這裡只分析其基本原理:
  在執行時環境中,有一個全域性的 hashMap:associations。它用來記錄那些有“關聯物件”的物件。當一個物件object第一次關聯物件時,這個 associations就將其作為 key加入到 hashMap中,而 value則是一另一個 map物件。就以上面的 rowid來說,當第一次 setRowid時,虛擬碼如下:

  ObjectAssociationMap *refs = new ObjectAssociationMap;
  associations[object] = refs;
  (*refs)[key] = ObjcAssociation(policy, rowid);

  看到這裡,物件跟 NSDictionary很像呢!把關聯到該物件的值看成字典中的 value。於是,存取關聯物件的值就相當於在 NSDictionary物件上呼叫[object setObject:value forKey:key]與[object objectForKey:key]方法。然而兩者之間有個重要的差別:設定關聯物件時用的 key是個“不透明的指標”。如果在兩個 key上呼叫“isEqual:”方法的返回值是 YES,那麼 NSDictionary就認為二者相等;然而在設定關聯物件值時,若想令兩個鍵匹配到同一個值,則二者必須是完全相同的指標才行。