[iOS开发]一篇文章带你深入理解runtime

一. runtime简介

runtime简称运行时,是一套底层的 C 语言 API。OC就是运行时机制,运行时机制中最主要的是消息机制。而消息机制就是开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。

  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数。
  • 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。OC作为动态语言,它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
  • 事实证明:
    在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
    在编译阶段,C语言调用未实现的函数就会报错。

二. runtime的数据结构

源码:

typedef struct objc_class *Class;
typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

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
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

通过以上源码可以总结出以下几点:

  • objc_class被typedef成了Class类型,objc_object被typedef成了id类型。
  • object类和NSObject类里面分别都包含一个objc_class类型的isa。
  • objc_class和objc_object都是结构体,且objc_class继承于objc_object,因此Objective-C 中类也是一个对象,叫类对象。
  • objc_object包含一个isa_t类型的结构体,因此objc_class也会包含isa_t类型的结构体isa。
  • 在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。

isa

isa可分为两类:

  • 指针型isa:其值代表Class地址。
  • 非指针型isa:其值的部分代表Class的地址,64位的其他部分留作他用,为了节省内存。

isa指向:

  • 关于对象,其isa指向类对象,实例对象调用方法就是通过isa找到类对象,到类对象中找到方法进行调用。
  • 关于类对象,其isa指向元类对象,调用类方法就是通过isa找到元类对象,到元类对象中找到方法进行调用。

cache_t

源码:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

typedef unsigned int uint32_t;
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long  uintptr_t;
typedef uintptr_t cache_key_t;

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

cache_t的数据结构如下:

它包括:

  • _buckets,一个散列表,用来存储Method的链表。
  • mask: 分配用来缓存bucket的总数。
  • occupied:表明目前实际占用的缓存bucket的个数。

而bucket_t的数据结构是:

  • key: cache_key_t。
  • IMP:无类型的函数指针, 指向了一个方法的具体实现。

cache_t的意义在于:

  • 用于快速查找方法执行函数,比如在调用方法时,如果方法有缓存,那就不用到方法列表中逐一遍历去查找方法的具体实现了。
  • 是可以增量扩展的哈希表结构,可增量扩展意思是当存储的数据量大时可以扩展内存空间来支持更多的缓存。
  • 是局部性原理的最佳应用:局部性原理体现在将调用频次最高的方法放到缓存当中,那么下次的命中率会更高,优化方法调用的性能。往往当一个实例对象调用方法时,首先根据对象的isa指针查找到它对应的类对象,然后在类对象方法列表中搜索方法,如果没有找到,就使用super_class指针到父类对象的方法列表中查找,一旦找到就调用方法,如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到方法列表中查找,这样就能优化方法调用的性能。

class_data_bits_t

class_data_bits_t数据结构:

总结:

  • class_data_bits_t结构体是对class_rw_t的封装,代表着类或分类中变量,属性,方法链表。
  • class_rw_t结构体又是对class_ro_t的封装,它的结构包括:class_ro_t,protocols,properties,methods,后三者是二维数组,比如methods的元素是一个类或者它的分类的方法列表methodList,而methodList的元素是method_t。
  • class_ro_t结构体是一个指向常量的指针,ro代表它是只读的,
    存储编译器决定了的成员变量,属性,方法和遵守协议,而它们都是一维数组。

method_t

源码:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

从源码可以看出method_t结构体有三个成员变量:

  • SEL name:代表方法名称。
  • const char* types:返回值和参数组合,不可变的字符类型指针。
  • IMP imp:无类型的函数指针,对应的是函数体。

其中types的数据结构如下:

我们知道runtime的messageSend方法头两个参数是默认固定的参数,第一个参数默认是self,第二个参数默认是方法名称SEL,所以types值中代表参数的两个固定的值是@:,比如方法- (void)aMethod;,它的types值就是:v@:v代表返回值是void,@代表第一个参数是id类型的self,:代表第二个参数SEL。

总结

runtime整个数据结构如下:

三. 消息传递

实例对象、类对象、元类对象

先看一张经典的图,它能展示出三者之间的关系:

