运行时(runtime)

  • 什么是runtime(运行时)? runtime 是一套比较底层的纯C语言API,属于1个C语言库, 包含了很多底层的C语言API,功能强大。 在我们平时编写的OC代码中,程序运行过程时, 其实最终都是转成了 runtime 的C语言代码, runtime 算是 OC 的幕后工作者。

使用 clang 重写 OC 代码,可以看到我们写的 OC 代码都被转成了 runtime 代码

  • 新建一个 命令行项目
  • 新建 Person 类,添加 nameage 属性
  • 使用 clang 重写 OC 代码
    clang --help | grep objc
    #-rewrite-objc           Rewrite Objective-C source to C++
    clang main.m -rewrite-objc
    
  • 对比 OC 和 转成的 runtime 代码
    • OC 代码
      Person *p1 = [[Person alloc] init];
      
    • 对应 runtime 代码
      Person *p1 = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
      
    • 将类型转换代码去掉,精简一下
      Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
      Person *p1 = objc_msgSend(p1, sel_registerName("init"));
      
    • 总结: Person *p1 = [[Person alloc] init];
      1. 调用 Person 的类方法创建 Person 对象 p,
      2. 在调用 p 对象的 init 对象方法
      3. OC 调用方法其实都是给对象发消息,使用 objc_msgSend 发消息
  • 其实在 OC 代码中可以直接调用 runtime 方法

    int main(int argc, const char * argv[]) {
      @autoreleasepool {
    //        Person *p1 = [[Person alloc] init];
          Person *p = objc_msgSend(NSClassFromString(@"Person"), @selector(alloc));
          Person *p1 = objc_msgSend(p, @selector(init));
    //        p1.name = @"冰冰";
          objc_msgSend(p1, @selector(setName:), @"冰冰");
    
          NSString *name = objc_msgSend(p1, @selector(name));
          NSLog(@"name = %@", name);
      }
      return 0;
    }
    
  • 可以看到将:
    Person *p1 = [[Person alloc] init];
    
    换成
    Person *p = objc_msgSend(NSClassFromString(@"Person"), @selector(alloc));
    Person *p1 = objc_msgSend(p, @selector(init));
    
    p1.name = @"冰冰";
    
    换成
    objc_msgSend(p1, @selector(setName:), @"冰冰");
    
    NSLog(@"name = %@", p1.name);
    
    换成
    NSString *name = objc_msgSend(p1, @selector(name));
    NSLog(@"name = %@", name);
    
    程序依然能够运行
  • 有人会说这没什么了不起的.那么接下来我们将 Person.h 里面的 nameage 属性放到 Person.m@interface-extension 里面.这样的话因为没有在 Person.h 公开,那么外界不知道 Personnameage 属性,使用有使用 p1.name = @"冰冰"; 就完全行不通了,但是这个时候依然可以使用 objc_msgSend(p1, @selector(setName:), @"冰冰"); 来调用 PersonsetName: 方法
  • 也就是说以后想使用别人没有公开的方法可以直接使用 objc_msgSend

