本章的主要內容將彙集在Runtime對成員變量與屬性的處理。在討論以前,咱們先介紹一個重要的概念:類型編碼。html
做爲對Runtime的補充,編譯器將每一個方法的返回值和參數類型編碼爲一個字符串,並將其與方法的selector關聯在一塊兒。這種編碼方案在其它狀況下也是很是有用的,所以咱們可使用@encode編譯器指令來獲取它。當給定一個類型時,@encode返回這個類型的字符串編碼。這些類型能夠是諸如int、指針這樣的基本類型,也能夠是結構體、類等類型。事實上,任何能夠做爲sizeof()操做參數的類型均可以用於@encode()。ios
在Objective-C Runtime Programming Guide中的Type Encoding一節中,列出了Objective-C中全部的類型編碼。須要注意的是這些類型不少是與咱們用於存檔和分發的編碼類型是相同的。但有一些不能在存檔時使用。數組
注:Objective-C不支持long double類型。@encode(long double)返回d,與double是同樣的。數據結構
一個數組的類型編碼位於方括號中;其中包含數組元素的個數及元素類型。如如下示例:多線程
float a[] = {1.0, 2.0, 3.0};
1
|
NSLog(@"array encoding type: %s", @encode(typeof(a))); |
輸出是:app
2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]
其它類型可參考Type Encoding,在此不細說。ide
另外,還有些編碼類型,@encode雖然不會直接返回它們,但它們能夠做爲協議中聲明的方法的類型限定符。能夠參考Type Encoding。函數
對於屬性而言,還會有一些特殊的類型編碼,以代表屬性是隻讀、拷貝、retain等等,詳情能夠參考Property Type String。性能
Runtime中關於成員變量和屬性的相關數據結構並很少,只有三個,而且都很簡單。不過還有個很是實用但可能常常被忽視的特性,即關聯對象,咱們將在這小節中詳細討論。ui
Ivar是表示實例變量的類型,其實際是一個指向objc_ivar結構體的指針,其定義以下:
typedef struct objc_ivar *Ivar; struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; // 變量名 char *ivar_type OBJC2_UNAVAILABLE; // 變量類型 int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字節 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
objc_property_t是表示Objective-C聲明的屬性的類型,其實際是指向objc_property結構體的指針,其定義以下:
typedef struct objc_property *objc_property_t;
objc_property_attribute_t定義了屬性的特性(attribute),它是一個結構體,定義以下:
typedef struct { const char *name; // 特性名 const char *value; // 特性值 } objc_property_attribute_t;
關聯對象是Runtime中一個很是實用的特性,不過可能很容易被忽視。
關聯對象相似於成員變量,不過是在運行時添加的。咱們一般會把成員變量(Ivar)放在類聲明的頭文件中,或者放在類實現的@implementation後面。但這有一個缺點,咱們不能在分類中添加成員變量。若是咱們嘗試在分類中添加新的成員變量,編譯器會報錯。
咱們可能但願經過使用(甚至是濫用)全局變量來解決這個問題。但這些都不是Ivar,由於他們不會鏈接到一個單獨的實例。所以,這種方法不多使用。
Objective-C針對這一問題,提供了一個解決方案:即關聯對象(Associated Object)。
咱們能夠把關聯對象想象成一個Objective-C對象(如字典),這個對象經過給定的key鏈接到類的一個實例上。不過因爲使用的是C接口,因此key是一個void指針(const void *)。咱們還須要指定一個內存管理策略,以告訴Runtime如何管理這個對象的內存。這個內存管理的策略能夠由如下值指定:
OBJC_ASSOCIATION_ASSIGN OBJC_ASSOCIATION_RETAIN_NONATOMIC OBJC_ASSOCIATION_COPY_NONATOMIC OBJC_ASSOCIATION_RETAIN OBJC_ASSOCIATION_COPY
當宿主對象被釋放時,會根據指定的內存管理策略來處理關聯對象。若是指定的策略是assign,則宿主釋放時,關聯對象不會被釋放;而若是指定的是retain或者是copy,則宿主釋放時,關聯對象會被釋放。咱們甚至能夠選擇是不是自動retain/copy。當咱們須要在多個線程中處理訪問關聯對象的多線程代碼時,這就很是有用了。
咱們將一個對象鏈接到其它對象所須要作的就是下面兩行代碼:
static char myKey; objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
在這種狀況下,self對象將獲取一個新的關聯的對象anObject,且內存管理策略是自動retain關聯對象,當self對象釋放時,會自動release關聯對象。另外,若是咱們使用同一個key來關聯另一個對象時,也會自動釋放以前關聯的對象,這種狀況下,先前的關聯對象會被妥善地處理掉,而且新的對象會使用它的內存。
id anObject = objc_getAssociatedObject(self, &myKey);
咱們可使用objc_removeAssociatedObjects函數來移除一個關聯對象,或者使用objc_setAssociatedObject函數將key指定的關聯對象設置爲nil。
咱們下面來用實例演示一下關聯對象的使用方法。
假定咱們想要動態地將一個Tap手勢操做鏈接到任何UIView中,而且根據須要指定點擊後的實際操做。這時候咱們就能夠將一個手勢對象及操做的block對象關聯到咱們的UIView對象中。這項任務分兩部分。首先,若是須要,咱們要建立一個手勢識別對象並將它及block作爲關聯對象。以下代碼所示:
- (void)setTapActionWithBlock:(void (^)(void))block { UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey); if (!gesture) { gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)]; [self addGestureRecognizer:gesture]; objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN); } objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY); }
這段代碼檢測了手勢識別的關聯對象。若是沒有,則建立並創建關聯關係。同時,將傳入的塊對象鏈接到指定的key上。注意block對象的關聯內存管理策略。
手勢識別對象須要一個target和action,因此接下來咱們定義處理方法:
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateRecognized) { void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey); if (action) { action(); } } }
咱們須要檢測手勢識別對象的狀態,由於咱們只須要在點擊手勢被識別出來時才執行操做。
從上面的例子咱們能夠看到,關聯對象使用起來並不複雜。它讓咱們能夠動態地加強類現有的功能。咱們能夠在實際編碼中靈活地運用這一特性。
成員變量操做包含如下函數:
// 獲取成員變量名 const char * ivar_getName ( Ivar v ); // 獲取成員變量類型編碼 const char * ivar_getTypeEncoding ( Ivar v ); // 獲取成員變量的偏移量 ptrdiff_t ivar_getOffset ( Ivar v );
● ivar_getOffset函數,對於類型id或其它對象類型的實例變量,能夠調用object_getIvar和object_setIvar來直接訪問成員變量,而不使用偏移量。
關聯對象操做函數包括如下:
// 設置關聯對象 void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy ); // 獲取關聯對象 id objc_getAssociatedObject ( id object, const void *key ); // 移除關聯對象 void objc_removeAssociatedObjects ( id object );
關聯對象及相關實例已經在前面討論過了,在此再也不重複。
屬性操做相關函數包括如下:
// 獲取屬性名 const char * property_getName ( objc_property_t property ); // 獲取屬性特性描述字符串 const char * property_getAttributes ( objc_property_t property ); // 獲取屬性中指定的特性 char * property_copyAttributeValue ( objc_property_t property, const char *attributeName ); // 獲取屬性的特性列表 objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
● property_copyAttributeValue函數,返回的char *在使用完後須要調用free()釋放。
● property_copyAttributeList函數,返回值在使用完後須要調用free()釋放。
假定這樣一個場景,咱們從服務端兩個不一樣的接口獲取相同的字典數據,但這兩個接口是由兩我的寫的,相同的信息使用了不一樣的字段表示。咱們在接收到數據時,可將這些數據保存在相同的對象中。對象類以下定義:
@interface MyObject: NSObject @property (nonatomic, copy) NSString * name; @property (nonatomic, copy) NSString * status; @end
接口A、B返回的字典數據以下所示:
@{@"name1": "張三", @"status1": @"start"} @{@"name2": "張三", @"status2": @"end"}
一般的方法是寫兩個方法分別作轉換,不過若是能靈活地運用Runtime的話,能夠只實現一個轉換方法,爲此,咱們須要先定義一個映射字典(全局變量)
static NSMutableDictionary *map = nil; @implementation MyObject + (void)load { map = [NSMutableDictionary dictionary]; map[@"name1"] = @"name"; map[@"status1"] = @"status"; map[@"name2"] = @"name"; map[@"status2"] = @"status"; } @end
上面的代碼將兩個字典中不一樣的字段映射到MyObject中相同的屬性上,這樣,轉換方法可以下處理:
- (void)setDataWithDic:(NSDictionary *)dic { [dic enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { NSString *propertyKey = [self propertyForKey:key]; if (propertyKey) { objc_property_t property = class_getProperty([self class], [propertyKey UTF8String]); // TODO: 針對特殊數據類型作處理 NSString *attributeString = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding]; ... [self setValue:obj forKey:propertyKey]; } }]; }
固然,一個屬性可否經過上面這種方式來處理的前提是其支持KVC。
本章中咱們討論了Runtime中與成員變量和屬性相關的內容。成員變量與屬性是類的數據基礎,合理地使用Runtime中的相關操做能讓咱們更加靈活地來處理與類數據相關的工做。