美文网首页
Objective-C语言的动态性总结(编译时与运行时)

Objective-C语言的动态性总结(编译时与运行时)

作者: 周英俊a | 来源:发表于2019-03-05 14:46 被阅读0次

    编译时与运行时

    编译时:即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。

    运行时: 即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。

    可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来,可读性也好;而运行时则是动态的阶段,开始具体与运行环境结合起来。

    OC语言的动态性

    OC语言的动态性主要体现在三个方面:动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。

    动态类型

    动态类型指的是对象指针类型的动态性,具体是指使用id任意类型将对象的类型确定推迟到运行时,由赋给它的对象类型决定对象指针的类型。另外类型确定推迟到运行时之后,可以通过NSObject的isKindOfClass方法动态判断对象最后的类型(动态类型识别)。也就是说id修饰的对象为动态类型对象,其他在编译器指明类型的为静态类型对象,通常如果不需要涉及到多态的话还是要尽量使用静态类型(原因上面已经说到:错误可以在编译器提前查出,可读性好)。

    示例:

    对于语句NSString* testObject = [[NSData alloc] init]; testObject在编译时和运行时分别是什么类型的对象?

    首先testObject是一个指向某个对象的指针,不论何时指针的空间大小是固定的。

    编译时: 指针的类型为NSString,即编译时会被当成一个NSString实例来处理,编译器在类型检查的时候如果发现类型不匹配则会给出黄色警告,该语句给指针赋值用的是一个NSData对象,则编译时编译器则会给出类型不匹配警告。但编译时如果testObject调用NSString的方法编译器会认为是正确的,既不会警告也不会报错。

    运行时: 运行时指针指向的实际是一个NSData对象,因此如果指针调用了NSString的方法,虽然编译时通过了,但运行时会崩溃,因为NSData对象没有该方法;另外,虽然运行时指针实际指向的是NSData,但编译时编译器并不知道(前面说了编译器会把指针当成NSString对象处理),因此如果试图用这个指针调用NSData的方法会直接编译不通过,给出红色报错,程序也运行不起来。

    下面给出测试例子:

        // 1.编译时编译器认为testObject是一个NSString对象,这里赋给它一个NSData对象编译器给出黄色类型错误警告,但运行时却是指向一个NSData对象
        NSString* testObject = [[NSData alloc] init];
        // 2.编译器认为testObject是NSString对象,所以允许其调用NSString的方法,这里编译通过无警告和错误
        [testObject stringByAppendingString:@"string"];
        // 3.但不允许其调用NSData的方法,下面这里编译不通过给出红色报错
        [testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
    

    将上面第三句编译不通过的注释掉,然后在第二句打断点,编译后让程序跑起来到断点出会看到testObject指针的类型是_NSZeroData,指向一个NSData对象。继续运行程序会崩溃,因为NSData对象没有NSString的stringByAppendingString这个方法。

    image

    那么,假设testObject是id类型会怎样呢?

        // 1.id任意类型,编译器就不会把testObject在当成NSString对象了
        id testObject = [[NSData alloc] init];
        // 2.调用NSData的方法编译通过
        [testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
        // 3.调用NSString的方法编译也通过
        [testObject stringByAppendingString:@"string"];
    

    结果是编译完全通过,编译时编译器把testObject指针当成任意类型,运行时才确定testObject为NSData对象(断点看指针的类型和上面的例子中结果一样还是_NSZeroData,指向一个NSData对象),因此执行NSData的函数正常,但执行NSString的方法时还是崩溃了。通过这个例子也可以很清楚的知道id类型的作用了,将类型的确定延迟到了运行时,体现了OC语言的一种动态性:动态类型。

    动态类型识别方法(面向对象语言的内省Introspection特性)

    1.首先是Class类型:

    • Class class = [NSObject class]; // 通过类名得到对应的Class动态类型
    • Class class = [obj class]; // 通过实例对象得到对应的Class动态类型
    • if([obj1 class] == [obj2 class]) // 判断是不是相同类型的实例

    2.Class动态类型和类名字符串的相互转换:

    • NSClassFromString(@”NSObject”); // 由类名字符串得到Class动态类型
    • NSStringFromClass([NSObject class]); // 由类名的动态类型得到类名字符串
    • NSStringFromClass([obj class]); // 由对象的动态类型得到类名字符串

    3.判断对象是否属于某种动态类型:

    • -(BOOL)isKindOfClass:class // 判断某个对象是否是动态类型class的实例或其子类的实例
    • -(BOOL)isMemberOfClass:class // 与isKindOfClass不同的是,这里只判断某个对象是否是class类型的实例,不放宽到其子类

    4.判断类中是否有对应的方法:

    • -(BOOL)respondsTosSelector:(SEL)selector // 类中是否有这个类方法
    • -(BOOL)instancesRespondToSelector:(SEL)selector // 类中是否有这个实例方法

    区别:
    上面两个方法都可以通过类名调用,前者判断类中是否有对应的类方法(通过‘+’修饰定义的方法),后者判断类中是否有对应的实例方法(通过‘-’修饰定义的方法)。此外,前者respondsTosSelector函数还可以被类的实例对象调用,效果等同于直接用类名调用后者instancesRespondToSelector函数。
    举个例子:假设有一个类Test,有它的一个实例对象test,Test类中定义了一个类函数:+ (void)classFun;和一个实例函数:- (void)objFunc;,那么各种调用情况的结果如下:

      [1][Test instancesRespondToSelector:@selector(objFunc)];//YES
      [2][Test instancesRespondToSelector:@selector(classFunc)];//NO
    
      [3][Test respondsToSelector:@selector(objFunc)];//NO
      [4][Test respondsToSelector:@selector(classFunc)];//YES
    
      [5][test respondsToSelector:@selector(objFunc)];//YES
      [6][test respondsToSelector:@selector(classFunc)];//NO
    

    结论: 如果想判断一个类中是否有某个类方法,应该使用[4]; 如果想判断一个类中是否有某个实例方法,可以使用[1]或者[5]。

    5.方法名字符串和SEL类型的转换

    在编译器,编译器会根据方法的名字和参数序列生成唯一标识改方法的ID,这个ID为SEL类型。到了运行时编译器通过SEL类型的ID来查找对应的方法,方法的名字和参数序列相同,那么它们的ID就都是相同的。另外,可以通过@select()指示符获得方法的ID。常用的方法如下:

    SEL funcID = @select(func);// 这个注册事件回调时常用,将方法转成SEL类型
    SEL funcID = NSSelectorFromString(@"func"); // 根据方法名得到方法标识
    NSString *funcName = NSStringFromSelector(funcID); // 根据SEL类型得到方法名字符串
    

    动态绑定

    动态绑定指的是方法确定的动态性,具体指的是利用OC的消息传递机制将要执行的方法的确定推迟到运行时,可以动态添加方法。也就是说,一个OC对象是否调用某个方法不是由编译器决定的,而是由运行时决定的;另外关于动态绑定的关键一点是基于消息传递机制的消息转发机制,主要处理应对一些接受者无法处理的消息,此时有机会将消息转发给其他接收者处理,具体见下面介绍。

    动态绑定是基于动态类型的,在运行时对象的类型确定后,那么对象的属性和方法也就确定了(包括类中原来的属性和方法以及运行时动态新加入的属性和方法),这也就是所谓的动态绑定了。动态绑定的核心就该是在运行时动态的为类添加属性和方法,以及方法的最后处理或转发,主要用到C语言语法,因为涉及到运行时,因此要引入运行时头文件:#include <objc/runtime.h>

    消息传递机制:

    在OC中,方法的调用不再理解为对象调用其方法,而是要理解成对象接收消息,消息的发送采用‘动态绑定’机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。方法的调用实际就是告诉对象要干什么,给对象(的指针)传送一个消息,对象为接收者(receiver),调用的方法及其参数即消息(message),给一个对象传消息表达为:[receiver message]; 接受者的类型可以通过动态类型识别于运行时确定。

    在消息传递机制中,当开发者编写[receiver message];语句发送消息后,编译器都会将其转换成对应的一条objc_msgSend C语言消息发送原语,具体格式为:
    void objc_msgSend (id self, SEL cmd, ...)

    这个原语函数参数可变,第一个参数填入消息的接受者,第二个参数是消息‘选择子’,后面跟着可选的消息的参数。有了这些参数,objc_msgSend就可以通过接受者的的isa指针,到其类对象中的方法列表中以选择子的名称为‘键’寻找对应的方法,找到则转到其实现代码执行,找不到则继续根据继承关系从父类中寻找,如果到了根类还是无法找到对应的方法,说明该接受者对象无法响应该消息,则会触发‘消息转发机制’,给开发者最后一次挽救程序崩溃的机会。

    消息转发机制:

    如果消息传递过程中,接受者无法响应收到的消息,则会触发进入‘消息转发’机制。

    消息转发依次提供了三道防线,任何一个起作用都可以挽救此次消息转发。按照先后顺序三道防线依次为:

    • 动态补加方法的实现
      +(BOOL)resolveInstanceMethod:(SEL)sel
      +(BOOL)resolveClassMethod:(SEL)sel

    • 直接返回消息转发到的对象(将消息发送给另一个对象去处理)
      -(id)forwardingTargetForSelector:(SEL)aSelector

    • 手动生成方法签名并转发给另一个对象
      -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
      -(void)forwardInvocation:(NSInvocation *)anInvocation;

    示例

    这里以一个简单的例子展示消息转发的完整个过程。定义一个Test类,类头文件声明一个名为instanceMethod的实例方法但不提供方法实现(消息转发主要就针对实例方法,类方法由于无法在运行时动态添加实现等事实并不能转发给其他类):

    /* Test.h */
    @interface Test : NSObject
    /* 只声明一个实例方法而不在.m文件中实现 */
    - (void)instanceMethod;
    @end
    

    然后在main函数中实例化Test对象并调用该实例方法,由于方法没有实现,因此在运行时一定会触发消息转发机制:

    /* main.m */
    #import "Test.h"
    #import <objc/runtime.h>
    
    int main(int argc, const char * argv[]) {
        Test *test = [[Test alloc] init];
        [test instanceMethod];
        return 0;
    }
    

    先进入消息转发的第一道防线,我们在Test类的.m文件中提供运行时的转发接应,实现resolveInstanceMethod方法为指定的instanceMethod消息补加对应方法的实现完成补救:

    /* Test.m */
    #import <objc/runtime.h>
    /*
     * 被动态添加的实例方法实现
     */
    void instanceMethod(id self, SEL _cmd) {
        NSLog(@"收到消息后会执行此处的函数实现...");
    }
    
    /*
     * 动态补加方法实现
     */
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(instanceMethod)) {
            class_addMethod(self, sel, (IMP)instanceMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    

    如果没有实现resolveInstanceMethod方法进行补救或者直接返回了NO,则进入第二道防线,这里我们要实现forwardingTargetForSelector函数返回另一个实例对象,让该对象代替原对象去处理这个消息。
    假设我们让一个叫做Test2的类对象去处理这个消息,Test2类中要有同名的方法和方法的实现,这样就会执行Test2中的同名方法完成消息转发:

    /* Test2.h */
    @interface Test2 : NSObject
    - (void)instanceMethod;
    @end
    
    /* Test2.m */
    @implementation Test2
    - (void)instanceMethod {
        NSLog(@"消息转发到这...");
    }
    @end
    
    /* Test.m */
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        /* 返回转发的对象实例 */
        if (aSelector == @selector(instanceMethod)) {
            return [[Test2 alloc] init];
        }
        return nil;
    }
    

    如果没有实现上面的两个补救方法或者forwardingTargetForSelector方法直接返回了nil,则进入最后一道防线,此时我们要手动生成方法签名并实现forwardInvocation方法将消息转发给另一个对象,同第二道防线类似:

    /* Test.m */
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        /* 为指定的方法手动生成签名 */
        NSString *selName = NSStringFromSelector(aSelector);
        if ([selName isEqualToString:@"instanceMethod"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        /* 如果另一个对象可以相应该消息,则将消息转发给他 */
        SEL sel = [anInvocation selector];
        Test2 *test2 = [[Test2 alloc] init];
        if ([test2 respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:test2];
        }
    }
    

    动态加载

    动态加载主要包括两个方面,一个是动态资源加载,一个是一些可执行代码模块的加载,这些资源在运行时根据需要动态的选择性的加入到程序中,是一种代码和资源的‘懒加载’模式,可以降低内存需求,提高整个程序的性能,另外也大大提高了可扩展性。

    例如:资源动态加载中的图片资源的屏幕适配,同一个图片对象可能需要准备几种不同分辨率的图片资源,程序会根据当前的机型动态选择加载对应分辨率的图片,像iphone4之前老机型使用的是@1x的原始图片,而retina显示屏出现之后每个像素点被分成了四个像素,因此同样尺寸的屏幕需要4倍分辨率(宽高各两倍)的@2x图片,最新的针对iphone6/6+以上的机型则需要@3x分辨率的图片。例如下面所示应用的AppIcon,需要根据机型以及机型分辨率动态的选择加载某张具体的图片资源:

    image

    相关文章

      网友评论

          本文标题:Objective-C语言的动态性总结(编译时与运行时)

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