博客> Objective-C Runtime 之动态方法解析实践
Objective-C Runtime 之动态方法解析实践
2019-11-14 19:23 评论:1 阅读:2135 MaxLeap干货专栏
ios runtime Objective-C Method Resolution

作为一种动态编程语言,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];

运行这段代码,可以看到下面的输出: ```[removed] 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 为例,你可以这么实现:

```javascript
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 更多阅读 请点击原文博客

收藏
2
sina weixin mail 回到顶部