Objective-C Runtime 執行時之二:成員變數與屬性

南峰子的技術部落格發表於2014-11-11

在前面一篇文章中,我們介紹了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中的相關操作能讓我們更加靈活地來處理與類資料相關的工作。

相關文章