iOS开发之进阶篇(9)—— runtime运行时

目录

  • 前言
  • iOS编译流程
  • runtime介绍
  • 消息发送流程
  • 消息转发流程
  • Method Swizzling
  • 参考文档

前言

关于runtime的文章, 网上实在太多了, 内容层次深浅不一. 诚然, 要想把runtime讨论明白, 讲得深入彻底, 没有相当功力是不行的. 故, 本文退而求其次, 希望能把runtime讲得"知其然", 想必也是挺好的.

iOS编译流程

我们编写的所有代码, 最终都是要转换成二进制机器指令去执行的. 如图

C/C++/OC编译流程:

Swift编译流程:

:
例图是以模拟器为例, 所以编译结果为 x86-64 CPU 机器语言; 如果是真机, 则最终编译成 ARM CPU 机器语言

解析

  • iOS 开发中 Objective-C 是 Clang / LLVM 来编译的。
  • Swift 是 Swift / LLVM,其中 Swift 前端会多出 SIL optimizer,它会把 .swift 生成的中间代码 .sil 属于 High-Level IR, 因为 swift 在编译时就完成了方法绑定直接通过地址调用属于强类型语言,方法调用不再是像OC那样的消息发送,这样编译就可以获得更多的信息用在后面的后端优化上。
  • 不管编译的语言时 Objective-C 还是 Swift 也不管对应机器是什么,亦或是即时编译,LLVM 里唯一不变的是中间语言 LLVM IR。

引申
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。

关于iOS中的汇编, 详见深入iOS系统底层之汇编语言.

结论
在整个iOS编译过程中, runtime处在LLVM的前端部分(Frontend). 更具体点, 可以说是runtime将OC转换成C.

runtime介绍

为何要有runtime

  • C: 静态语言. 编译阶段就要决定调用哪个函数, 如果函数未实现就会编译报错.
  • OC: 动态语言(得益于runtime机制). 运行时才决定调用哪个函数, 只要函数声明过即使没有实现也不会报错.
  • Swift: 静态语言. 其对象方法的调用基本上是在编译链接时刻就被确定的. 详见Swift5.0的Runtime机制浅析.

Swift基本上取消了runtime机制, 故本文还是主要讨论OC下的runtime. 当然, 通过Swift与OC混编, 我们也可以在Swift文件中调用OC的runtime接口.

总所周知, OC 扩展自 C 语言,然后拥有了面向对象性质和消息传递机制, 成为了动态语言。而这个扩展的核心就是我们今天的主角—— runtime。

何为runtime

runtime 其实是一个系统动态共享库, 具有一个公共接口, 该公共接口由头文件中的一组函数和数据结构组成 (纯C语言API). 由于所有的OC代码终将转换成C代码, 使得 runtime 的API调用非常频繁, 所以新版runtime里面对应的实现基本上都是用C++和汇编语言混合来写的, 以便提高系统效率.

runtime原理

这一部分的讨论, 将围绕实例属性方法以及类别等在runtime中的表现形式来展开.

1. id --> objc_object

id是一个指向类实例的指针, 它在runtime中的定义如下:

typedef struct objc_object *id;

而objc_object在objc-private.h中定义如下:

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    ... 内容太多故省略
}

objc_object结构体包含一个isa指针, 根据isa就可以顺藤摸瓜找到对象所属的类. 而isa的类型isa_t使用union实现, 可能表示多种形态, 既可以当成是指针, 也可以存储标志位. 这是苹果提出的Tagged Pointer类型对象的概念, 目的是为了减少内存资源的浪费. 毕竟用 64 bit 存储一个内存地址显然是种浪费.

Tagged Pointer类型的对象采用一个跟机器字长一样长度的整数来表示一个OC对象,而为了跟普通OC对象区分开来,每个Tagged Pointer类型对象的最高位为1而普通的OC对象的最高位为0.

小结: OC中的对象终将转换成C中的结构体objc_object.

2. Class --> objc_class

我们在Xcode中输入基类NSObject, 然后 ⌘+单击 这个NSObject, 查看它的定义:

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

其中省略了#pragma clang部分, 有兴趣可查阅这篇博文#pragma

