南峰子的技術博客
本章的主要內(nèi)容將聚集在Runtime對成員變量與屬性的處理烟勋。在討論之前,我們先介紹一個重要的概念:類型編碼筐付。
類型編碼(Type Encoding)
作為對Runtime的補充卵惦,編譯器將每個方法的返回值和參數(shù)類型編碼為一個字符串,并將其與方法的selector關聯(lián)在一起瓦戚。這種編碼方案在其它情況下也是非常有用的沮尿,因此我們可以使用@encode編譯器指令來獲取它。當給定一個類型時较解,@encode返回這個類型的字符串編碼畜疾。這些類型可以是諸如int、指針這樣的基本類型印衔,也可以是結構體啡捶、類等類型。事實上当编,任何可以作為sizeof()操作參數(shù)的類型都可以用于@encode()届慈。
在Objective-C Runtime Programming Guide中的Type Encoding一節(jié)中,列出了Objective-C中所有的類型編碼忿偷。需要注意的是這些類型很多是與我們用于存檔和分發(fā)的編碼類型是相同的金顿。但有一些不能在存檔時使用。
注:Objective-C不支持long double類型鲤桥。@encode(long double)返回d揍拆,與double是一樣的。
一個數(shù)組的類型編碼位于方括號中茶凳;其中包含數(shù)組元素的個數(shù)及元素類型嫂拴。如以下示例:
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雖然不會直接返回它們箱沦,但它們可以作為協(xié)議中聲明的方法的類型限定符辩恼。可以參考Type Encoding。
對于屬性而言灶伊,還會有一些特殊的類型編碼疆前,以表明屬性是只讀、拷貝聘萨、retain等等竹椒,詳情可以參考Property Type String。
成員變量米辐、屬性
Runtime中關于成員變量和屬性的相關數(shù)據(jù)結構并不多胸完,只有三個,并且都很簡單儡循。不過還有個非常實用但可能經(jīng)常被忽視的特性舶吗,即關聯(lián)對象征冷,我們將在這小節(jié)中詳細討論择膝。
基礎數(shù)據(jù)類型
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; // 基地址偏移字節(jié)
#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;
關聯(lián)對象(Associated Object)
關聯(lián)對象是Runtime中一個非常實用的特性,不過可能很容易被忽視饺律。
關聯(lián)對象類似于成員變量窃页,不過是在運行時添加的。我們通常會把成員變量(Ivar)放在類聲明的頭文件中复濒,或者放在類實現(xiàn)的@implementation后面脖卖。但這有一個缺點,我們不能在分類中添加成員變量巧颈。如果我們嘗試在分類中添加新的成員變量畦木,編譯器會報錯。
我們可能希望通過使用(甚至是濫用)全局變量來解決這個問題砸泛。但這些都不是Ivar十籍,因為他們不會連接到一個單獨的實例。因此唇礁,這種方法很少使用勾栗。
Objective-C針對這一問題,提供了一個解決方案:即關聯(lián)對象(Associated Object)盏筐。
我們可以把關聯(lián)對象想象成一個Objective-C對象(如字典)围俘,這個對象通過給定的key連接到類的一個實例上。不過由于使用的是C接口,所以key是一個void指針(const void *)楷拳。我們還需要指定一個內(nèi)存管理策略绣夺,以告訴Runtime如何管理這個對象的內(nèi)存。這個內(nèi)存管理的策略可以由以下值指定:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
當宿主對象被釋放時欢揖,會根據(jù)指定的內(nèi)存管理策略來處理關聯(lián)對象陶耍。如果指定的策略是assign,則宿主釋放時她混,關聯(lián)對象不會被釋放烈钞;而如果指定的是retain或者是copy,則宿主釋放時坤按,關聯(lián)對象會被釋放毯欣。我們甚至可以選擇是否是自動retain/copy。當我們需要在多個線程中處理訪問關聯(lián)對象的多線程代碼時臭脓,這就非常有用了酗钞。
我們將一個對象連接到其它對象所需要做的就是下面兩行代碼:
static char myKey;
objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
在這種情況下,self對象將獲取一個新的關聯(lián)的對象anObject来累,且內(nèi)存管理策略是自動retain關聯(lián)對象砚作,當self對象釋放時,會自動release關聯(lián)對象嘹锁。另外葫录,如果我們使用同一個key來關聯(lián)另外一個對象時,也會自動釋放之前關聯(lián)的對象领猾,這種情況下米同,先前的關聯(lián)對象會被妥善地處理掉,并且新的對象會使用它的內(nèi)存摔竿。
id anObject = objc_getAssociatedObject(self, &myKey);
我們可以使用objc_removeAssociatedObjects函數(shù)來移除一個關聯(lián)對象面粮,或者使用objc_setAssociatedObject函數(shù)將key指定的關聯(lián)對象設置為nil。
我們下面來用實例演示一下關聯(lián)對象的使用方法拯坟。
假定我們想要動態(tài)地將一個Tap手勢操作連接到任何UIView中但金,并且根據(jù)需要指定點擊后的實際操作。這時候我們就可以將一個手勢對象及操作的block對象關聯(lián)到我們的UIView對象中郁季。這項任務分兩部分冷溃。首先,如果需要梦裂,我們要創(chuàng)建一個手勢識別對象并將它及block做為關聯(lián)對象似枕。如下代碼所示:
- (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);
}
這段代碼檢測了手勢識別的關聯(lián)對象。如果沒有年柠,則創(chuàng)建并建立關聯(lián)關系凿歼。同時,將傳入的塊對象連接到指定的key上。注意block對象的關聯(lián)內(nèi)存管理策略答憔。
手勢識別對象需要一個target和action味赃,所以接下來我們定義處理方法:
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateRecognized)
{
void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
if (action)
{
action();
}
}
}
我們需要檢測手勢識別對象的狀態(tài),因為我們只需要在點擊手勢被識別出來時才執(zhí)行操作虐拓。
從上面的例子我們可以看到心俗,關聯(lián)對象使用起來并不復雜。它讓我們可以動態(tài)地增強類現(xiàn)有的功能蓉驹。我們可以在實際編碼中靈活地運用這一特性城榛。
成員變量、屬性的操作方法
成員變量
成員變量操作包含以下函數(shù):
// 獲取成員變量名
const char * ivar_getName ( Ivar v );
// 獲取成員變量類型編碼
const char * ivar_getTypeEncoding ( Ivar v );
// 獲取成員變量的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );
- ivar_getOffset函數(shù)态兴,對于類型id或其它對象類型的實例變量狠持,可以調(diào)用object_getIvar和object_setIvar來直接訪問成員變量,而不使用偏移量瞻润。
關聯(lián)對象
關聯(lián)對象操作函數(shù)包括以下:
// 設置關聯(lián)對象
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
// 獲取關聯(lián)對象
id objc_getAssociatedObject ( id object, const void *key );
// 移除關聯(lián)對象
void objc_removeAssociatedObjects ( id object );
關聯(lián)對象及相關實例已經(jīng)在前面討論過了喘垂,在此不再重復。
屬性
屬性操作相關函數(shù)包括以下:
// 獲取屬性名
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函數(shù)敢订,返回的char *在使用完后需要調(diào)用free()釋放王污。 ● property_copyAttributeList函數(shù)罢吃,返回值在使用完后需要調(diào)用free()釋放楚午。
實例
假定這樣一個場景,我們從服務端兩個不同的接口獲取相同的字典數(shù)據(jù)尿招,但這兩個接口是由兩個人寫的矾柜,相同的信息使用了不同的字段表示。我們在接收到數(shù)據(jù)時就谜,可將這些數(shù)據(jù)保存在相同的對象中怪蔑。對象類如下定義:
@interface MyObject: NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * status;
@end
接口A、B返回的字典數(shù)據(jù)如下所示:
@{@"name1": "張三", @"status1": @"start"}
@{@"name2": "張三", @"status2": @"end"}
通常的方法是寫兩個方法分別做轉換丧荐,不過如果能靈活地運用Runtime的話缆瓣,可以只實現(xiàn)一個轉換方法,為此虹统,我們需要先定義一個映射字典(全局變量)
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: 針對特殊數(shù)據(jù)類型做處理
NSString *attributeString = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
...
[self setValue:obj forKey:propertyKey];
}
}];
}
當然车荔,一個屬性能否通過上面這種方式來處理的前提是其支持KVC渡冻。
小結
本章中我們討論了Runtime中與成員變量和屬性相關的內(nèi)容。成員變量與屬性是類的數(shù)據(jù)基礎忧便,合理地使用Runtime中的相關操作能讓我們更加靈活地來處理與類數(shù)據(jù)相關的工作族吻。