详解Runtime

411 阅读9分钟

Runtime是什么

顾名思义,Runtime是运行时, 是 Objective-C 区别于 C 语言这样的静态语言的一个非常重要的特性。

对于静态语言,函数的调用会在编译期就已经决定好,在编译完成后直接执行。OC 是一门动态语言,函数调用变成了消息发送,在编译期不知道要调用哪个函数。 Runtime 就是去解决如何在运行时期找到具体调用的方法。

#if !OBJC_TYPES_DEFINED

/// An opaque type that represents an Objective-C class.

typedef struct objc_class *Class;

/// 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;

#endif

/// An opaque type that represents a method selector.

typedef struct objc_selector *SEL;


/// 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

struct objc_protocol_list {

    struct objc_protocol_list *next;

    long count;

    Protocol *list[1];

};

struct objc_category {

    char *category_name  OBJC2_UNAVAILABLE;

    char *class_name     OBJC2_UNAVAILABLE;

    struct objc_method_list *instance_methods  OBJC2_UNAVAILABLE;

    struct objc_method_list *class_methods    OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE;

}       OBJC2_UNAVAILABLE;

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

}                         OBJC2_UNAVAILABLE;


struct objc_ivar_list {

    int ivar_count      OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space           OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_ivar ivar_list[1]    OBJC2_UNAVAILABLE;

}          OBJC2_UNAVAILABLE;


struct objc_method {

    SEL method_name     OBJC2_UNAVAILABLE;

    char *method_types  OBJC2_UNAVAILABLE;

    IMP method_imp      OBJC2_UNAVAILABLE;

}     OBJC2_UNAVAILABLE;

struct objc_method_list {

    struct objc_method_list *obsolete   OBJC2_UNAVAILABLE;

    int method_count      OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space             OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_method method_list[1]  OBJC2_UNAVAILABLE;

}   OBJC2_UNAVAILABLE;


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;

在OC中怎么调用方法

实例对象中存放 isa 指针,有 isa 指针可以找到实例对象所属的类对象 ,类中存放着实例方法列表。 在编译时期,根据方法名字会生成方法的唯一的标识 SEL。IMP 是函数指针, 指向函数实现。 简化过程即:instance -> class -> methodList -> SEL -> IMP -> 实现函数

具体细化方法的调用分三种情况: 1,当调用实例方法时,通过 isa 指针找到实例对应的 class对象并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类中查询,直至根类(NSObject).

2,当调用类方法时,通过 isa 指针找到实例对应的 metaclass 并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类的metaclass中查询,直至根类的metaClass.

3,如果还没找到则进入消息转发过程。

整个 Runtime 的核心就是 objc_msgSend 函数,通过给类发送 SEL 以传递消息,找到匹配的 IMP。如下的这张图描述了对象的内存布局。

image.png

消息转发机制

简化图如下: image.png 消息转发共分为3个步骤:

  • 消息动态处理阶段

  • 消息快速转发阶段

  • 消息常规转发阶段

消息动态处理阶段

  检查该类是否实现了resolveInstanceMethod: 方法,然后检查是否动态向该类添加了方法。

- (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(run)) {

        SEL readSEL = @selector(readBook);

        Method readM = class_getInstanceMethod(self, readSEL);

        IMP readImp = method_getImplementation(readM);

        const char *type = method_getTypeEncoding(readM);

        return class_addMethod(self, sel, readImp, type);

    }

    return [super resolveInstanceMethod:sel];

}

+ (BOOL)resolveClassMethod:(SEL)name
{
    return [super resolveClassMethod:name];
}

动态方法解析的实质: 在该方法内动态向该类添加方法。添加后类对象里面的实例方法列表和缓存方法列表中就有此方法了,返回true后,系统会自动再次遍历继承链去查找imp。

消息快速转发阶段

对象内部可能还有其他可以响应此方法的对象,所以这个方法是转发SEL去对象内部的其他可以响应该方法的对象。

- (id)forwardingTargetForSelector:(SEL)aSelector {

    return [[Car alloc] init];

}

消息常规转发阶段

常规转发本质上跟快速转发是一样的,都是切换接受消息的对象,但是常规转发切换接受消息的对象更复杂一些,快速转发只需返回一个可以响应的对象就可以了,常规转发还需要手动将响应方法切换给响应对象。

第三步有2个步骤:

1.创建签名

2.转发方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {

    NSString *sel = NSStringFromSelector(selector);

    if([sel isEqualString:@"run"]) {

        return [NSMethodSignature signatureWithObjcTypes:"v@:"];

    }

    return [sunper methodSignatureForSelector:selector];

}

