ios-Runtime

首先我们先看看文档如何描述Runtime的,如下:

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

大概的意思就是:OC语言尽可能的动态地处理事情,将决定尽可能地从编译时链接时推迟到运行时。意味着,OC语言不仅仅需要一个编译器,还需要一个运行时系统执行已经编译好的代码。

下面将从两个方面学习Runtime:

  1. OC 运行时系统是如何工作的?
  2. 如何使用运行时系统?

补充:
其实发展到现在,Runtime技术已经发展了两个版本,一个OC2.0以后的modern版本,和之前legacy版本。
这两个版本的主要区别是:

  1. 类中实例变量的排序改变后,是否需要重新编译该类的子类,在legacy版本是必须的,而modern则不必须。
  2. modern版本支持@property属性

支持的平台:
modern:iPhone系统 && 64-bit OS X v10.5后的系统
legacy:32bit OS X 系统

与运行时系统交互的的方式( 详细的交互在后面章节再说明 )

分为三个不同的层次:

  1. Objective-C 源码
  2. NSObject 的方法
  3. 直接调用 runtime 函数

Objective-C 源码

当编译器编译OC的类和方法时,会生成数据结构函数调用实现语言的动态特性。

  • 数据结构在哪里获取到信息?

    1. 类和分类定义 / 协议声明
    2. 类对象和协议对象
    3. 方法选择器( method selector )
    4. 实例变量模板
    5. 源码提取到的其它信息
  • 函数调用

    1. 最主要的运行时函数就是发送消息,由源码的消息表达式调用。例如:[dog run]这个表达式会调用运行时的发送消息函数

NSObject 的方法

其中一些方法可以简单查询运行时系统获取信息。这些方法可以让对象实现自省

+(Class)class // 获得类对象
-(BOOL)isKindOfClass:(Class)aClass // 是否是指定类或者其子类
-(BOOL)isMemberOfClass:(Class)aClass // 是否是指定类
-(BOOL)respondsToSelector:(SEL)aSelector // 是否实现指定方法或者其父类实现
+(BOOL)conformsToProtocol:(Protocol *)aProtocol // 类是否实现了指定协议
-(IMP)methodForSelector:(SEL)aSelector // 定位和返回接收者方法实现的地址,所以可以像函数调用一样

Cocoa中大多数的类都继承于NSObject( 特例:NSProxy ),所以大多数类都拥有上述自省方法。在NSObject中,大多数方法只是简单地实现,例如:

+(NSString *)description // 默认返回类名和地址

详细实现功能还需自己重写。

直接调用 runtime 函数

运行时系统是一个动态共享库,/usr/include/objc 目录下提供了一套函数数据结构的公共接口。