可以看到, NSObject有且仅有一个Class类型的isa属性, 也就是说, 一个类对象唯一保存的信息就是它的 Class 的地址. 由于OC中几乎所有的类(NSProxy等除外)都直接或间接地继承于NSObject类, 可以说, OC中的类都有一个isa属性. 那么这个isa又是什么呢?

我们继续 ⌘+单击 Class, 可以看到他在runtime中的定义:

typedef struct objc_class *Class;

此时我们发现, Class在runtime中是一个指向objc_class结构体的指针.

isa, 意思是is a, 这是一个...

继续查看objc_class结构体, 我们看到它在Xcode的runtime.h里定义如下:

/**
 * objc1.0
 */
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;

注意后面这个OBJC2_UNAVAILABLE
其实上面这个是兼容objc1.0版本的定义, 而目前我们使用的是objc2.0版本, 2.0中没有暴露出bjc_class所定义的详细内容.

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下载和查看开源的最新版本的Runtime库源代码。Runtime库的源代码是用汇编和C++混合实现的,你可以在头文件objc-runtime-new.h中看到关于struct objc_class结构的详细定义。

/**
 * objc2.0
 */
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_class.png

objc_class继承于objc_object, 也就是说一个OC类本身同时也是一个对象. 既然说类也是对象, 那么类的类型是什么呢?这里就引出了另外一个概念 —— Meta Class (元类).

小结: OC中的类终将转换成C中的结构体objc_class.

3. Meta Class 元类

为了处理类和对象的关系, runtime 库创建了一种叫做元类 (Meta Class) 的东西.

其实观察objc_classobjc_object的定义, 会发现两者本质相同(都包含isa指针), 只是objc_class多了一些额外的字段. 这些字段包括了创建一个类实例所需的信息, 以及这些实例的方法等. 那么类的信息和类方法储存在哪呢? 答案是在元类里.

我们来看下这张著名的图:

Meta Class.jpg

小结:

  • 实例的isa指针指向类, 类的isa指针指向元类.
  • 类所对应的objc_class里储存了实例的方法, 元类所对应的objc_class里储存了类方法.
  • 元类的isa指针指向自己, 形成闭环.
4. Ivar 成员变量 和 objc_property_t 属性

Ivar

Ivar: instance variable

Ivar 代表类实例的变量或属性(带下划线"_"), 其在runtime中定义如下:

typedef struct ivar_t *Ivar;

ivar_t最终嵌套在objc_class里, 在objc-rentime-new.h中的结构体层级关系如下:

ivar_t -> ivar_list_t -> class_ro_t -> class_rw_t -> class_data_bits_t -> objc_class

我们可以遍历一个类的成员变量和属性(加"_"):

