Runtime那些事

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Runtime介绍。

Runtime那些事

前言

从字面意思看,就是运行时。但是这个运行时究竟什么意思?可以把它理解成:不是在编译期也不是在链接期,而是在运行时。那究竟在运行期间做了什么呢?按照苹果官方的说法,就是把一些决策(方法的调用,类的添加等)推迟,推迟到运行期间。只要有可能,程序就可以动态的完成任务,而不是我们在编译期已经决定它要完成什么任务。这就意味了OC不仅仅需要编译器,还需要一个运行时的系统来支撑。

目录

接下来就对Runtime做一个系统的介绍,主要内容包括:

  1. 简介
  2. 涉及到的数据结构
  3. runtime.h解析
  4. 如何可以触及到RunTime?
  5. 消息
  6. 动态消息解析
  7. 消息转发
  8. Runtime的使用场景

1.简介

根据前言,你已经了解了Runtime大概是个什么鬼,在OC发展历程中,它主要有两个版本:Legacy和Modern。Legacy版本采用的是OC1.0版本;Modern版本采用的OC2.0版本,而且相比Legacy也添加了一些新特性。最明显的区别在于:

  • 在legacy版本,如果你改变了类的布局,那么你必须重新编译继承自它的类。
  • 在modern版本,如果你改变了类的布局,你不必重新编辑继承自它的类。
平台

iPhone的应用程序以及OS X v10.5版本的64位机器使用的是modern版本的runtime。
其他(OS X桌面应用32位程序)使用的是legacy版本的runtime。

2.涉及到的数据结构

这里主要介绍一下在runtime.h里面涉及到的一些数据结构。

Ivar

Ivar从字面意思来讲,它就是代表的实例变量,它也是一个结构体指针,包含了变量的名称、类型、偏移量以及所占空间。

SEL

选择器,每个方法都有自己的选择器,其实就是方法的名字,但是不仅仅是方法的名字,在objc.h中,我们可以看到它的定义:

/// An opaque type that represents a method selector.一个不透明类型,用来代表一个方法选择器
typedef struct objc_selector *SEL;

由定义可知它是一个objc_selector的结构体指针,尴尬的是在runtime源码中并没有找到该结构体。猜想它内部应该就是一个char 的字符串。
你可以使用:

 NSLog(@"%s",@selector(description));  //%s用来输出一个字符串

打印出来description。
在这里你可以把它理解成一个选择器,可以标识某个方法。

IMP

它是一个函数指针,指向方法的实现,在objc.h里面它的定义是这样的:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
id

id是一个我们经常使用的类型,可用于作为类型转换的中介者。它类似于Java里面的Object,可以转换为任何的数据类型。它在objc.h里面是这样定义的:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

它其实是一个objc object的结构体指针,而在后面将要提到的Class其实是个objc class的指针,而objc _ class是继承自objc _o bject的,因此可以相互转换,这也是为什么id可以转换为其他任何的数据类型的原因。

Method

方法,它其实是一个objc_method的结构体指针,其定义如下:

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}  

这个就比较好理解了,该结构体包含了方法的名称(SEL),方法的类型以及方法的IMP。

Class

它是一个objc_class的结构体指针,在runtime.h中的定义如下:

/// An opaque type that represents an Objective-C class.一个不透明类型,代表OC的类
typedef struct objc_class *Class;

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;

该结构体中各部分介绍如下:

  • isa:是一个Class类型的指针,每个对象的实例都有isa指针,他指向对象的类。而Class里面也有个isa指针,它指向meteClass(元类),元类保存了类方法的列表。
  • name:对象的名字
  • version:类的版本号,必须是0
  • info:供运行期间使用的位标识
  • instance_size:该类的实例大小
  • ivars:成员变量数组,包含了该类包含的成员变量
  • methodLists:包含方法的数组列表,也是一个结构体,该结构体里面还包含了一个obsolete的指针,表示废弃的方法的列表
  • cache:缓存。这个比较复杂,在后面会提到,这里先忽略。
  • protocols:协议列表,也是一个数组

而在objc-runtime-new.h中,你会发现这样的定义(在runtime中并没有完全暴露objc_class的实现):

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    //其他的省略

其实objc class继承自objc object。所以这也说明了为什么id能够转换为其他的类型。

3.runtime.h解析

