• runtime


    Objective-c是一门动态语言,动态两个字主要就体现在我们调用方法的时候,运行时回动态的查找方法,然后调用相应的函数地址。运行时是整个Objective-c程序的基石,有了它我们的程序才能正常运行起来。

      NSObject是Cocoa中绝大部分类的基类,它主要是提供了序列话,拷贝对象,以及支持运行时动态识别的框架。

      在Objective-c中每一个类对象最开始的位置都会有一个isa指针,该指针指向一块内存区域,该部分主要包含两部分信息:

      1、指向父类的指针。

      2、自身的方法分发表。

      有了这两部分,Objective-c的方法的调用流程就可以跑起来了。当我们调用一个对象的某一个方法的时候,首先会在当前类的分发表中寻找该方法,如果找不到对应的方法,然后再去其父类中寻找该方法,依次类推直到找到对应的方法为止,流程图如下:

      你可能会想到,如果一个类有很深的继承层次,每次去调用根类的某个函数,岂不是都要做很多次查找。理论上是这个样子的,不过runtime也并非那么傻,它会为每一个类(不是对象)维护一个经常调用的方法的列表,只要调用过就会缓存起来(官方没有明确说明缓存机制),这样当程序运行稳定以后整个方法调用的过程就会更加高效。

      通过学习官方文档Objective-C Runtime Programming Guide,可以发现其实所有的selector调用最后都会转化为C类型的函数调用。举个例子我们创建了一个A类型的对象aSample,然后调用其test方法([aSample test]),编译的时候,编译器就会将该调用转化为objc_send(aSample, selector)的形式,runtime会调用test方法实现所对应的函数地址。该函数的参数包含了两个隐含的参数self以及_cmd,其中self指向调用该方法的对象,_cmd则代表要调用的方法。

      前面提到了NSObject提供了很多遍历的方法可以和运行时进行交互,其中有个方法methodForSelector,通过它我们可以直接获取到指定的方法对应的函数指针。通常我们直接使用Objective-c方式的方法调用就可以了,但有时程序中可能会频繁的调用某一个方法,为了提高效率。我们可以直接获取到方法对应的函数地址,然后直接调用该函数,这样就少了动态识别的时间。

      下面举个例子:

    复制代码
    // 父类中定义该方法
    - (void)testMethod
    {
        //NSLog(@"the implementation of BaseSample!!!");
        int a = 5 / 2.0f;
        a = ~a;
    }
    
    // 测试方法,分别使用两种方法调用1亿次
    - (void)test
    {
        void (*methodAddress)(id,SEL);
        methodAddress = (void(*)(id,SEL))[self methodForSelector:@selector(testMethod)];
        
        NSLog(@"Invoke with Method Address start!!!");
        for (int i = 0; i < 100000000; ++i) {
            methodAddress(self, @selector(testMethod));
        }
        NSLog(@"Invoke with Method Address finish!!!");
        
        NSLog(@"Invoke with direct selector start!!!");
        for (int i = 0; i < 100000000; ++i) {
            [self testMethod];
        }
        NSLog(@"Invoke with direct selector finish!!!");
    }
    复制代码

      运行结果如下图:

      可以看出调用时间:使用函数地址调用共花费0.151s,直接调用方法花费0.734s。时间是有一点儿差距,但是已经微乎其微了,这也从侧面说明了runtime的缓存机制还是很给力的。

      当我们调用某一个不存在的方法的时候,程序会crush,在命令行提示“unrecognized selector sent to instance 0xxxxxxx”,并抛出“NSInvalidArgumentException”的异常。当调用一个对象不能识别的方法时,runtime会一直沿着类的继承关系往基类方向寻找,直到NSObject类,如果还是识别不了该方法的话,再抛出异常之前runtime还给我们了最后一次“补救”的机会。它会先调用forwardInvocation方法,如果我们想把这个方法异常调用捕获并传递到其他地方的话,可以在类中重写该方法。NSObject对于forwardInvocation方法的默认实现是调用doesNotRecognizeSelector方法,而doesNotRecognizeSelector则是直接抛出异常。

      当调用forwardInvocation的时候会传入一个NSInvocation的参数,该参数标识了调用的方法的对象以及调用的方法,并对该方法的调用结果进行封装。我们重写forwardInvocation方法的时候,还必须同时重写methodSignatureForSelector方法,该方法返回表示一个方法的字符串,具体如何构建请看Type Encodings

      下面举一个简单的重写forwardInvocation的例子:

    复制代码
    #pragma mark-
    #pragma mark 重写 ForwardInvocation
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([self respondsToSelector:aSelector]) {
            return [super methodSignatureForSelector:aSelector];
        }
        else {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        NSLog(@"Hello unreconginized selector!");
    }
    
    // 在init中调用一个不存在的方法hello
    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {
            // Initialization code
            [self hello];
        }
        return self;
    }
    复制代码

      上面的例子,截获了不能识别的方法调用,创建了一个返回void类型的方法签名,当调用不能识别的方法的时候打印简单的日志。当然在程序中最好不要这么做,特别是开发的时候,大部分时候我们更希望能够尽早的发现这种调用错误。

  • 相关阅读:
    Flutter 步骤进度组件
    程序员到底要不要学习框架、库和工具
    Flutter 吐血整理组件继承关系图
    超过百万的StackOverflow Flutter 问题
    Flutter 实现网易云音乐字幕
    Flutter 实现虎牙/斗鱼 弹幕效果
    Flutter AbsorbPointer 与 IgnorePointer的区别
    《Flutter 小技巧》一行禁用App,一行置灰App,致敬
    Python 任务自动化工具:nox 的配置与 API
    Flask 作者 Armin Ronacher:我不觉得有异步压力
  • 原文地址:https://www.cnblogs.com/needly/p/3664277.html
Copyright © 2020-2023  润新知