补充:

  • 什么是selector?
    一个对象选择一个方法执行的名称,又或者是源码编译后,取代这个名称的唯一标识

  • selector的作用:
    在OC源码阶段,selector一般当前是方法的唯一标识。而在运行时,selector就作为一个动态函数指针,根据给的方法名指向相对应类中的方法的具体实现

  • 获得一个selector:
    编译好的类型是:SEL,有两种途径可以获得

    1. 编译时,使用@selector指令: 

      SEL aSelector = @selector(methodName);

    2. 运行时,使用NSSelectorFromString函数 

      SEL aSelector = NSSelectorFromString(@"methodName");

  • 调用selector:
    使用performSelector:方法

    SEL aSelector = @selector(run); // 运行时会自动指向 对应类 的方法实现
    [aDog performSelector:aSelector];
    [anAthlete performSelector:aSelector];


    消息机制

    在Objective-C中,最重要的就是消息发送机制。本文从两个方面深入学习该机制:

    1. 消息表达式如何转换为调用objc_msgSend函数 && 如何通过方法名发送消息?
    2. 如何利用objc_msgSend函数 && 必要时如何避免动态绑定?

    objc_msgSend

    在Objective-C语言里,消息直到运行时才会绑定到具体的方法实现。编译器把消息表达式

    [receiver message]

    转化为调用消息发送函数objc_msgSend函数。该函数有两个主要的参数:

    • receiver:消息接收者
    • selector:方法选择器

    objc_msgSend(receiver, selector)

    如果消息表达式有参数,则转化如下:

    objc_msgSend(receiver, selector, arg1, arg2, ...)

    动态绑定就是在消息发送函数objc_msgSend里实现的

    1. 根据receiver的类找到selector指向的方法的实现
    2. 调用找到的方法实现,把消息接收对象( 实际上传递的是对象数据的指针 )和方法的相关参数传递进去
    3. 将执行方法实现后的返回值返回出去

    其实当一个对象被创建、分配好内存、初始化实例变量时,第一个被创建的变量就是指向类结构体的指针( isa 指针 )

    note:一个对象与在运行时系统上工作,isa 指针是必须有的。定义一个对象的结构体,必须与struct objc_object { Class isa; }相符合( 在objc/objc.h中定义 )。但是我们很少去定义这个结构体,因为NSObjectNSProxy中的 alloc 和 allocWithZone: 方法会调用 class_createInstance 生成 objc_object

    如下整个消息发送的流程框架:实现了上面(1.)步骤

    深入研究Runtime(2) - 发送消息Messaging - 简书

    Messaging.png

    • 在上面内容中已经知道,调用发送消息函数时,已经获得对象数据的指针。通过该指针,我们可以找到对象内存地址,从而找到isa指针。由上图可知,获得isa指针后,我们可以遍历相关的类对象。每个类对象中都包含了一个方法列表,配合已知receiver的类,则可以定位到要调用的方法( 当前类对象方法列表没有,则寻找父类的,直到找到或者到NSObject为止 )。

    • 上述整个过程就是动态绑定的过程( OOP编程 )

    补充:

    • 对象方法存储在类对象的方法列表当中,而类方法存储在元类( metaclass )对象的方法列表当中
    • 其实为了加快整个消息发送的过程,运行时系统缓存了我们曾经使用的方法名和方法地址( 如方法列表中的 selector ... address )。每个类的缓存是分开的,包括重写了的父类方法。在查找方法列表之前,会先查找缓存。如果方法已经缓存,发送消息只是比直接调用函数慢一点。

    使用隐藏参数

    由上面内容可以,objc_msgSend函数实现中,当找到方法的实现后,会执行该实现,并传入相关的对象和参数。而相关对象就是:

    1. 消息接收的对象 ( self )
    2. 方法的动态指针 ( _cmd )

    note:为什么叫隐藏参数?因为在方法声明实现时,在参数列表中,我们没有明显的写出来

    如下( strange方法动态绑定大概实现猜测 ):

      - strange {
      id target = getTheReceiver();
      SEL method = getTheMethod();
      if ( target == self || method == _cmd )
        return nil; 
        return [target performSelector:method];  // 根据方法名调用方法
     }

    获取方法的地址

    想要避免动态绑定,唯一方法就是获得获得方法地址,然后想函数一样调用它。一般这种情况很少用,例如想大量重复调用方法,而你又减少动态绑定的资源开销。

    • 使用 NSObject 的 methodForSelector: 方法可以根据方法名获得方法的地址

    例子: setFilled: (BOOL)重复调用

    // - (IMP) methodForSelector:(SEL)aSelector 返回的是类型是IMP
    // id (*IMP)(id, SEL, ...):方法实现对应的C语言函数的函数指针
    
    void (*setter)(id, SEL, BOOL);     // 函数指针定义必须与方法实现的参数一一对应
    int i;
    setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
    for ( i = 0 ; i < 1000 ; i++ )
          setter(targetList[i], @selector(setFilled:), YES);
    • 定义IMP函数指针时,隐藏参数不能省略,方法参数需必须传入。 
      • id:消息接收对象
      • SEL:方法选择器
      • BOOL:setFilled:的参数

    其实如果不是大量的循环发送消息,这种资源的浪费是很少的,所以没必要去主动去避免动态绑定。

    本文介绍runtime的两个进阶用法:

    1. 动态方法解析:如何动态地提供方法的实现
    2. 消息转发:发送消息时,如果消息接收者没有实现该方法,则运行时会报错。当消息接收者没有实现该方法时,应该如何实现将消息转发给另一个接收者?

    动态方法解析

    有一些情况,可能是需要动态提供方法的实现的,如下:

    1. 编译器指令@dynamic:在 xcode4.5 之前,@property 只是声明了 setter/getter 方法,如果不使用@synthesize,则必须自己提供 setter/getter 或者 getter 方法。另外一个就是@dynamic,但是需动态添加 getter/setter 方法的实现。

    如何给指定方法动态添加实现?

    • 在 resolveInstanceMethod: 和 resolveClassMethod: 中根据给定的方法名,使用class_addMethod( )把函数作为一个方法添加到类里。

      void dynamicMethodIMP(id self, SEL _cmd) {  // 若方法有参数,则需在后面添加相应参数
        // implementation ... 方法的实现
      }
    • BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

      • cls:需要添加方法的类
      • name:方法名
      • imp:方法实现对应的函数,必须有 self 和 _cmd 两个参数
      • types:C语言字符串,描述实现函数对应的参数类型( 包括返回值,第1个为描述返回值,因为2、3分别是id self,SEL _cmd,所以必须为"@:" )
      @implementation MyClass
      + (BOOL)resolveInstanceMethod:(SEL)aSEL
      {
          if (aSEL == @selector(resolveThisMethodDynamically)) {  // 是否为resolveThisMethodDynamically方法
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");  // 动态添加方法
          return YES;  // 返回YES,则不会转发
      }
          return [super resolveInstanceMethod:aSEL]; // 不是需要动态添加的方法,按照父类处理
      }
      @end

    动态添加方法实现与消息的转发是不可以同时存在的,动态添加优先于消息转发。如果实现了resolveInstanceMethod: 和 resolveClassMethod:,并且返回YES,则不会进行消息的转发。如果没有实现,则转发消息。

    补充:动态加载

    Objective-C的程序允许在运行时加载和链接新的类与分类,此时加载与开始时加载无异。
    动态加载类与分类一个比较重要的应用就是加载动态库

    • 使用方法: 
      1. 使用运行时函数objc_loadModules( 在objc/objc-load.h头文件中定义 ),动态加载 Mach-O文件里的Objective-C的功能模块(动态库)
      2. NSBundle类中提供了简便的接口进行动态加载 Mach-O文件

    Mach-O文件:是Mac和iOS系统的可执行二进制文件,类似于window中的.exe文件

    消息转发

    当给对象发送消息时,如果该对象没有实现相应的方法处理该消息,则会调用 -forwardInvocation: 方法。NSObject类中实现了该方法,默认调用 -doesNotRecognizeSelector:方法,所以如果继承于NSObject的类接收没有实现的消息,则会报运行时错误
    所以,在-forwardInvocation: 方法中实现消息转发即可。

    • 转发一条消息,在-forwardInvocation: 方法中需要做的事情:

      • 决定把消息转发给谁
      • 传递原始的参数

        -forwardInvocation: 方法中,会传递给该方法唯一一个参数,NSInvocation的对象( 包含了原始的消息和参数 )

        - (void)forwardInvocation:(NSInvocation *)anInvocation
        {
            if ([someOtherObject respondsToSelector:[anInvocation selector]])   // 如果指定对象实现了该方法
            [anInvocation invokeWithTarget:someOtherObject];  // 给指定对象发送消息
            else
            [super forwardInvocation:anInvocation];  // 按照父类处理
        }

    转发与多重继承

    如下图所示:

    转发.png

    Warrior类没有实现negotiate方法。当接收negotiate消息时,会调用Diplomat的对象方法,并且会传递原始参数。所以,这个过程类似于继承调用方法的过程。而Warrior本来就有父类,所以这种现象相当于使用转发机制 实现了多重继承。

    转发与继承的对比:
    继承是把所有父类的能力都集中到一个类中,而转发只是仅仅调用了一下方法,把问题分离出去。在一定程度上还是有很大区别的。

    转发与继承

    转发是模仿继承的。但是NSObject永远不会混淆这两个概念。就好像方法-respondsToSelector:-isKindOfClass:只会在继承层次中有效,而在转发链中是无效的,永远只会返回NO

    但是有些情况下,你也想转发链中返回的是YES。那样就必须重写-respondsToSelector:-isKindOfClass:方法。

    如下重写-respondsToSelector:方法:

    - (BOOL)respondsToSelector:(SEL)aSelector
    {
        if ( [super respondsToSelector:aSelector] )  // 如果父类实现了,则返回YES
        return YES;
        else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
                }
    
         return NO;
    }

    当你如果想将转发完全模仿了继承,重写-respondsToSelector:-isKindOfClass:方法是远远不够的。

    以下是大概上还需要重写的方法:

    • instancesRespondToSelector:
    • 如果使用了协议,则需重写conformsToProtocol:
    • 重写methodSignatureForSelector:方法,返回转发后的方法描述 
      - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
      {
          NSMethodSignature* signature = [super methodSignatureForSelector:selector];  // 如果父类实现了该方法,才会有方法的描述,返回父类的即可
          if (!signature) {  // 若没有,则返回转发对象的
               signature = [surrogate methodSignatureForSelector:selector];
          }
          return signature;
      }

    note:这是一个高级别的技术点,当真的没有其它解决方案时才会考虑使用转发机制。千万不要用来替代继承,先考虑使用继承。如果真的需要使用,要保证自己对实行转发接收转发这两个类的所有行为有一个完全的理解。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值