我们先看一下在usr/include/objc/runtime.h,这个是任何一个工程都可以直接找到的,它是SDK的一部分。主要定义了以下内容:

  1. 定义了一些类型,例如Method/Ivar/Category等,还有一些结构体。
  2. 函数。函数里面有分了几大类:

    • 关于对象实例的方法,例如object getClass、object setClass以及object _ getIvar等。这些函数大多以object开头。 用来获取属性或者对对象进行操作。
    • 获得类定义的方法,例如objc getClass/objc getMetaClass等,这些方法更多的是获取Class或者在Class级别上进行操作。 多以objc开头
    • 和类相关的方法。例如class getName/class isMetaClass等,这些更多的是获取Class的一些属性。比如该类的属性列表、方法列表、协议列表等。传参大多为Class。 多以class开头
    • 实例化类的一些方法。例如class _ createInstance方法,就是相当于平时的alloc init。
    • 添加类的方法。例如你可以使用这些方法冬天的注册一个类。使用objc allocateClassPair创建一个新类,使用 objc registerClassPair对类进行注册
    • 等等。。。
  3. 另外就是一些废弃的方法和类型。

4. 如何可以触及到RunTime?

有三种不同的方式可以让OC编程和runtime系统交互。

OC源代码

大多数情况下,我们写的OC代码,其实它底层的实现就是runtime。runtime系统在背后自动帮我们处理了操作。例如我们编译一个类,编译器器会创建一个结构体,然后这个结构体会从类中捕获信息,包括方法、属性、Protocol等。

NSObject的一些方法

在Foundation框架里面有个NSObject.h,在usr/include/objc里面也有一个NSObject.h。而我们平时用到的类的基类是/usr/include/objc里面的这个NSObject.h,Foundation里面的NSObject.h只是NSObject的一个Category。所以这里我们更关注一下/usr/include/objc里面的NSObject.h。
由于大多数对象都是NSObject的子类,所以在NSObject.h里面定义的方法都可以使用。
在这些方法里面,有一些方法能够查询runtime系统的信息,例如:

- (BOOL)isKindOfClass:(Class)aClass;   //用来检测一个对象是否是某各类的实例对象,aClass也有可能是父类,同样可以检测出来。
- (BOOL)isMemberOfClass:(Class)aClass;   //而该方法只能检测一个对象是否是某各类的实例对象。但如果aClass不能为该类的父类,如果是父类则该方法返回NO

