在OC中,我們可以通過Category 對已有的類進行擴展,這得益於OC的Runtime機制,讓類可以‘動態’的添加方法以及實現。
但是,在Category中我們無法向已有的類中添加屬性,這是因爲OC中記錄當前類屬性的ivars無法動態改變的緣故。
那麼,我們真的就無法通過Category向已有的類添加屬性了嗎?看本文標題就知道,還是有辦法可以實現的。
Category的限制
讓我們先回憶一下,當我們使用@property關鍵字聲明屬性時,OC都爲我們做了什麼。
如下代碼
@interface UIView (testCategory)
@property(nonatomic, strong) UIView *firstView;
@property(nonatomic, assign) BOOL isShown;
@end
我在UIView的Category中聲明瞭兩個屬性,firstView與isShown。
一般情況下,.h中聲明屬性後,我們就可在類的實例中使用這些屬性了。
爲了能夠正確使用屬性,OC會默認爲我們完成以下工作
- 在.m中,編譯器通過@synthesize關鍵字,將我們聲明的屬性轉換爲了對應的實例變量。並默認在屬性名稱前添加‘_’來在類中標識對應的實例變量。
- 根據我們在@property中指定的訪問限制說明(readwrite,readonly),編譯器會自動生成默認的setter/getter方法(其本質仍是對帶下劃線的實例變量的操作)。(如果是readonly,則只會生成getter方法)
由property生成的實例變量,會在類實例創建時(alloc)被分配內存,類實例銷燬時釋放內存。
現在我們再來看看上面OC爲了支持property所做的的工作。其中@synthesize與生成setter/getter方法,均是在編譯期完成的,而Category則屬於Runtime時期加載,自然編譯器就不會爲我們做@synthesize與生成對應setter/getter方法的工作了,因此我們也就不能夠在Category中添加屬性。
所以,當我使用UIView (testCategory)類實例調用firstView屬性時,程序運行時會因爲unrecognized selector 異常退出。(因爲編譯器並沒有爲我們生成對應的accessor方法)
- (void)viewDidLoad {
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.firstView = [[UIView alloc] init]; // unrecognized selector crash!!
}
異常錯誤
好,既然編譯器沒有自動生成accessor方法,那我自己寫可以嗎?
如圖中所示,因爲編譯器不會爲我們生成對應的_fristView實例變量,因此accessor方法中會提示未聲明變量_firstView錯誤,導致編譯無法通過。
我還是不死心,我要用@synthesize來告訴編譯器要進行property->實例變量的轉換:
OK,Xcode編輯器說的很明確,不能夠在Category中使用@synthesize關鍵字。原因很簡單,因爲@synthesize是在編譯期完成的,對於Runtime時期才加載的Category,自然是沒有意義的。
服了吧,難道我們真的不能夠在Category中添加屬性嗎?下面,我們就介紹兩種在Category中添加property的方法。
僅聲明@property
在上面的示例代碼中,我們發現,僅僅在.h中聲明屬性,是不會有任何編譯錯誤的,而且可以編譯通過。
只是在使用屬性的時候,由於沒有實現相應的accessor方法,纔會引發unrecognized selector異常。
那麼,我們可以不借助編譯器,而是在Category的.m文件中手動實現accessor方法。之前說過,在Category中直接用下劃線屬性名稱的方式是無法獲取對應的實例變量的。我們這裏就不創建新的實例變量,而是對當前類已有的屬性做一個封裝,這裏面對已有屬性做一些操作,實際上是一個函數,但卻可以通過屬性的方式進行調用。
示例代碼如下:
#import <UIKit/UIKit.h>
@interface UIView (testCategory)
@property(nonatomic, strong) UIView *firstView;
//@property(nonatomic, assign) BOOL isShown;
@end
#import "UIView+testCategory.h"
@implementation UIView (testCategory)
-(void) setFirstView:(UIView *)firstView
{
[self addSubview:firstView];
[self bringSubviewToFront:firstView];
}
-(UIView *) firstView
{
return self.subviews.firstObject;
}
@end
在UIView testCategory中我聲明瞭一個UIView *property叫firstView,同時手工實現了其accessor方法,由於在accessor方法中我並沒有使用任何新的實例變量,而是對UIView的subviews操作做了一些封裝,因此編譯器並不會報任何錯誤。
但是,在代碼中,我們卻可以像屬性一樣調用firstView:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.firstView = [[UIView alloc] init];
}
這種方式實際上是通過property的形式調用函數方法,並沒有在類中添加新的屬性。下面一種方法,則可以將id變量像屬性一樣與類實例關聯起來,並像屬性一樣調用。
Associated Objects
OC的Runtime爲我們提供了能夠將id變量通過key的方式與當前類實例關聯起來。
當我們想要建立關聯時,需要引入runtime頭文件:
<objc/runtime.h>
關聯方法則包括如下三個C函數:
- objc_setAssociatedObject
- objc_getAssociatedObject
objc_removeAssociatedObjects
我們想通過Category向類添加屬性,則在.h中先聲明屬性:
#import <UIKit/UIKit.h>
@interface UIView (testCategory)
@property(nonatomic, strong) UIView *firstView;
//@property(nonatomic, assign) BOOL isShown;
@end
在.m中編寫對應的accessor方法:
#import <objc/runtime.h>
#import "UIView+testCategory.h"
@implementation UIView (testCategory)
-(void) setFirstView:(UIView *)firstView
{
objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
}
-(UIView *) firstView
{
return objc_getAssociatedObject(self, @selector(firstView));
}
@end
其中objc_setAssociatedObject, objc_getAssociatedObject的聲明爲:
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
id objc_getAssociatedObject(id object, const void *key)
這裏要說明的只有一點,關於建立關聯的key值,key值可以是任何類型,但是要保證其唯一性,const,並且在setter和getter中均能夠被訪問。通常我們使用一個static const char * 來作爲key。但是在OC中,由於@selector恰好具有同樣的特性,因此這裏我們將@selector(firstView)作爲了key值。
在代碼中,我們可以這樣調用我們的屬性firstView:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.firstView = [[UIView alloc] init];
myView.firstView.tag = 12;
NSLog(@"The view tag is %ld", myView.firstView.tag);
}
輸出:
OK,我們已經成功的在Category中爲UIView類“添加”了新的屬性firstView。
需要注意的是,通過associate方法管理對象,其關聯的對象是id類型,即必須是NSObject類及其子類對象。對於像BOOL這樣的一般變量,是無法關聯的。
另外,對應於屬性的strong,copy,weak,atomic,在關聯對象也有對應的關聯policy,見說明:
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
對於OBJC_ASSOCIATION_ASSIGN,雖然是weak引用,但其並不會像property的weak那樣釋放後自動爲nil,而是一個野指針。這裏要注意不要引發BAD ACCESS異常。