美文网首页iOSiOS进阶指南程序员
Objective-C Runtime 之动态方法解析实践

Objective-C Runtime 之动态方法解析实践

作者: 商领云 | 来源:发表于2016-04-11 10:25 被阅读229次

    作为一种动态编程语言,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 = %@, \n 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,转载请务必注明作者和原文出处
    欢迎访问原文链接

    相关文章

      网友评论

        本文标题:Objective-C Runtime 之动态方法解析实践

        本文链接:https://www.haomeiwen.com/subject/gneplttx.html