运行时编程

作者: 于先笙 | 来源:发表于2018-03-01 10:47 被阅读21次

    Objective-C 拥有相当多的动态特性,这些特性在运行程序时发挥作用,而不是在编译或者链接代码时发挥作用。Objective-C运行时系统实现了这些特性,而这些功能为Objective-C语言提供了非常多的强大功能和灵活性。

    • 运行时系统
    • 运行时系统的结构
    • 使用运行时系统API

    运行时系统

    1.1 动态功能

    在运行时,Objective-C语言会执行其他语言在程序编译或链接时会执行的许多常规操作,如确定类型和方法解析。这些操作还可以提供API,使你编写的程序能够执额外的运行时操作,如对象内省和以动态方式创建和加载代码。Objective-C运行时系统的结构和实现方式生成了这些动态特性

    1.2 对象消息

    在OOP术语中,消息传递是指一种在对象之间发送和接受消息的通信模式。用于调用类和类实例的方法。

    对象消息传递是以动态方式实现的特性,接收器的类型和相应的调用方法是在运行时决定的。

    • 消息:向对象/类发送的名称(选择器)和一些列参数
    • 方法:Objective-C中的类或实例方法,其申明中含有名称、输入参数、返回值和方法签名(即输入参数和返回值的数据类型)
    • 方法绑定:接收向指定接收器发送的消息并寻找和执行适当方法的处理过程。Objective-C运行时系统在调用方法时,会以动态绑定方式处理消息。

    1.3 选择器

    选择器是一种文本字符串,用于指明调用对象或类中的哪些方法。选择器是一种分为多个段的文本字符串,每个段以冒号结尾并且后跟参数,例如:

    分段1:分段2:分段3:
    

    这个选择器中有3个分段,每个分段都带一个冒号,因而表明了相应的消息具有3个输入参数。

    // 选择器示例
    description
    description:
    sumAddend1:addend2:
    sumAddend1::
    

    消息的选择器直接与一个或多个类/实例方法声明对应。下面展示一个类接口,该接口中含有一个实例方法声明

    @interface Calculator : NSObject
    - (int)sumAddend1:(NSInteger)a1 addend2:(NSInteger)a2;
    @end
    

    Calculator 类实例方法的选择器为sumAddend1:addend2:。如果初始化一个Calculator对象并将其分配给一个名为myCalculator的变量,那么调用该实例方法就需要使用接收器对象(myCalculator)后跟带输入参数的选择器,例如:

    [myCalculator sumAddend1:25 addend2:10];
    

    当源代码被编译时,编译器(运行时系统的组成部分)会创建数据结构和函数调用语句,使用它们以动态方式将接收器(类/对象)和消息选择器与方法的实现代码对应起来。在执行程序时,运行时库(运行时系统的另一个组成部分)利用这些消息找到并调用适当的方法。

    选择器类型(SEL)是一种特殊的Objective-C数据类型,是用于在编译源代码时替换选择器值的唯一标识符。所有具有相同选择器值的方法都拥有相同的SEL标识符。可使用关键字@selector创建SEL类型的变量,例如:

    SEL myMethod = @selector(myMethod:);
    

    如果在运行时创建选择器变量,可以这样写:

    SEL myMethod = NSSelectorFromString(@"myMethod:");
    [obj performSelector: myMethod withObject:[NSNumber numberWithInteger:40]];
    

    1.4 方法签名

    方法签名(method signature)定义了方法输入参数的数据类型和方法的返回值(如果存在)。编译器会将 [接收器 消息] 形式的对象消息,转换为声明中含有方法签名的(ANSI)C函数调用语句。因此,为了生成正确的对象消息传递代码,编译器需要获得选择器值和方法签名。编译器可以从对象消息表达式中轻松提取选择器,但是它如何获取方法签名呢?消息可能含有输入参数,而且因为接收器和相应的方法是在程序运行时确定的,所以编译器无法知道使用怎样的数据类型才能与要调用的方法对应起来。为了确定正确的方法签名,编译器会根据已解析的方法声明进行猜测。如果它找不到方法签名,或者它从方法声明获得的方法签名与运行时实际执行的方法不匹配,就会出现方法签名不匹配的情况,会导致从编译器警告道运行时错误的各种问题。

    1.5 使用对象消息

    创建Calculator类,继承NSObject类,添加两个实例方法:sumAddend1:addend2: 和 sumAddend1::。

    @interface Calculator : NSObject
    
    - (NSNumber *)sumAddend1:(NSNumber *)a1 addend2:(NSNumber *)a2;
    - (NSNumber *)sumAddend1:(NSNumber *)a1 :(NSNumber *)a2;
    
    @end
    

    我们可以这样调用该方法

    Calculator *obj = [[Calculator alloc] init];
    NSNumber *rs1 = [obj sumAddend1:@(10) addend2:@(15)];
    NSNumber *rs2 = [obj sumAddend1:@(10) :@(25)];
    

    或者通过运行时方式调用该方法

    SEL myMethod = NSSelectorFromString(@"sumAddend1::");
    NSNumber *rs3 = [obj performSelector:myMethod withObject:@(10) withObject:@(35)];
    

    1.5 动态类型

    运行时系统通过动态类型(dynamic typing)功能,可以在运行程序时决定对象的类型,因而可以使运行时因素能够在程序中指定使用哪种类型对象。在事先无法知道为变量分配那种类型的对象的情况下(如向方法传送参数),这就特别有用。既支持静态类型也支持动态类型。当使用静态方式设置变量的类型时,变量的类型就由它的声明决定。

    // 变量 myAtom 声明为指向Atom类实例的指针
    Atom *myAtom;
        
    // myAtom 声明为id数据类型,运行时检查myAtom类型
    id *myAtom;
    

    使用动态类型可以简化类接口,无须为每个可能出现的输入参数类型编写不同的方法声明。动态类型还可以提供非常大的灵活性,可以在执行程序的过程中改进程序使用的数据类型,并在不重新编译和重新部署的情况下引入新的数据类型。

    1.6 动态绑定

    动态绑定是指在运行时(而不是编译时)将消息与方法对应起来的处理过程。因为许多接收器对象会实现相同的方法,调用方法的方式会动态变化。因此动态绑定实现了 OOP 的多态性。使用动态绑定可以在不影响既有代码的情况下,将新对象和代码连接或添加到系统中,从而降低对象之间的耦合度。通过消除用于处理多选情景(通常由条件语句实现)的条件逻辑,动态绑定还能够降低程序的复杂度。如图:

    动态绑定
    id atom = [[Hydrogen alloc] initWithNeutrons:1];
    [atom logInfo];
    

    在执行这段代码时,运行时系统会确定变量atom的实际类型(通过动态绑定),然后使用消息选择器(logInfo)将该消息与接收器(atom对象)的实例方法对应起来。在本例中,变量atom的类型被设置为Hydrogen *,因此运行时系统会搜索Hydrogen类中的实例方法logInfo。如果找不到,那么它就会在Hydrogen类的父类中寻找相应的实例方法。运行时系统会一直在类层次结构中寻找该实例方法,直到找到为止。否则方法被转发甚至异常。

    1.7 动态方法决议

    使用动态方法决议能够以动态方式实现方法。使用Objective-C中的@dynamic指令,可以告知编译器与属性关联的方法会议动态方式实现。NSObject类中含有 resolveInstanceMethod:resolveClassMethod: 方法,它们能够以动态方式分别为指定的实例和类方法选择器提供实现代码。你可以重写这些方法,以动态方式实现实例/类方法。下面例子:

    #import <objc/runtime.h>
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSString *method = NSStringFromSelector(sel);
        NSLog(@"Selector name = %@",method);
        if ([method hasPrefix:@"absoluteValue"]) {
            class_addMethod([self class], sel, (IMP)absoluteValue, "@@:@");
            NSLog(@"Dynamically instance method %@ to class %@",method,[self class]);
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
        
    // 添加该方法
    id absoluteValue(id self, SEL _cmd, id value) {
        NSInteger intValue = [value integerValue];
        NSLog(@"Self = %@, value = %@",self,value);
        if (intValue < 0) {
            return [NSNumber numberWithInteger:(intValue * -1)];
        }
        else {
            return [NSNumber numberWithInteger:intValue+1];
        }
    }
        
    // 客户端调用
    Calculator *obj = [[Calculator alloc] init];
        
    SEL selector = NSSelectorFromString(@"absoluteValue:");
    id sum = [obj performSelector:selector withObject:[NSNumber numberWithInteger:40]];
    NSLog(@"sum = %@",sum);
        
    // 结果 sum = 41
    

    这段代码以动态方式为新建方法创建了一个选择器,然后使用该选择器调用了一个实例方法。通过动态方法决议可以在运行时添加和调用这个方法,并获得返回值。

    1.8 内省

    Foundation框架中NSObject类的API含有非常多用于执行对象内省的方法。因为Objective-C的运行时行为与它的编译和链接时行为差异非常大,所以对象内省就成为了一种关键功能,使用它可以避免运行时错误。使用这些方法能够以动态方式在程序运行时查询下列信息:

    • 与方法有关的信息;
    • 测试对象的继承性、行为和一致性的信息。

    测试接收器是Calculator类的实例还是Calculator类的子类的实例

    BOOL isCalculator = [obj isKindOfClass:[Calculator class]];
    

    检查某个对象是否会对选择器做出回应

    BOOL responds = [obj respondsToSelector:@selector(sumAddend1::)];
    

    检查对象是否遵守指定协议

    BOOL conforms = [obj conformsToProtocol:@protocol(MyProtocol)];
    

    为选择器提取方法签名

    NSMethodSignature *signature = [obj methodSignatureForSelector:@selector(sumAddend1::)];
    

    运行时系统的结构

    Objective-C的运行时系统由两个主要部分构成:编译器和运行时系统库。

    2.1 编译器

    编译器会接收Objective-C源文件并进行处理(该处理过程由词法分析、语法分析、代码生成和优化、汇编以及链接操作等阶段构成),生成构成可执行程序的二进制文件。编译器的作用是接收输入的源代码,生成使用了运行时系统库的代码,从而得到合法的、可执行的Objective-C程序。

    2.2 运行时系统库

    就像C语言标准函数库会为C语言程序提供标准API和实现代码一样,运行时系统库也会为Objective-C的面向对象特性提供标准API和实现代码。这种库与所有Objective-C程序链接。

    运行时系统由下列部分组成:

    1. 类元素(接口、实现代码、协议、分类、属性、实例变量)
    2. 类实例(对象)
    3. 对象消息传递(包括动态类型和动态绑定)
    4. 动态方法决议
    5. 动态加载
    6. 对象内省

    运行时系统的公用API是在头文件runtime.h中声明的。可以从 http://opensource.apple.com获取苹果公司提供的运行时系统库。随着Objective-C语言的不断发展,将它与各种设计元素与系统服务一同使用,可以获得更好的性能和可扩展性。

    使用运行时系统库API创建类

    #import <objc/runtime.h>
    #import <objc/message.h>
        
    NSString *greeting2(id self, SEL _cmd) {
        return [NSString stringWithFormat:@"Hello, World!"];
    }
        
    // 以动态的方式创建一个类
    Class dynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);
    // 以动态方法添加一个方法,使用已有的方法 (description)获取特性
    Method description = class_getInstanceMethod([NSObject class], @selector(description));
    const char *type = method_getTypeEncoding(description);
    class_addMethod(dynaClass, @selector(greeting2), (IMP)greeting2, type);
        
    // 注册这个类
    objc_registerClassPair(dynaClass);
        
    // 使用该类创建一个实例并向其发送一条消息
    id dynaObj = [[dynaClass alloc] init];
        
    SEL selector = NSSelectorFromString(@"greeting2");
    NSString *rs = [dynaObj performSelector:selector];
    NSLog(@"rs = %@",rs);
    
    // 结果为: rs = Hello, World!
    

    首先导入头文件才能将运行时系统库中的消息传递API。它创建了一个类对(类及其元类),向这个类对中添加了一个指向先前创建的greeting2()函数的方法,然后在运行时注册该类对,从而使你编写的程序能够创建这个类的实例。方法签名是通过使用拥有相同签名的方法(NSObject类的description方法)获取的。再然后创建了一个类实例并向其发送了一条消息,然后将获得的结果显示到输出窗格中。

    运行时系统库含有可用于访问下列信息的函数(括号中为函数名)
    
    • 对象的类定义(objc_getClass)
    • 类的父类(class_getSuperClass)
    • 对象的元类定义(objc_getMetaClass)
    • 类的名称(class_getName)
    • 类的版本信息(class_getVersion)
    • 以字节为单位的类尺寸(class_getInstanceSize)
    • 类的实例变量列表(class_copyIvarList)
    • 类的方法列表(class_copyMethodList)
    • 类的协议列表(class_copyProtocolList)
    • 类的属性列表(class_copyPropertyList)

    总而言之,运行时系统库数据类型和函数为运行时系统库提供了实现各种Objective-C特性(如对象消息传递)所必需的数据类型和函数。当程序向对象发送消息时,运行时系统会通过自定义代码中的类方法缓存和虚函数表,查找类的实例方法。为了找到相应的方法,运行时系统会搜索整个类层次结构,找到该方法后,它就会执行该方法的实现代码。

    运行时系统中的消息传递操作

    运行时系统库中的方法数据类型

    struct objc_method
    {
        SEL method_name;
        char *method_types;
        IMP method_imp;
    };
    typedef objc_method Method;
    

    运行时系统库的方法查询逻辑

    运行时系统库的方法查询逻辑

    使用运行时系统API

    TODO

    相关文章

      网友评论

        本文标题:运行时编程

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