- (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

- (IMP)methodForSelector:(SEL)aSelector;

这里用代码对isKindOfClass和isMemberOfClass做个简单介绍:

//stu是Student的实例对象,Student的父类为Person,Person的父类为NSObject。
[stu isKindOfClass:[Student class]];    //YES
[stu isKindOfClass:[Person class]];    //YES
[stu isKindOfClass:[NSObject class]];   //YES

[stu isMemberOfClass:[Student class]];    //YES
[stu isMemberOfClass:[Person class]];    //NO
[stu isMemberOfClass:[NSObject class]];   //NO

我们可以在objc源代码中的NSObject.mm中看到相应的实现:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

从具体实现可知,为什么isKindOfClass能够检测出superclass。另外,在NSObject.h中,并没有看到两个方法的类方法声明,但是在实现里面却包含了类方法的实现。这里有个疑问:为什么没有对外声明的两个类方法依然可以在外部调用呢?(比如我可以直接使用[Student isMemberOfClass:[NSObject class]])
这里还用到了class方法,这个方法声明如下:

+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");

- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead");

+ (Class)class {   //返回当前的self
    return self;
}

- (Class)class {
    return object_getClass(self);
}

这里重要的是理解self究竟代表着什么:

  1. 当self为实例对象的时候,[self class] 和 object_getClass(self)是等价的。object_getClass([self class])得到的是元类。
  2. 当self为类对象的时候,[self class]返回的是自身,还是self。object_getClass(self) 与object_getClass([self class])等价。拿到的是元类。
Runtime函数

runtime系统其实就是一个动态共享的Library,它是由在/usr/include/objc目录的公共接口中的函数和数据结构组成。
runtime

5. 消息

在Objective-C中,消息直到运行时才将其与消息的实现绑定,编译器会将

[receiver message];

转换成

objc_msgSend(receiver,selector);   //1

objc_msgSend(receiver,selector,arg1,arg2,...);  //2

如果包含参数,那么就会执行2方法。其实除了该方法,还有以下几个方法:

objc_msgSend_stret    
objc_msgSendSuper
objc_msgSendSuper_stret

当想一个对象的父类发送message时,会使用

objc_msgSendSuper

如果方法的返回值是一个结构体,那么就会使用

objc_msgSend_stret
objc_msgSendSuper_stret

这里我们可以打开objc源码,然后你会发现里面有多个.s文件:
objc_msg_diff
这里之所以有objc-msg-类的不同文件,我猜想应该是对不同的CPU指令集(指令不一样)做了分别处理。因为这些.s文件名称中包含的是不同的arm指令集。而且打开.s文件你会发现里面的实现是汇编语言,所以苹果为了效率还是蛮拼的,直接用汇编语言实现。
其中就能找到objc _ msgSend的实现(objc-msg-i386.s中):
objc_msgSend
虽然对汇编了解不是太多,但是这个文件中的注释很详细,从注释可以看出objc_msgSend方法的执行过程:

  1. 先加载receiver和selector到寄存器,然后判断receiver是否为空,如果为空,则函数执行结束;
  2. 如果receiver不为空,开始搜索缓存,查看方法缓存列表里面是否有改selector,如果有则执行;
  3. 如果没有缓存,则搜索方法列表,如果在方法列表中找到,则跳转到具体的imp实现。没有则执行结束。
使用了隐藏参数

在发送一个消息的时候,会被编译成objc_msgSend,此时该消息的参数将会传入objc_msgSend方法里面。除此之外,还会包含两个隐藏的参数:

  1. receiver
  2. method的selector

这两个参数在上面也有提到。其中的receiver就是消息的发送方,而selector就是选择器,也可以直接用 cmd来指代( cmd用来代表当前所在方法的SEL)。之所以隐蔽是因为在方法声明中并没有被明确声明,在源代码中我们仍然可以引用它们。

获取方法地址

我们每次发送消息都会走objc_msgSend()方法,那么有没有办法避开消息绑定直接获取方法的地址并调用方法呢?答案当然是有的。我们上面简单介绍了IMP,其实我们可以使用NSObject的

- (IMP)methodForSelector:(SEL)aSelector;

方法,通过该方法获得IMP,然后调用该方法。但是避开消息绑定而直接调用的使用并不常见,但是如果你要多次循环调用的话,直接获取方法地址并调用不失为一个省时操作。看下面的代码:

 void (*setter)(id,SEL,BOOL);
    setter = (void(*)(id,SEL,BOOL))[stu2 methodForSelector:@selector(learning)];
    NSDate *startDate = [NSDate date];
    for (int i = 0;i<100000;i++) {
        setter(stu2,@selector(learning),YES);
    }
    double deltaTime = [[NSDate date] timeIntervalSinceDate:startDate];
    NSLog(@"----%f",deltaTime);
    
    NSDate *startDate1 = [NSDate date];
    for (int i = 0;i<100000;i++) {
        [stu2 learning];
    }
    double deltaTime1 = [[NSDate date] timeIntervalSinceDate:startDate1];
    NSLog(@"----%f",deltaTime1);

你可以自行跑一下,看一下时间差异。你会发现:获取方法地址直接调用更省时间,但请注意使用场景。

6. 动态消息解析

这里介绍一下如果动态地提供方法的实现。

动态方法解析

在开发过程中,你可能想动态地提供一个方法的实现。比如我们对一个对象声明了一个属性,然后我们使用了 @dynamic 标识符:

@dynamic propertyName;

该标识符的目的就是告诉编译器:和这个属性相关的getter和setter方法会动态地提供(当然你也可以直接手动在代码里面实现)。这个时候你就会用到NSObject.h里面的两个方法

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

来提供方法的实现。
其实OC方法就是一个简单的C函数,它至少包含了两个参数self和 _ cmd,你可以自己声明一个方法:

void dynamicMethodIMP(id self, SEL _cmd) {
    //这里是方法的具体实现
}

此时我们可以在声明属性的类中实现上面提到的两个方法(一个是解析类方法,一个是解析实例方法),例如我在Person里面这样写:

@dynamic address;   //也就意味着我们需要手动/动态实现该属性的getter和setter方法。

你会发现当我们运行下面的代码时,程序会crash:

   Person *zhangsan = [[Person alloc] init];
    zhangsan.address = @"he nan xinxiang ";
    
    NSLog(@"%@",zhangsan.address);
    
//    crash reason
// -[Person setAddress:]: unrecognized selector sent to instance 0x1d4449630

这里简单的做一个动态方法解析:

void setter(id self,SEL _cmd) {
    NSLog(@"set address");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selStr = NSStringFromSelector(sel);
    if ([selStr hasPrefix:@"set"]) {
        class_addMethod([self class], sel, (IMP)setter, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

所以我们需要自己去实现setAddress: 方法。(这里判断用hasPrefix不太准确,开发者可以自行根据需求调整)。转发消息(下面会讲到)和动态解析是正交的。也就是说一个class有机会再消息转发机制前去动态解析此方法,也可以将动态解析方法返回NO,然后将操作转发给消息转发。

动态加载

OC编程也允许我们在程序运行的时候动态去创建和链接一个类或者分类。这些创建的类或者分类将会和运行app前创建的类一样,没有差别。
动态加载在开发的过程中可以做好多事情,例如系统设置中的不同模块就是动态加载的。
在Cocoa环境中,最经典的就是Xcode,它可以安装不同的插件,这个也是动态加载的方式实现的。

7. 消息转发

发送一个消息给对象,如果对象不能处理,那么就会产生错误。然而,在产生错误之前,runtime 系统会给对象第二次机会去处理该消息。这里详细已经在深入浅出理解消息的传递和转发文章中做了介绍,这里就不再介绍了。

8. Runtime的使用场景

Runtime的使用几乎无处不在,OC本身就是一门运行时语言,Class的生成、方法的调用等等,都是Runtime。另外,我们可以用Runtime做一些其他的事情。

字典转换Model

平时我们从服务端拿到的数据是json字符串,我们可以将其转换成成NSDictionary,然后通过runtime中的一些方法做一个转换:
先拿到model的所有属性或者成员变量,然后将其和字典中的key做映射,然后通过KVC对属性赋值即可。更多可参见class_copyIvarList方法获取实例变量问题引发的思考中的例子。

热更新(JSPatch的实现)

JSPatch能做到JS调用和改写OC方法的根本原因就是OC是动态语言,OC上的所有方法的调用/类的生成都通过OC Runtime在运行时进行,我们可以根据名称/方法名反射得到相应的类和方法。例如

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也正是鉴于此,才实现了热更新。

给Category添加属性

我们可以使用runtime在Category中给类添加属性,这个主要使用了两个runtime钟的方法:

OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);

OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

具体使用可参见:给分类(Category)添加属性

Method Swizzling

它是改变一个已存在的selector的实现的技术,比如你想将viewDidload方法替换为我们自定义的方法,给系统的方法添加一些需要的功能,来实现某些需求。比如你想跟踪每个ViewController展示的次数,你可以使用该技术重写ViewDidAppear方法,然后做一些自己的处理。可以参见Method Swizzling里面的讲解。

总结

Objective-c本身就是一门冬天语言,所以了解runtime有助于我们更加深入地了解其内部的实现原理。也会把一些看似很难的问题通过runtime很快解决。

参考链接:

1.Objective-C Runtime Programming Guide
2.Objective-C Runtime
3.objc4
4.深入浅出理解消息的传递和转发
5.class_copyIvarList方法获取实例变量问题引发的思考
6.JSPatch 实现原理详解
7.给分类(Category)添加属性
8.Method Swizzling

相关文章
|
7月前
|
Linux Shell Windows
Java.Runtime.exec()的使用
Java.Runtime.exec()的使用
35 0
|
Kubernetes API 调度
Container Runtime CDI与NRI介绍
CDI介绍什么是CDICDI(Container Device Interface)是Container Runtimes支持挂载第三方设备(比如:GPU、FPGA等)机制。它引入了设备作为资源的抽象概念,这类设备由一个完全限定的名称唯一指定,该名称由设备商ID,设备类别与一个设备类别下的一个唯一名称组成,格式如下:vendor.com/class=unique_name设备商ID和设备类型(ve
4148 1
Container Runtime CDI与NRI介绍
|
存储 编译器 API
Runtime的使用
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。
293 0
Runtime的使用
|
编译器 C语言 iOS开发
Runtime详解及应用
动态语言:编译时确定变量的数据类型。 静态语言:运行时确定变得的数据类型。
340 0
Runtime详解及应用
|
设计模式 Java 开发者
Runtime 类|学习笔记
快速学习 Runtime 类
126 0
Runtime 类|学习笔记
|
设计模式 Java 开发者
Runtime 相关说明 | 学习笔记
快速学习 Runtime 相关说明。
150 0
|
Windows 设计模式
|
存储 JSON 数据格式