- (void)forwardInvocation:(NSInvocation *)invocation {

    SEL selector = [invocation selector];

    //新建需要转发消息的对象

    Car *car = [[Car alloc] init];

    if([car respondsToSelector:selector]){

        [invocation invokeWithTarget:car];

    }

}

总结:在三个步骤的每一步,消息接受者都还有机会去处理消息。同时,越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二步转移消息的接受者也比进入常规转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。

Category原理

struct objc_category {

    char *category_name  OBJC2_UNAVAILABLE;

    char *class_name     OBJC2_UNAVAILABLE;

    struct objc_method_list *instance_methods  OBJC2_UNAVAILABLE;

    struct objc_method_list *class_methods    OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE;

}       OBJC2_UNAVAILABLE;

从category的定义也可以看出category可为类添加实例方法、类方法、协议、属性)和不可为类添加实例变量。

category和关联对象

在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值:

#import "MyClass.h"

@interface MyClass (Category1)

@property(nonatomic,copy) NSString *name;

@end

----------

#import "MyClass+Category1.h"

#import <objc/runtime.h>

@implementation MyClass (Category1)

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

- (void)setName:(NSString *)name

{

    objc_setAssociatedObject(self,

                             "name",

                             name,

                             OBJC_ASSOCIATION_COPY);

}

- (NSString*)name

{

    NSString *nameObject = objc_getAssociatedObject(self, "name");

    return nameObject;

}

@end

category中的方法

当category中的方法methodA和原类中的方法同名时,Category中的方法会替换原来中的方法,但是没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA,这事category的方法被放到了方法列表的前面,而原来类的方法被放到了方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会返回,殊不知后面可能还有一样名字的方法。

load和initialize

  • load

load方法在程序启动时加载类或者分类的时候调用;

------分类中load方法不会覆盖本类的load方法。

load函数的加载顺序为:superClass -> class -> category 注意:Runtime调用+(void)load时没有autorelease pool: 原因是runtime调用+(void)load的时候,程序还没有建立其autorelease pool,所以那些需要使用到autorelease pool的代码,都会出现异常。这一点是非常需要注意的,也就是说放在+(void)load中的对象都应该是alloc出来并且不能使用autorelease来释放。

2,initialize

initialize是在类或者其子类的第一个方法被调用前调用。 ------分类中initialize方法会覆盖本类的initialize方法。

initialize函数的调用顺序为:superClass -> class 或者 superClass-> category

如果该类是子类:

a,子类中没有实现 + (void)initialize 消息,那么则会调用其父类的实现。父类的 + (void)initialize 可能会被调用多次。

b,子类中实现 + (void)initialize 消息,会先调用其父类的实现,再调用子类的实现。

c,如果类包含分类(扩展 catagory),且分类重写了initialize方法,那么则会调用分类的 initialize 实现,而原类的该方法实现不会被调用。子父类调用顺序不变。

这个机制同 NSObject 的其他方法(除 + (void)load 方法) 一样,即如果原类同该类的分类包含有相同的方法实现,那么原类的该方法被隐藏而无法被调用。

load和initialize的使用场景

4,使用场景

  • load load方法是线程安全的,而且一定会调用且只会调用一次,一般可以用来交换方法Method Swizzle。

  • initialize initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作

KVO的原理

KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

- (void)viewDidLoad {

    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];

    Person *p2 = [[Person alloc] init];

    p1.age = 1;

    p1.age = 2;

    p2.age = 2;

    // self 监听 p1的 age属性

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    p1.age = 10;

    [p1 removeObserver:self forKeyPath:@"age"];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}

// 打印内容

监听到<Person: 0x604000205460>的age改变了{
    kind = 1;
    new = 10;
    old = 2;
}

在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。

KVO底层实现原理

1,person对象内存结构图 image.png 在调用setage方法的时候,首先会通过p2对象中的isa指针找到Person类对象,然后在类对象中找到setage方法。然后找到方法对应的实现。

2,person加了KVO后的内存结构图

image.png p1对象的isa指针在经过KVO监听之后已经指向了NSKVONotifyin_Person类对象,NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Person是runtime在运行时生成的。那么p1对象在调用setage方法的时候,肯定会根据p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及实现。

NSKVONotifyin_Person中的setage方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作是先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

注意:NSKVONotifyin_Person内重写的class内部实现大致为

- (Class) class { 
    // 得到类对象,在找到类对象父类 
    return class_getSuperclass(object_getClass(self)); 
}

apple不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1的class对象方法时,是Person类。

常见KVO相关问题

1,iOS用什么方式实现一个对象的KVO?(KVO的本质是什么?)

---------当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

2,如何手动触发KVO 答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

Person *p1 = [[Person alloc] init];
p1.age = 1.0;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];