总结:

  • 类对象存储实例方法列表等信息。
  • 元类对象存储类方法列表等信息。
  • 类对象和元类对象都是objc_class数据结构类型的,都继承于objc_object, 所以都有isa指针,所以实例对象可以通过isa找到类对象,进而访问类对象存储的实例方法列表,类对象也可以通过isa找到元类对象,进而访问元类对象存储的类方法列表。

实例对象调用实例方法过程

图解过程:

在此过程中会经历三种查找:

  • 缓存查找:根据给定的SEL,通过哈希函数的算法得到bucket_t对应cache_t数组中的索引位置,哈希函数表达式就是SEL选择器因子和对应的mask作与操作,mask是cache_t的成员变量,查找到bucket_t后,就能提取到IMP指针,返回给调用方。
  • 在当前类中查找:
  1. 对于已排序好的方法列表,采用二分查找算法查找方法对应执行函数。
  2. 对于没有排序的方法列表,采用一般遍历查找方法对应执行函数。
  • 父类逐级查找:通过当前类的superClass指针去访问父类,先判断父类是否为空,是空就结束查找,否就看能否根据SEL在父类缓存中找到相应的方法实现,如果找到就结束流程,如果父类缓存中没有,就遍历父类方法列表,看看能否查找到SEL对应的方法实现,如果还没有,就根据父类的superClass找到父类的父类,继续此逐级查找过程。


所以整个消息传递流程可以作如下总结:

  1. 当实例对象调用一个方法时,系统会根据实例对象的isa指针找到它的类对象,查找类对象的缓存中是否有对应SEL的IMP方法实现,如果没有,则在当前类对象的实例方法列表,二分查找或遍历查找同名的方法实现IMP,如果找到,填充到缓存中,并返回selector,如果没有找到,根据类对象的superClass指针到父类对象的方法列表中进行父类逐级查找,直到根类对象还没找到,则进入消息转发流程。
  2. 当调用一个类方法时,类对象根据isa指针找到元类对象,也是先到元类对象的缓存中查找,没找到则在当前元类对象的类方法列表,二分查找或遍历查找同名的方法实现IMP,如果找到,填充到缓存中,并返回selector,如果没有找到,根据元类对象的superClass指针到父元类对象的方法列表中进行父类逐级查找,直到根元类对象,如果还没找到,就到根元类对象的superClass指向的根类对象(也就是NSObject)中找同名的方法,如果还没找到就走消息转发流程。

四. 消息转发机制

我们定义的方法如果没有实现,系统会依次调用以下方法:

- (void)testMethod;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        NSLog(@"resolveInstanceMethod:");
        
        return NO;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector:(");
    return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testMethod)) {
        NSLog(@"methodSignatureForSelector:");
        return [NSMethodSignature signatureWithObjCTypes: "v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"forwardInvocation:");
}

整个实例方法的消息转发流程:

  • resolveInstanceMethod方法如果返回YES,那么根据SEL就找到了对应的函数实现,代表消息已处理;如果返回NO,系统会给出第二次机会去处理消息。
    比如,我们给原来定义的方法动态添加一个实现:
void testImp (void) {
    NSLog(@"hhh");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        NSLog(@"resolveInstanceMethod:");
        
        //动态添加方法实现
        class_addMethod(self, @selector(testMethod), testImp, "v@:");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

那么根据testMethod方法的SEL就找到了testImp的实现,因此就不会再调用下面的方法,消息转发结束。

  • forwardingTargetForSelector方法用于指定一个转发目标,系统会把消息转发给此目标。如果我们在此函数中返回一个nil,也就是没有指定转发目标的话,系统会给出第三次处理这条消息的机会。
  • methodSignatureForSelector方法的返回值是个NSMethodSignature对象,它是对方法的返回值,返回值类型,参数,参数类型,参数个数的封装,如果此函数返回了方法签名,则系统会接着调用forwardInvocation方法,如果该方法能处理消息,则消息转发流程就此结束,如果不能处理或methodSignatureForSelector没有返回方法签名,则此消息就被标记为无法处理。

消息转发流程图解:

五. method-Swizzling

将两个方法对应的方法实现进行交换:


使用场景:比如统计页面进入次数或时长,那么可以交换系统的viewWillAppear方法,在新方法中插入统计逻辑即可,而不用在每个页面的viewWillAppear方法中都作统计。

#import "UIViewController+runtime.h"
#import 
@implementation UIViewController (runtime)

+ (void)load {
    Method testMethod = class_getInstanceMethod([UIViewController class], @selector(viewWillAppear:));
    Method testOtherMethod = class_getInstanceMethod(self, @selector(testOtherMethod));
    method_exchangeImplementations(testMethod, testOtherMethod);
}

- (void)testOtherMethod {
    //插入逻辑
    NSLog(@"testOtherMethod");
}

六. 动态方法解析

动态方法解析总结:

  • 用@dynamic标记属性,则属性的get,set方法就是运行时添加的而不是编译时声明好的。
  • 编译时语言和动态运行时语言的区别是:
  1. 动态运行时语言将函数决议推迟到运行时,为方法添加具体的实现。比如当我们把一个属性标示为@dynamic时,代表着不需要编译器在编译时为属性生成get,set方法实现,而是在运行时具体调用属性的get或set方法时,再添加相应的实现。
  2. 编译时语言在编译期进行函数决议,也就是在编译期间就确定了一个方法名称对应的具体的方法实现,而在具体的运行中是不能修改的。

方法缓存

方法缓存的目的:优化方法查找性能,因为当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。所以将调用频率高的方法缓存下来,提高命中率,也是上面文章所说的局部性原理的最佳应用。

推荐美团技术团队的这篇文章: 深入理解 Objective-C:方法缓存。

关于runtime的面试题

1. [self class] 和 [super class]。

 #import "Animal.h"

 @interface Dog: Animal

 @end

 @implementation Dog

 - (id)init
 {
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
 }
 @end

self和super的区别:

  • self是类的一个隐藏参数,每个方法的实现的第一个参数是self。
  • super是一个”编译器标示符”,而不是隐藏参数,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。

在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法。

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

从源码可以看出:

  • objc_msgSendSuper方法中,第一个参数是一个objc_super结构体。
  • objc_super结构体有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class。

调用objc_msgSendSuper的流程是:
从objc_super结构体中的superClass指向的父类的方法列表开始查找selector,找到后以objc_super->receiver去调用父类的这个selector。所以最后的调用者是objc->receiver也就是self,即objc_super->receiver = self。

最后结论:
无论是self还是super,接收者都是当前对象,区别是self调用class时,是从该实例对象的类对象顺次向上查找,而super调用class时,越过了该实例对象的类对象,是从其类对象的父类对象顺次向上查找,不过最终都找到了根类对象NSObject中的class方法,从而返回相同的结果。

2. isKindOfClass 与 isMemberOfClass。

    BOOL result1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL result2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL result3 = [(id)[Dog class] isKindOfClass:[Dog class]];
    BOOL result4 = [(id)[Dog class] isMemberOfClass:[Dog class]];
    NSLog(@"%d %d %d %d", result1, result2, result3, result4);

在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不等再继续取super class,如此循环下去。

[NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObject 和 NSObject的meta class是否相等,很明显不等。接着第二次循环判断NSObject与meta class的superclass是否相等,而Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身,所以相等,result1为YES。

同理,[Dog class]执行完之后调用isKindOfClass,第一次for循环,Dog的meta Class与Dog不等,第二次循环,Dog meta Class的super class 指向的是 NSObject meta Class, 和 Dog也不相等。第三次循环,NSObject Meta Class的super class指向的是NSObject Class,和 Dog 不相等。第四次循环,NSObject Class 的super class 指向 nil, 和 Dog不相等,最终result3为NO。

如果是Dog的实例对象,[dog isKindOfClass:[Dog class],那么此时就应该输出YES了。因为在isKindOfClass函数中,判断Dog的isa指向是否是自己的类Dog,第一次for循环就能输出YES了。

isMemberOfClass是拿到自己的isa指针和自己比较,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject不相等。第四行,isa指向Dog的Meta Class,和Dog也不等,所以第二行result2和第四行result4都为NO。

你可能感兴趣的:([iOS开发]一篇文章带你深入理解runtime)