初识 runtime

  • iOS 开发中, runtime 可以用来干什么呢?,我们先来看看 runtime 的头文件, 使用 command + shift + o 查找 runtime.h 文件, runtime 相关的内容基本都定义在这个头文件中了

  • 我们在 OC 中使用的类实际上是一个结构体,在 runtime.h 定义如下:

      struct objc_class {
          Class isa  OBJC_ISA_AVAILABILITY;
    
      #if !__OBJC2__
          Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    
          const char *name                        OBJC2_UNAVAILABLE;  // 类名
          long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
          long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    
          long instance_size                      OBJC2_UNAVAILABLE;  // 类的实例变量大小
          struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 类的成员变量链表
    
          struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
          struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
    
          struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
      #endif
    
      } OBJC2_UNAVAILABLE;
    
  • 我们在 OC 对象中定义的实例变量如 _name 实际上也是一个结构体,在 runtime.h 定义如下:

      typedef struct objc_ivar *Ivar;
    
      struct objc_ivar {
          char *ivar_name                 OBJC2_UNAVAILABLE;  // 变量名
          char *ivar_type                 OBJC2_UNAVAILABLE;  // 变量类型
          int ivar_offset                 OBJC2_UNAVAILABLE;  // 基地址偏移字节
      #ifdef __LP64__
          int space                       OBJC2_UNAVAILABLE;
      #endif
      }
    
  • 我们在 OC 对象中定义的方法实际上对应三个结构体,在 runtime.h 定义如下:

    1. SEL 实际是 objc_selector 但是在 runtime.h 找不到, 仅在 objc.h 定义如下

      typedef struct objc_selector *SEL;
      
      • SEL 又叫选择器,是表示一个方法的 selector 的指针,映射方法的名字。Objective-C 在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是 SEL
      • SEL 的作用是作为 IMPKEY,存储在 NSSet 中,便于 hash 快速查询方法。
      • Objective-C 同一个类(及类的继承体系)中 SEL 不能相同。 所以同一个类(及类的继承体系)中,不能存在2个同名的方法,就算参数类型不同也不行。不同的类可以有相同的方法名,多个方法可以有同一个 SEL, 不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP

        • SEL 使用

          1. 定义 Person 类, 并定义 sayName 方法

             @implementation Person
            
             - (void)sayName {
                 NSLog(@"sayName = %@", self);
             }
            
             @end
            
          2. 定义 Animal 类, 并定义 sayName 方法

              @implementation Animal
            
              - (void)sayName {
                  NSLog(@"sayName = %@", self);
              }
            
              @end
            
          3. main 方法中使用 sayName 创建一个 SEL, 不同类的对象调用同一个 SEL

              int main(int argc, const char * argv[]) {
                  @autoreleasepool {
                      // 创建 SEL
                      SEL sel = NSSelectorFromString(@"sayName");
            
                      Person *p1 = [[Person alloc] init];
                      Animal* a1 = [[Animal alloc] init];
            
                      // 不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP
                      [p1 performSelector:sel];   // 输出 sayName = <Person: 0x100206ee0>
                      [a1 performSelector:sel];   // 输出 sayName = <Animal: 0x100106fa0>
                  }
                  return 0;
              }
            
    2. IMP 实际是 一个函数指针 但是在 runtime.h 找不到, 仅在 objc.h 定义如下

      typedef id (*IMP)(id, SEL, ...);
      
      • IMP 是指向实现函数的指针,通过 SEL 取得 IMP后,我们就获得了最终要找的实现函数的入口
      • IMP 使用

          int main(int argc, const char * argv[]) {
              @autoreleasepool {
                  Person *p1 = [[Person alloc] init];
                  Animal* a1 = [[Animal alloc] init];
        
                  // 创建 SEL
                  SEL sel = NSSelectorFromString(@"sayName");
        
                  // 根据 sel 去不同类的对象里面找对应的IMP
                  IMP imp1 = [p1 methodForSelector:sel];
                  IMP imp2 = [a1 methodForSelector:sel];
        
                  // 每个对象执行对应的imp
                  imp1(p1,sel);
                  imp2(a1,sel);
              }
              return 0;
          }
        
      • 由于每个方法对应唯一的 SEL,因此我们可以通过 SEL 方便快速准确地获得它所对应的 IMP,查找过程将在下面讨论。取得 IMP 后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了。通过取得 IMP,我们可以跳过 Runtime 的消息传递机制,直接执行 IMP 指向的函数实现,这样省去了 Runtime 消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
      • 如果类似于for循环这种情况下频繁调用同一方法,可以直接调用 IMP 以提高性能

          int main(int argc, const char * argv[]) {
              @autoreleasepool {
                  Person *p1 = [[Person alloc] init];
                  Animal* a1 = [[Animal alloc] init];
        
                  // 创建 SEL
                  SEL sel = NSSelectorFromString(@"sayName");
        
                  // 根据 sel 去不同类的对象里面找对应的IMP
                  IMP imp1 = [p1 methodForSelector:sel];
                  IMP imp2 = [a1 methodForSelector:sel];
        
                  // 每个对象执行对应的imp
                  imp1(p1,sel);
        
                  // 调用同一个方法1000次
                  for (int i = 0; i < 1000; i++) {
                      imp2(a1,sel);
                  }
              }
              return 0;
          }
        
    3. Method 对应 objc_method 结构体,在 runtime.h 定义如下:

      typedef struct objc_method *Method;
      
      struct objc_method {
        SEL method_name                     OBJC2_UNAVAILABLE;  // 方法名
        char *method_types                  OBJC2_UNAVAILABLE; // 参数类型
        IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法实现
      }
      // {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}
      
      1. 我们可以看到该结构体中包含一个 SELIMP,实际上相当于在 SELIMP 之间作了一个映射。有了 SEL,我们便可以找到对应的 IMP,从而调用方法的实现代码。
      2. runtime 提供了一系列的方法来处理与方法相关的操作

          // 调用指定方法的实现
          id method_invoke (id receiver, Method m, ...);
        
          // 调用返回一个数据结构的方法的实现
          void method_invoke_stret (id receiver, Method m, ...);
        
          // 获取方法名
          SEL method_getName (Method m);
        
          // 返回方法的实现
          IMP method_getImplementation (Method m);
        
          // 获取描述方法参数和返回值类型的字符串
          const char * method_getTypeEncoding (Method m);
        
          // 获取方法的返回值类型的字符串
          char * method_copyReturnType (Method m);
        
          // 获取方法的指定位置参数的类型字符串
          char * method_copyArgumentType (Method m, unsigned int index);
        
          // 通过引用返回方法的返回值类型字符串
          void method_getReturnType (Method m, char *dst, size_t dst_len);
        
          // 返回方法的参数的个数
          unsigned int method_getNumberOfArguments (Method m);
        
          // 通过引用返回方法指定位置参数的类型字符串
          void method_getArgumentType (Method m, unsigned int index, char *dst, size_t dst_len);
        
          // 返回指定方法的方法描述结构体
          struct objc_method_description * method_getDescription (Method m);
        
          // 设置方法的实现
          IMP method_setImplementation (Method m, IMP imp);
        
          // 交换两个方法的实现
          void method_exchangeImplementations (Method m1, Method m2);
        
      3. 方法调用流程
        1. 编译器会将消息表达式 [obj method] 转化为一个消息函数的调用,即 objc_msgSend。这个函数将消息接收对象(obj)和方法名(method)作为其基础参数,如以下所示:
           objc_msgSend(obj, method)
          
          如果方法需要参数如下所示:
           objc_msgSend(obj, method, arg1, arg2, ...)
          
        2. objc_msgSend 首先它找到 method 对应的 IMP (方法实现)。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收对象的类来找到的确切的实现。
        3. objc_msgSend 调用 IMP (方法实现),并将接收对象及方法的所有参数传给它。
        4. 最后,objc_msgSendIMP (方法实现) 的返回值作为 objc_msgSend 自己的返回值。
      4. 方法调用过程中的根据 SEL 查找对象的 IMP

        1. 主要依赖 objc_class 结构体中的: 1.isa 2. super_class 3. objc_method_list 4. objc_cache

           struct objc_class {
               Class isa  OBJC_ISA_AVAILABILITY;
          
           #if !__OBJC2__
               Class super_class                       OBJC2_UNAVAILABLE;  // 父类
          
               const char *name                        OBJC2_UNAVAILABLE;  // 类名
               long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
               long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
          
               long instance_size                      OBJC2_UNAVAILABLE;  // 类的实例变量大小
               struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 类的成员变量链表
          
               struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
               struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
          
               struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
           #endif
          
           } OBJC2_UNAVAILABLE;
          
        2. 寻找IMP的过程:
          • 先从当前 classcache 方法列表(cache methodLists)里去找 找到了,跳到对应函数实现
          • 没找到,就从 class 的方法列表(methodLists)里找
          • 还找不到,就到 super class 的方法列表里找,直到找到基类(NSObject)为止
          • 最后再找不到,就会进入动态方法解析和消息转发的机制。

results matching ""

    No results matching ""