Objective-C Runtime 之動態方法解析實踐

力譜雲發表於2016-04-06

作為一種動態程式語言,Objective-C 擁有一個執行時系統來支援動態建立類,新增方法、進行訊息傳遞和轉發。利用 Objective-C 的 Runtime 可以實現一些很棒的功能。本篇文章會簡單介紹一下消動態方法解析,並使用它實現一個容易擴充套件和序列化的實體類。
本文僅簡單介紹相關概念,更詳盡的說明請參考蘋果官方文件Objective-C Runtime Programming Guide

訊息傳遞(Messaging)

在很多語言,比如 C ,呼叫一個方法其實就是跳到記憶體中的某一點並開始執行一段程式碼。沒有任何動態的特性,因為這在編譯時就已經確定了。而在 Objective-C 中,執行 [object foo] 語句並不會立即執行 foo 這個方法的程式碼。它是在執行時給 object 傳送一條叫 foo 的訊息。這個訊息,也許會由 object 來處理,也許會被轉發給另一個物件,或者不予理睬假裝沒收到這個訊息。多條不同的訊息也可以對應同一個方法實現。這些都是在程式執行的時候決定的。

事實上,在編譯時你寫的 Objective-C 函式呼叫的語法都會被翻譯成一個 C 的函式呼叫 – objc_msgSend()。比如,下面兩行程式碼就是等價的:

[object foo];

objc_msgSend(object, @selector(foo));

訊息傳遞過程:
首先,找到 object 的 class;
通過 class 找到 foo 對應的方法實現;
如果 class 中沒到 foo,繼續往它的 superclass 中找;
一旦找到 foo 這個函式,就去執行它的實現.

假如,最終沒找到 foo 的方法實現,會發生什麼呢?讓我們看一個類:

@interface SomeClass : NSObject
- (void)foo;
- (void)crash;
@end

@implementation SomeClass

-(void)foo {
   NSLog(@"method foo was called on %@", [self class]);
}

@end

SomeClass 這個類宣告瞭一個方法 foo,和一個方法 crash, 我們實現了 foo 方法,但是沒有實現 crash 方法。現在分別呼叫這兩個方法,會發生什麼?

SomeClass *someClass = [[SomeClass alloc] init];
[someClass foo];
[someClass crash];

執行這段程式碼,可以看到下面的輸出:

