运行时(Runtime)篇幅一

170 阅读7分钟

为什么说Objective-C是一门动态的语言?文章中提到,Objective-C拥有的三大动态特性都是基于运行时的环境来实现。所以本篇就来探讨下运行时的特性及其它功能。

在iOS中运行时的另一个名称叫Runtime,是一套底层C语言的API,Objective-C拥有的三大动态特性都是基于Runtime来实现的。

苹果为了动态系统能够足够的强大使其拥有众多的API,所以Runtime相当于一个库。而且苹果为了开发人员能够足够的了解Runtime的底层特性也提供了开源代码。通过objc4-781.2版本中的runtime.h文件可以大致的了解下内容。

typedef struct objc_method *Method; 

typedef struct objc_ivar *Ivar;

typedef struct objc_category *Category;

typedef struct objc_property *objc_property_t;

struct objc_class {

    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

    Class _Nullable super_class                              OBJC2_UNAVAILABLE;

    const char * _Nonnull name                               OBJC2_UNAVAILABLE;

    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;

    long instance_size                                       OBJC2_UNAVAILABLE;

    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;

    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;

    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;

    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

虽然只是极小部分源码但该部分源码足以证明Runtime的强大。objc_method是一个方法的结构体、objc_ivar表示的是一个实例变量的结构体、objc_category表示的是一个分类的结构体等都是Objective-C中很基础的功能模块。因此要想全面性的了解Runtime就必须逐个的去了解其中的结构体结构和作用。本篇文章就先从objc_method入手。

objc_method

method是方法的意思,objc_method是指iOS方法的底层结构。iOS中方法可分实例方法类方法

实例方法:也叫动态方法对象方法,以减号(-)开头由对象来调用,实例方法中能访问当前对象的成员变量(实例变量)

类方法:以加号(+)开头,只能由来调用,且类方法中不能访问成员变量

实例方法表达方式:- (void)addSubview:(UIView *)view;
实例方法调用方式:[self.view addSubview:label];
类方法表达方式:+ (void)setAnimationsEnabled:(BOOL)enabled;
类方法调用方式:[UIView setAnimationsEnabled];

例:
@interface ViewController () {
    NSString *loadString; // 设定实例变量
}

然后设定一个实例方法和一个类方法且分别在方法中访问实例变量 loadString

- (void)loading {
    loadString = @"yi";
}

+ (void)load{
    loadString = @"yi"; // 报错
}

实例方法中访问时没有任何问题的,但在类方法中访问会报**Instance variable 'loadString' accessed in class method**错误,意思就是不能在类方法中访问实例变量'loadString'

在底层所有的方法都是普通C语言函数,借助Clang来逐渐深入了解下方法的本质。通过main函数首先来模拟下实例方法的调用是如何执行的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person stringForWord];
    }
    return 0;
}

打开终端,cd到main.m的上级目录并执行clang -rewrite-objc main.m,随后会生成一个main.cpp文件。打开文件拉至底部就能看到main函数编译成C++语言的结果。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("stringForWord"));
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

从编译后的代码中不难看出 [person stringForWord] 编译后主要的体现为 ((id)person, sel_registerName("stringForWord")),但前部还跟着一个objc_msgSend

同样 [Person alloc] 以及 init 的方法调用除了本体外也都有一个objc_msgSend的跟随。那调用类方法呢?

[Person man];
编译后
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("man"));

同样也有个objc_msgSend,所以基本可以认定objc_msgSend肯定是跟方法调用密切相关而且还是很重要的内容。那么就接下往下深究。

objc_msgSend

在Objective-C中向某个对象传递消息,就会使用动态绑定机制来决定需要调用的方法。既然由动态绑定机制决定那具体调用哪个方法则完全于运行期来决定。

不管是实例方法的调用还是类方法的调用,方法调用时OC开发中经常使用的功能,也被称为“传递消息”,消息有“名称”或“选择子(选择器)”,可以接收参数且可能还有返回值

通过Runtime库源码可以查询到objc_msgSend的“原型”大致如下:

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
  • id _Nullable self : 接收者
  • SEL _Nonnull op : 选择子 (方法的名字)
    • SEL : 选择子的类型

objc_msgSend函数会依据接收者选择子的类型来调用适当的方法。该方法需要在接收者所属的类中搜寻其“方法列表”。该“方法列表”可分实例方法列表类方法列表,并且都有相对应的缓存方法列表

当查找消息时底层逻辑会优选查找当前类的缓存方法列表内的方法看是否有匹配的,有则调用;没有的话才去查找对应的实例/类方法列表

如果当前类中查找不到对应的方法则就转向其父类中去查找,其查找方式跟当前类相同优先查找缓存列表在查找对应的方法列表。

Objective-C的对象都是继承至NSobject,当一直往上找到跟类即NSObject都没有找到对应的方法时并不会立马报错而是进行消息的转发机制。这是因为在运行期可以即系向类中添加方法,编译期在编译时还无法明确类中到底会不会有某个方法实现。

消息转发可分为两个阶段:

  • 动态方法解析:是指选征询接收者所属的,看其是否能动态添加方法来处理当前这个“未知的选择子(方法名)”

对象在收到无法处理的消息后,首先将调用其所属类下的类方法:

+(BOOL)resoveInstanceMethod:(SEL)selector // 实例方法调用
+(BOOL)resolveClassMethod:(SEL)selector   // 类方法调用

返回的BOOL值来决定该类是否可以新增一个方法来处理这个消息,并且提供了实例方法和类方法的不同的动态方法解析的方法。使用动态方法解析的前提条件是:相关的调用方法已被实现,只需在运行时动态插在类里面就可以。

当未找到动态插入的方法时,运行时系统就会通过

-(id)forwardingTagrgetForSelector:(SEL)selector

方法来处理,看是否可以通过把当前无法处理的消息转给其他接收者来处理。该过被称为:备援接收者

  • 完整的消息转发机制:
    • 查看接收者有没有其他对象能处理这条消息,有则调用
    • 备援接收者启动完整消息转发机制,让运行期系统把与消息有关的信息都封装到NSInvocation对象中并在继承体中逐一触发,直至NSObject
    • 如果继承体中都无法触发则由NSObject的方法抛出异常

再回过头来讲述下objc_method。首先看下其结构体代码(删除不重要部分代码)

struct objc_method {
    SEL method_name    
    char *method_types                            
    IMP method_imp                                 
}
  • SEL method_name : 函数名/方法名 类型为SEL 相同名字的方法即使在不同类中定义,它们的方法选择器也相同

    • SEL:代表方法的名称(选择子 选择器)在类加载的时候编译器会生成与方法相对应的选择子,并注册到Runtime运行系统中。
  • char *method_types: 类型是一个char指针,存储着方法的参数类型返回值类型

  • IMP method_imp: 指向方法的实现,本质上是一个函数指针

    • IMP: 代表函数指针-即函数的执行入口 使用C来调用

讲解了这么多虽然并不是消息发送及方法调用的全部内容,不难看出方法调用的逻辑还是有一定复杂的,但每一步理解清晰了也并不是那么的难。然而其重要程度那是相当高的,毕竟是一门语言存在的关键,也是程序运行的基础。从中也了解了运行时Runtime与方法之间密切不可分的关系,和方法对运行时的影响。