资讯专栏INFORMATION COLUMN

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

MadPecker / 1996人阅读

摘要:作为一种动态编程语言,拥有一个运行时系统来支持动态创建类,添加方法进行消息传递和转发。本篇文章会简单介绍一下消动态方法解析,并使用它实现一个容易扩展和序列化的实体类。本文仅简单介绍相关概念,更详尽的说明请参考苹果官方文档。

作为一种动态编程语言,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
更多阅读 查看原文博客

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/16186.html

相关文章

  • iOS 进阶必读 - 收藏集 - 掘金

    摘要:深入研究捕获外部变量和实现原理掘金前言是语言的扩充功能,而在和中引入了这个新功能。是由和两位大神在对的开发过程中中所有变换操作底层实现分析上掘金前言在上篇文章中,详细分析了是创建和订阅的详细过程。 深入研究Block捕获外部变量和__block实现原理 - 掘金 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能B...

    sf_wangchong 评论0 收藏0
  • iOS 进阶必读 - 收藏集 - 掘金

    摘要:深入研究捕获外部变量和实现原理掘金前言是语言的扩充功能,而在和中引入了这个新功能。是由和两位大神在对的开发过程中中所有变换操作底层实现分析上掘金前言在上篇文章中,详细分析了是创建和订阅的详细过程。 深入研究Block捕获外部变量和__block实现原理 - 掘金前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能Bl...

    szysky 评论0 收藏0
  • iOS 进阶必读

    摘要:广州线下活动八面玲珑的淘宝出品月日,由淘宝主办的地下铁沙龙在广州广电平云广场举办。此次邀请的嘉宾在方面有所实践探索,分别来自腾讯淘宝公司。从这次入院考试开始,就成功入院了。一常见类信号类。然而实际使用过程中,还是会遇到一些问题,比如的问题。 初探 CALayer 属性 一直觉得一个 view 就一个 layer,到今天才发现不是这样子的。 Xcode8调试黑科技:Memory Grap...

    waruqi 评论0 收藏0
  • iOS 进阶必读

    摘要:广州线下活动八面玲珑的淘宝出品月日,由淘宝主办的地下铁沙龙在广州广电平云广场举办。此次邀请的嘉宾在方面有所实践探索,分别来自腾讯淘宝公司。从这次入院考试开始,就成功入院了。一常见类信号类。然而实际使用过程中,还是会遇到一些问题,比如的问题。 初探 CALayer 属性 一直觉得一个 view 就一个 layer,到今天才发现不是这样子的。 Xcode8调试黑科技:Memory Grap...

    lansheng228 评论0 收藏0
  • iOS文章

    摘要:在单核时代,使用多线程技术更多时候是为了避免耗时操作堵塞了主线程。而在多核时代,多线程技术才真正完成了提升执行效率的工作。 iOS 监控 - DNS 劫持 DNS 劫持指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的 IP 地址或者什么都不做使请求失去响应。 JavaScript深入系列15篇正式完结! 写在前面 JavaScript 深入...

    dreamans 评论0 收藏0

发表评论

0条评论

MadPecker

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<