標籤:
在前面一篇文章中,我們介紹了Runtime中與類和對象相關的內容,從這章開始,我們將討論類實現細節相關的內容,主要包括類中成員變數,屬性,方法,協議與分類的實現。
本章的主要內容將聚集在Runtime對成員變數與屬性的處理。在討論之前,我們先介紹一個重要的概念:類型編碼。
類型編碼(Type Encoding)
作為對Runtime的補充,編譯器將每個方法的傳回值和參數類型編碼為一個字串,並將其與方法的selector關聯在一起。這種編碼方案在其它情況下也是非常有用的,因此我們可以使用@encode編譯器指令來擷取它。當給定一個類型時,@encode返回這個類型的字串編碼。這些類型可以是諸如int、指標這樣的基本類型,也可以是結構體、類等類型。事實上,任何可以作為sizeof()巨集指令引數的類型都可以用於@encode()。
在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};
- NSLog(@"array encoding type: %s", @encode(typeof(a)));
輸出是:
- 2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]
其它類型可參考Type Encoding,在此不細說。
另外,還有些編碼類別型,@encode雖然不會直接返回它們,但它們可以作為協議中聲明的方法的類型限定符。可以參考Type Encoding。
對於屬性而言,還會有一些特殊的類型編碼,以表明屬性是唯讀、拷貝、retain等等,詳情可以參考Property Type String。
成員變數、屬性
Runtime中關於成員變數和屬性的相關資料結構並不多,只有三個,並且都很簡單。不過還有個非常實用但可能經常被忽視的特性,即關聯對象,我們將在這小節中詳細討論。
基礎資料類型
Ivar
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
objc_property_t是表示Objective-C聲明的屬性的類型,其實際是指向objc_property結構體的指標,其定義如下:
- typedef struct objc_property *objc_property_t;
objc_property_attribute_t
objc_property_attribute_t定義了屬性的特性(attribute),它是一個結構體,定義如下:
- typedef struct {
- const char *name; // 特性名
- const char *value; // 特性值
- } objc_property_attribute_t;
關聯對象(Associated Object)
關聯對象是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中的相關操作能讓我們更加靈活地來處理與類資料相關的工作。
Objective-C Runtime 運行時之二:成員變數與屬性