// 打印成员变量列表
- (void)logIvarList {
    
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    for (int i=0; i

objc_property_t
@property 标记了类中的属性, 它是一个指向objc_property 结构体的指针:

typedef struct property_t *objc_property_t;

遍历属性:

// 打印属性列表
- (void)logPropertyList {
    
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    
    for (unsigned int i=0; i
5. Method / SEL / IMP 方法

Method
Method在runtime中定义如下:

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: selector
IMP: implementation

Method中存储了这三样东西:

  • SEL类型的方法名.
  • char指针的方法类型, 指向存储方法的参数类型和返回值类型.
  • IMP类型的方法实现地址.

SEL

Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

SEL是一个方法选择器, 在runtime中其实是一个C字符串, 用来表示方法名称.

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);          
// 打印: viewDidLoad

SEL是由编译器在装载类的时候自动生成的, 因此我们不能强制将一个C字符串转化为SEL. 我们可以使用OC编译器命令@selector()或者runtime系统的sel_registerName函数来获得一个 SEL 类型的方法选择器. 例如:

SEL sel1 = @selector(viewWillAppear:);
SEL sel2 = sel_registerName("init");

不同类中相同名字的方法所对应的方法选择器是相同的, 即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器. 比如, 同一个类中, 相同方法名不同参数类型也是会报错的:

// 同一个类中, 相同方法名不同参数类型, 报错
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

IMP
IMP在runtime中的定义如下:

/// A pointer to the function of a method implementation.
typedef void (IMP)(void / id, SEL, ... */ ); 

IMP是一个函数指针, 指向了方法实现的首地址.

这里要注意, IMP 指向的函数的前两个参数是默认参数, id 和 SEL 。这里的 SEL 好理解,就是函数名。而 id ,对于实例方法来说, self 保存了当前对象的地址;对于类方法来说, self 保存了当前对应类对象的地址。后面的省略号即是参数列表。

小结:
Method / SEL / IMP 这三个概念之间关系: 在运行时, 类(Class)维护了一个消息分发列表来解决消息的正确发送. 每一个消息列表的入口是一个方法(Method), 这个方法映射了一对键值对, 其中键值是这个方法的名字 selector (SEL), 值是指向这个方法实现的函数指针 implementation (IMP).

6. Category

Category 为现有的类提供了拓展性, 它是 objc_category 结构体的指针.

typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}       

虽然在OC2中不是这样定义, 但是都有这些内容, 为简洁起见, 姑且这样分析讨论吧.

其中包含对象方法列表、类方法列表、协议列表等. 从这里我们也可以看出, Category 支持添加对象方法、类方法、协议, 但不能保存成员变量.

注意:在 Category 中是可以添加属性的,但不会生成对应的成员变量、 getter 和 setter 。因此,调用 Category 中声明的属性时会报错。

关联对象
我们可以通过关联对象的方式来添加可用的属性:

- (void)setXxx:(NSString *)xxx {
    
    objc_setAssociatedObject(self, &xxx, xxx, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)xxx {
    
    return objc_getAssociatedObject(self, &xxx);
}

消息发送流程

当我们在OC中执行一个方法:

[receiver message]

编译器会编译成运行时的C代码:

objc_msgSend(receiver, selector)

如果消息含有参数, 则为:

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

其实有四个消息发送方法: objc_msgSend, objc_msgSend_stret, objc_msgSendSuperobjc_msgSendSuper_stret。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”。

对于[receiver message], 在编译阶段确定了要向接收者 receiver 发送 message 这条消息,而 receive 将要如何响应这条消息, 那就要看运行时发生的情况来决定了.

以下是objc_msgSend消息发送流程:

  1. 检测这个selector是不是要被忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个target对象是不是nil对象。(nil对象执行任何一个方法都不会Crash,因为会被忽略掉)
  3. 首先会根据target(objc_object)对象的isa指针获取它所对应的类(objc_class)
  4. 查看缓存cache中是否存在方法。 如果有,则找到objc_method中的IMP类型(函数指针)的成员method_imp去找到实现内容,并执行; 如果没有,那么到该类的方法表(methodLists)查找该方法,依次从后往前查找。
  5. 如果没有在类(class)找到,再到父类(super_class)查找,直至根类。
  6. 一旦找到与选择子(selector)名称相符的方法,就跳至其实现代码。
  7. 如果没有找到,就会执行消息转发(message forwarding)的第一步动态解析。

消息转发流程

先来看看这张图:

消息转发.png

向不处理该消息的对象发送消息是错误的. 但是, 在宣布错误之前, 运行时系统会给接收对象第二次处理消息的机会. 这个机会分三步走:

  1. 动态方法解析
  2. 接收者重定向
  3. 消息重定向

1. 动态方法解析

我们可以通过分别重载+resolveInstanceMethod:+resolveClassMethod:方法, 分别添加实例方法实现和类方法实现. 然后返回YES, 运行时系统就会重新启动一次消息发送的过程.

// Person.h

@interface Person : NSObject

+ (void)eat;
- (void)work;

@end
// Person.m

#import "Person.h"
#import 

@implementation Person

// 添加类方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if(sel == @selector(eat)){
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "v@");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

// 添加实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(work)){
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 新的类方法
+ (void)kk_eat {
    NSLog(@"%s", __func__);
}

// 新的实例方法
- (void)kk_work {
    NSLog(@"%s", __func__);
}

@end
    // 类方法
    [Person eat];
    // 对象方法
    Person *person = [Person new];
    [person work];
// --------------------------------------------------------------
打印:
+[Person kk_eat]
-[Person kk_work]

注: 添加的方法必须实现, 否则报错! 比如上述代码中的kk_eatkk_work必须实现.

关于class_addMethod的最后一个参数types
我们使用C函数来说明会比较好理解, 比如:

void eat(id self, SEL _cmd, NSString *str)
{
    NSLog(@"%@", str);
}

那么types参数为"v @ : @“, 按顺序分别表示:

  • v: 返回值类型void, 若是i则表示int
  • @: 参数id(self)
  • :: SEL(_cmd)
  • @: id(str)

更多类型详见Type Encodings

2. 接收者重定向

如果动态方法解析部分中, +resolveInstanceMethod:+resolveClassMethod:都返回了NO, 则会分别调用重定向类方法+forwardingTargetForSelector:和重定向实例方法-forwardingTargetForSelector:. 在这两个方法中, 我们可以指定新的消息接收者, 但要注意的是新的接受者必须实现了该消息.

新建一个类Alien.

// Alien.h

@interface Alien : NSObject

+ (void)eat;
- (void)work;

@end
// Alien.m

#import "Alien.h"

@implementation Alien

+ (void)eat {
    NSLog(@"%s", __func__);
}

- (void)work {
    NSLog(@"%s", __func__);
}

@end

在原来的Person类的+resolveInstanceMethod:+resolveClassMethod:方法里返回NO. 然后重写重定向类方法+forwardingTargetForSelector:和重定向实例方法-forwardingTargetForSelector:.

#import "Person.h"
#import 
#import "Alien.h"

@implementation Person

// 添加类方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if(sel == @selector(eat)){
        return NO;
//        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "v@:");
//        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

// 添加实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(work)){
        return NO;
//        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "v@:");
//        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 重定向类方法:返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(eat)) {
        return [Alien class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 重定向实例方法:返回一个实例
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(work)) {
        Alien *alien = [Alien new];
        return alien;
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 新的类方法
+ (void)kk_eat {
    NSLog(@"%s", __func__);
}

// 新的实例方法
- (void)kk_work {
    NSLog(@"%s", __func__);
}

打印的不是Person的方法, 而是Alien的方法:

log:
+[Alien eat]
-[Alien work]

3. 消息重定向

如果
对于类方法, +resolveClassMethod:返回NO, +forwardingTargetForSelector:返回nil;
对于实例方法, +resolveInstanceMethod:返回NO, -forwardingTargetForSelector:返回nil.
那么
进入第三步也是最后一步 —— 消息重定向.

消息重定向又分为两个小步骤:

  1. runtime系统会向对象发送-methodSignatureForSelector消息, 并取到返回的方法签名用于生成NSInvocation对象;
  2. 将生成的NSInvocation对象作为参数调用-forwardInvocation:

// ViewController.m

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ViewController没有work方法, 将会走转发
    [self performSelector:@selector(work)];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 返回nil,进入下一步转发
}


// 返回一个NSInvocation对象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        // 生成方法签名
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return methodSignature;
}


// 消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    SEL sel = anInvocation.selector;

//    // 类方法
//    if ([[Alien class] respondsToSelector:sel]) {
//        [anInvocation invokeWithTarget:[Alien class]];
//    }else{
//        // 若无法响应, 则报错: 找不到响应方法
//        [self doesNotRecognizeSelector:sel];
//    }
    
    // 实例方法
    Alien *alien = [Alien new];
    if ([alien respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:alien];
    }else{
        // 若无法响应, 则报错: 找不到响应方法
        [self doesNotRecognizeSelector:sel];
    }
}

log:

-[Alien work]

Method Swizzling

消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子:

#import  
 
@implementation UIViewController (Tracking) 
 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
        // When swizzling a class method, use the following:
        // Class aClass = object_getClass((id)self);
        
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
 
        BOOL didAddMethod = 
            class_addMethod(aClass, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(aClass, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
 
#pragma mark - Method Swizzling 
 
- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 
 
@end

参考文档

Objective-C Runtime
深入解构objc_msgSend函数的实现
Runtime-iOS运行时基础篇
iOS Runtime详解
runtime开源

你可能感兴趣的:(iOS开发之进阶篇(9)—— runtime运行时)