: method foo was called on SomeClass
: -[SomeClass crash]: unrecognized selector sent to instance 0x7ff67ac377f0
: *** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `-[SomeClass crash]: unrecognized selector sent to instance 0x7ff67ac377f0`
*** First throw call stack:
(
    0   CoreFoundation                      0x0000000101380e65 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x0000000100a70deb objc_exception_throw + 48
    2   CoreFoundation                      0x000000010138948d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    ...

程式執行了 foo 方法,並列印出日誌。然後程式崩潰了,在執行 crash 方法時就丟擲了一個異常,因為 crash 方法沒有對應的實現。但在異常丟擲前,Objective-C 的執行時會給你三次拯救程式的機會:

  • Method resolution

  • Fast forwarding

  • Normal forwarding

Method Resolution

首先,Objective-C 執行時會呼叫 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函式實現。如果你新增了函式並返回 YES, 那執行時系統就會重新啟動一次訊息傳送的過程。還是以 foo 為例,你可以這麼實現:

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

Core Data 有用到這個方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在執行時動態新增的。
如果 resolveInstanceMethod: 方法返回 NO,執行時就會進行下一步:訊息轉發(Message Forwarding)。 

實現一個容易擴充套件和序列化的實體類

這裡,就使用上述的 Normal forwarding 來建立一個容易擴充套件和序列化的類。
通常我們會這樣定義一個實體類:在類中定義許多屬性,然後通過屬性的 setter 和 getter 方法來存取值。

@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...
@end

現在我們需要把上面的實體類物件匯出成一個 JSON,可能就需要下面 toDictionary: 這樣的方法:

@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...
- (NSDictionary *)toDictionary;
@end

@implementation MyModel
- (NSDictionary *)toDictionary {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    if (self.prop1) dict[@"prop1"] = self.prop1;
    if (self.prop2) dict[@"prop2"] = self.prop2;
    return [dict copy];
}
@end

假如 MyModel 有很多個屬性,這樣寫就比較繁瑣。那麼,既然要匯出為 JSON 物件,中間肯定需要構建一個字典物件,能不能再儲存值的時候就直接儲存到一個字典中呢?於是,對上面的類改造一下:

@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
// ...

@property (nonatomic, strong) NSMutableDictionary *dictionary;
@end

@implementation MyModel

- (NSMutableDictionary *)dictionary {
    if (!_dictionary) {
        _dictionary = [NSMutableDictionary dictionary];
    }
    return _dictionary;
}

- (void)setProp1:(NSString *)prop1 {
    if (prop1) {
        self.dictionary[@"prop1"] = prop1;
    } else {
        [self.dictionary removeObjectForKey:@"prop1"];
    }
}
- (NSString *)prop1 {
    return self.dictionary[@"prop1"];
}

@end

我們在 MyModel 中加了一個屬性 dictionary,在儲存值的時候直接儲存到這個字典裡面,匯出 JSON 的時候就簡單許多。但是要對每一個屬性寫一個 setter 一個 getter,這樣也不合適。

通過觀察這些 setter 和 getter,我發現他們非常相似,而且通過這些方法名可以解析出屬性名。那麼,我們能不能在執行時再決定把值存在那個 key 下面呢?結合動態方法解析,然後就有了下面這個雛形:

@implementation MyModel

- (NSMutableDictionary *)dictionary {
    if (!_dictionary) {
        _dictionary = [NSMutableDictionary dictionary];
    }
    return _dictionary;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (isGetter) {
        // 如果 sel 是一個 Getter,動態新增一個 Getter 實現
        // Getter 的實現需要從 dictionary 中取出對應的值
        return YES;
    }
    if (isSetter) {
        // 如果 sel 是一個 Setter,就動態新增一個 Setter 實現
        // Setter 實現中需要把值儲存到 dictionary 中
        return YES;
    }
    return NO;
}

@end

- (void)setProp1:(NSString *)prop1 {
    if (prop1) {
        self.dictionary[@"prop1"] = prop1;
    } else {
        [self.dictionary removeObjectForKey:@"prop1"];
    }
}
- (NSString *)prop1 {
    return self.dictionary[@"prop1"];
}

@end

為了實現上面的功能,要做下面幾個事情:

  • 需要讓 setter 和 getter 在執行時決定

  • 執行時要判斷需要解析的 selector 是不是 setter 或者 getter。

  • 實現一個通用的 setter 和 getter

編譯器預設會為每個屬性建立 setter 和 getter 方法,可以使用 @dynamic 關鍵詞告訴編譯器不要為某個屬性建立 setter 和 getter 方法。

@implementation MyModel
// 編譯器不再自動實現 setProp1: 和 prop1 方法
// 在執行時就可以為 prop1 屬性動態新增 setter 和 getter
@dynamic prop1;
@end

最終實現的 MyModel 類如下:

@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...

@property (nonatomic, strong) NSMutableDictionary *dictionary;

+ (objc_property_t)parseSelector:(SEL)selector isGetter:(BOOL *)isGetter isSetter:(BOOL *)isSetter;
@end

// 針對 id 型別屬性 getter 的實現
void dynamicSetter(MyModel *obj, SEL sel, id value) {
    objc_property_t prop = [[obj class] parseSelector:sel isGetter:NULL isSetter:NULL];
    NSString *propName = [NSString stringWithFormat:@"%s", property_getName(prop)];
    if (value) {
        obj.dictionary[propName] = value;
    } else {
        [obj.dictionary removeObjectForKey:propName];
    }
}

// 針對 id 型別屬性 setter 的實現
id dynamicGetter(MyModel *obj, SEL sel) {
    objc_property_t prop = [[obj class] parseSelector:sel isGetter:NULL isSetter:NULL];
    NSString *propName = [NSString stringWithFormat:@"%s", property_getName(prop)];
    return obj.dictionary[propName];
}

@implementation MyModel

// 宣告這兩個屬性的 setter 和 getter 是動態建立的
@dynamic prop1, prop2;

- (NSMutableDictionary *)dictionary {
    if (!_dictionary) {
        _dictionary = [NSMutableDictionary dictionary];
    }
    return _dictionary;
}

// 判斷是否是 setter 或 getter,返回屬性名
+ (objc_property_t)parseSelector:(SEL)selector isGetter:(BOOL *)isGetter isSetter:(BOOL *)isSetter {

    NSString *selStr = NSStringFromSelector(selector);

    // 首先根據 setter 和 getter 的特點推斷出屬性名
    char propName[selStr.length +1];
    memset(propName, 0, selStr.length +1);

    if ([selStr hasPrefix:@"set"]) {
        strncpy(propName, selStr.UTF8String +3, selStr.length -4); // drop `set` and `:`
        propName[0] += (`a` - `A`); // lowercase first letter
        if (isSetter!=NULL) *isSetter = YES;
    } else {
        strncpy(propName, selStr.UTF8String, selStr.length);
        if (isGetter!=NULL) *isGetter = YES;
    }

    // 然後使用推斷出的屬性名反查屬性,如果沒找到,說明這個 selector 既不是某個屬性的 setter 也不是 getter
    objc_property_t prop = class_getProperty([self class], propName);
    if (!prop) {
        if (isSetter!=NULL) *isSetter = NO;
        if (isGetter!=NULL) *isGetter = NO;
    }

    return prop;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    BOOL isGetter, isSetter;

    objc_property_t prop = [self parseSelector:sel isGetter:&isGetter isSetter:&isSetter];
    const char *typeEncoding = property_copyAttributeValue(prop, "T");

    if (typeEncoding != NULL) {
        if (typeEncoding[0] == `@`) {
            if (isGetter) {
                class_addMethod([self class], sel, (IMP)dynamicGetter, "@@:");
                return YES;
            }
            if (isSetter) {
                class_addMethod([self class], sel, (IMP)dynamicSetter, "v@:@");
                return YES;
            }
        } else {
            // 這裡可以新增一些 setter 和 getter 實現以支援 int, float 等基本型別的屬性
        }
    }
    return NO;
}

@end

有關上面提到的屬性型別 typeEncoding 可以檢視蘋果文件

注意:上面的實現僅支援 OC 物件型別的屬性,對於 int, float 和結構體等型別的屬性,需要實現特別的 setter 和 getter。

現在,可以為 MyModel 新增許多屬性,而不用在寫 toDictionary 或者手動實現從 dictionary 中存取值的方法了。也可以繼承 MyModel,新增許多屬性:

@interface MyModelSub : MyModel
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickname;
@end

// 類實現中不需要新增許多程式碼
@implementation MyModelSub
@dynamic name, nickname;
@end

MyModel 和它子類的物件可以快速轉化成一個 NSDictionary:

MyModelSub *model = [[MyModelSub alloc] init];
model.prop1 = @"pro1value";
model.prop2 = @"pro2value";
model.name = @"Alex";
model.nickname = @"alex";
NSLog(@"model.dictionary = %@, 
 model.prop1=%@", model.dictionary, model.prop1);

執行後,可以看到下面的輸出:

model.dictionary = {
    name = Alex;
    nickname = alex;
    prop1 = pro1value;
    prop2 = pro2value;
}, 
 model.prop1=pro1value

我們可以很方便的把 NSDictionary 轉化成一個 MyModel 物件:

MyModelSub *model = [[MyModelSub alloc] init];
model.dictionary = [@{@"name":@"Alex", @"nickname":@"alex"} mutableCopy];

執行後,可以看到下面的輸出:

model.name = Alex,
model.nickname = alex

利用 Objective-C 的 runtime 特性,我們可以自己來對語言進行擴充套件,解決專案開發中的一些設計和技術問題。後續文章裡,我會介紹訊息轉發以及使用訊息轉發實現 MyModel 這樣一個類。
 
 

原文作者來自 MaxLeap 團隊_UX專業打雜成員:Alex Sun
更多閱讀 檢視原文部落格

相關文章