iOS的Runtime

一.Runtime简介

C语言中,在编译期,函数的调用就会决定调用哪个函数。而OC的函数,属于动态调用过程,在编译期并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数来调用。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。Runtime 就是这么一个主要使用 C 和汇编写的运行时库。

二.深入解析Objective-C

1. 类和对象(如何面向对象)

我们可以在Runtime文件里看到类(objc_class)和对象(objc_object)的底层实现。Class 是一个 objc_class 结构类型的指针, id是一个 objc_object 结构类型的指针(这也就是为什么能用id类型表示任何对象的原因)。

/*   类   */
typedef struct objc_class *Class;
struct objc_class : objc_object {
    Class isa;            
    Class superclass;          // 父类的指针
    cache_t cache;             // 存储方法缓存的结构体cache_t
    class_data_bits_t bits;    // 最终指向一个存储数据的结构体class_ro_t
}

/*   对象   */
typedef struct objc_object *id;
struct objc_object {
    Class _Nonnull isa;
};

/*  存储方法缓存的结构体  */
struct cache_t {
    struct bucket_t *_buckets;//用来缓存 bucket 的总数
    mask_t _mask;//目前实际占用的缓存 bucket 的个数
    mask_t _occupied;//一个散列表,用来方法缓存,包含 key 以及方法实现 IMP
};

/*  存储数据(方法列表,成员变量列表,协议列表)的结构体  */
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //实例大小
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;//方法列表
    protocol_list_t * baseProtocols;//协议列表
    const ivar_list_t * ivars;//实例变量列表,const是存储在只读区,这也是分类添加不了实例变量的原因

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;//属性列表
};

objc_object中有一个Class类型的指针isa,所以常说所有对象都会包含isa指针。objc_class继承于objc_object,其中也包含一个isa指针,所以说类本质上也是一个对象。
class保存了方法列表,还有指向父类的指针。但class也是object,也会有isa变量,那么它又指向哪儿呢?这里就引出了第三个类型: metaclass。object的isa指向class,class的isa指向metaclass。
以下是一副经典的图,清楚说明了对象、类、元类之间的关系。

对象、类、元类关系图.png

isa的走向(用来找方法)
当一个对象的实例方法被调用时,会通过isa找到相应的类,然后在该类的class_data_bits_t中去查找方法,class_data_bits_t是指向类的数据区域,在数据区域查找相应方法的实现。对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
superclass的走向(用来找父类)

所以我们可以从图中看出(蓝色字体是我举的例子):

  1. 所有类(包括元类)的superclass的指针最终指向NSObject,NSObject没有父类,所以superclass指针指向nil。
    2.所有的isa指针最终指向NSObject元类,NSObject元类的isa指针指向自己,形成一个闭环。

2. 消息分发(如何发消息)

在 Objective-C 中的“方法调用”其实应该叫做消息传递。消息有“名称”和“选择子”,可以接受参数,而且可能有返回值。
给对象发送消息可以这样写:

id returnValue = [someObject messageName:paramter];

本例中,someObject叫做“接收者”,messageName叫做“选择子”,选择子和参数合起来叫“消息”。编译器看到此消息后,会将其转换成一条标准的C语言函数调用,所调用的函数是消息传递机制的核心函数objc_msgSend。

void objc_msgSend(id self,SEL cmd,...)

这是个参数可变的函数,第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数。编译器会把例子中的消息转换成如下函数。

objc_msgSend(someObject,@selector(messageName:),paramter);

objc_msgSend函数会依据接收者和选择子的类型来调用适当的方法。首先,需要在接收者所属的类中搜寻其方法列表,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿继承体系继续向上查找,等找到合适的方法再进行跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作(后续会说明)。
当然,每个类里面都有一块缓存来保存匹配的结果,这样稍后还想该类发同一消息就可以从缓存中获取。

/*  存储方法缓存的结构体  */
struct cache_t {
    struct bucket_t *_buckets;//用来缓存 bucket 的总数
    mask_t _mask;//目前实际占用的缓存 bucket 的个数
    mask_t _occupied;//一个散列表,用来方法缓存,包含 key 以及方法实现 IMP
};

/* 方法缓存的散列表 */
struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

objc_msgSend方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话直接通过函数指针 IMP ,找到方法的具体实现函数执行,没有的话才去搜寻其方法列表。如果有找到去执行,并将该方法保存在bucket_t中。
当然,objc_msgSend只能处理部分消息的调用过程,有的消息则需要其他函数来处理。比如:

  • objc_msgSend_stret(要返回结构体)
  • objc_msgSend_fpret(要返回浮点型)
  • objc_msgSendSuper(要给超类发消息)

3. 消息转发(接收到无法解读的消息怎么办)

在上面提到的发送消息过程中,选择子在当前类和父类中都没有找到实现,就会进入消息转发。
消息转发有两个阶段。
① 动态方法解析
征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个未知的选择子。对象在收到无法解读的消息后,会调用其所属类的以下方法:

//如果是对象方法调用这
+ (BOOL)resolveInstanceMethod:(SEL)sel;

//如果是类方法调用这
+ (BOOL)resolveClassMethod:(SEL)sel;

举个动态添加属性存取方法的例子:

id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selString = NSStringFromSelector(sel);
    if (/* selector is from  a @dynamic property */) {
        if ([selString hasPrefix:@"set"]) {
            /*
             * i:返回值类型int,若是v则表示void
             * @:参数id(self)
             * ::SEL(_cmd)
             * @:id(str)
             */
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        }else{
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

当接收者没动态添加方法处理该未知选择子时,会询问能不能把这条消息转给其他接收者处理(备胎接收者)。

//参数是选择子,返回值是备胎接收者,若找不到,就返回nil
- (id)forwardingTargetForSelector:(SEL)aSelector;

我们常用“组合”来模拟“多继承”。假如有个在student对象包含teacher,student无法处理的消息可以转移给teacher让它去处理。

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if ([_teacher respondsToSelector:aSelector]) {
        return _teacher;//备胎就是老师了
    }
    return [super forwardingTargetForSelector:aSelector];
}

②完整的消息转发机制
当以上方法没有实现响应,就会通过完整的消息转发机制来做了。
首先创建NSInvocation 对象,把尚未处理的那条消息相关的全部细节封装于其中。NSInvocation是命令模式的一种传统实现,它把一个目标,一个选择器,一个方法签名和所有的参数都塞进对象中。当NSInvocation被调用时,它会发送消息,OC运行时会找到正确的方法实现来执行。

NSMutableSet *set = [NSMutableSet set];
NSString *parameter = @"parameter";
SEL selector = @selector(addObject:);

NSMethodSignature *signature = [set methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:set]; //设置目标
[invocation setSelector:selector]; //设置选择子
[invocation setArgument:&parameter atIndex:2];//设置参数(0是目标,1是选择子,2后是参数)
//[invocation retainArguments];保留参数不被释放
[invocation invoke];//调用

其中,NSMethodSignature(方法签名)了一个方法的返回类型和参数类型,但不包括方法名。

//可以这样手动创建,但一般不这么用
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:"];

/*  正确的创建方法  */
SEL initSEL = @selector(init);
SEL allocSEL = @selector(alloc);

//从对象中获取实例方法签名
NSMethodSignature *initSig = [@"String" methodSignatureForSelector:initSEL];
//从类中获取实例方法签名
NSMethodSignature *initSig = [NSString instanceMethodForSelector:initSEL];

//从类中获取类方法签名
NSMethodSignature *allocSig = [NSString methodSignatureForSelector:allocSEL];

我们举个举个例子来说明,创建一个蹦床类来将消息弹给它的目标对象。

#import "Trampoline.h"
@interface Trampoline()
@property(nonatomic,strong)id  target;
@end

@implementation Trampoline
- (id)initWithTarget:(id)target{
    if (self = [super init]) {
        _target = target;
    }
    return self;
}

#pragma mark - 消息转发机制
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [self.target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //让目标对象去调用该方法
    [anInvocation setTarget:_target];
    [anInvocation retainArguments];
    //可以在主线程调用或直接调用
    [anInvocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
}

然后我们这样调用(当然前提是目标类能实现该方法):

id trampoline = [[Trampoline alloc] initWithTarget:student];
[trampoline doSomeThing];

不过,只改变调用目标,跟前面所说的备胎接收者所实现的方法等效。所以到这一步比较有用的实现方式是:在触发消息前,先以某种方式改变消息内容,比如追加另外的参数,或改变选择子等等。

以下这张流程图描述了转发机制处理消息的各个步骤。接收者在每一步中均有机会处理消息,步骤越往后,处理消息的代价越大。最好是在第一步就处理完,即使要把消息转给备援者,也要放在第二步较为简单,不用生成NSInvocation。


消息转发.png

我们来总结一下消息传递的所有流程(消息分发和消息转发):
① 检查对象是否为nil,如果是调用nil处理程序。
② 检查类缓存中是不是有方法实现了,如果找到,调用方法实现。
③ 比较请求的选择子和父类中定义的选择子,然后是父类的父类,以此类推,如果找到选择子,调用方法实现。
④ 调用resolveInstanceMethod:(或对应类方法)。返回YES,那么从新开始。这一次对象会找到这个选择子,因为已经经过class_addMethod。
⑤ 调用forwardingTargetForSelector:,如果返回非nil,就把消息发送到返回的对象上。不要返回self,会造成死循环。
⑥ 调用methodSignatureForSelector:,如果返回非nil,创建一个NSInvocation并传给forwardInvocation:。
⑦ 调用doesNotRecognizeSelector:,默认的实现是抛出异常。

三.Runtime的应用

runtime有很多api我们经常用到:
class

/* 修改类(以add开头) */
class_addIvar;
class_addProperty;
class_addMethod;
class_addProtocol;

/* 获取类中的所有内容(一般是数组)以copy开头*/
class_copyIvarList;
class_copyPropertyList;
class_copyMethodList;
class_copyProtocolList;

/* 获取类中的单个内容(以get开头)*/
class_getName;
class_getSuperclass;
class_getProperty;
class_getClassMethod;
    
/* 还有一些判断,替换,创建实例的方法*/
class_createInstance;
class_conformsToProtocol;
class_replaceMethod;
class_respondsToSelector;
objc
/* 获取设置实例变量 */
object_getIvar;
object_setIvar;

/* 获取设置类 */
object_getClass;
object_setClass;

ivar

ivar_getName;          //获取名字
ivar_getOffset;        //获取内存地址
ivar_getTypeEncoding;  //获取type encoding

property

property_getName;        //获取名字
property_getAttributes;  //获取所有属性信息(是否是atomic,getter/setter名字,是否弱引用)

method

method_getName;
method_getReturnType;

/* 方法交换常用 */
method_setImplementation;
method_getImplementation;
method_exchangeImplementations;

sel

sel_getName;    //获取名字
sel_registerName;  //注册方法

protocol
Protocols有点像Classes,运行时的方法是一样的。可以获取method, property, protocol列表, 检查是否实现了其他的protocol。

接下来,我们举几个runtime常见的应用:
① 方法交换(Method Swizzling)→交换IMP
② 动态继承、交换(ISA Swizzling)→改变ISA

方法交换(Method Swizzling)
我们知道了运行时会发消息给对象。一个对象的class保存了方法列表。class的方法列表其实是一个字典,key为selectors,IMPs为value,一个IMP是指向方法在内存中的实现。selector和IMP之间的关系是在运行时才决定的,而不是编译时。所以可以通过交换IMP来实现交换方法,从而达到重写某个方法而不用继承,同时还可以调用原先的实现的目的(面向切面编程的一个应用)。
我们先来看看一下Runtime中方法(Method)的构成,方法交换的本质就是交换Method里面的IMP指针。

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name;       //选择子(方法名)
    char * _Nullable method_types;  //该方法参数的类型
    IMP _Nonnull method_imp;        //函数指针
}          

接下来书写代码,将viewWillAppear:换成我们自己写的方法,同时也调用原来的方法,然后在方法中加入一些处理(比如监测、统计数据):

#import "UIViewController+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (MethodSwizzling)

+ (void)load{
    Method oldMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method newMethod = class_getInstanceMethod(self, @selector(yc_viewWillAppear:));
    method_exchangeImplementations(oldMethod, newMethod);
    
}

- (void)yc_viewWillAppear:(BOOL)animated{
    [self yc_viewWillAppear:animated];
    NSLog(@"添加监测代码");
}
@end
方法交换原理图.png

动态继承、交换(ISA Swizzling)
我们可以在运行时创建一个新的类,我们能通过它创建新的子类,并添加新的方法。

//动态创建对象(类也是对象)(创建一个Student类,继承NSObject)
Class Student = objc_allocateClassPair([NSObject class], "Student", 0);

//使用Block作为方法的IMP
IMP myIMP = imp_implementationWithBlock(^(id _self,NSString *string){
    NSLog(@"Hello %@",_self);
});
//为该类增加report的方法
class_addMethod(Student, @selector(report), myIMP, "v@:");

//注册该类
objc_registerClassPair(Student);

//创建student对象
id student = [[Student alloc] init];
[student report];

但是能这个动态创建子类有什么用呢?object内部有一个叫做isa的变量指向它的class。我们可以改变一个实例的isa指向,让它指向我们创建的类。可以通过以下命令来修改一个object的class:

object_setClass(myObject, [MySubclass class]);

我们来举个例子:

#import "MyNotificationCenter.h"

@implementation MyNotificationCenter
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName object:(id)anObject{
    NSLog(@"adding observer:%@",observer);
    [super addObserver:observer selector:aSelector name:aName object:anObject];
}
@end
#import "NSObject+ISASwizzle.h"
#import <objc/runtime.h>

@implementation NSObject (ISASwizzle)
- (void)yc_setClass:(Class)aClass{
    NSAssert(class_getInstanceSize([self class]) == class_getInstanceSize(aClass), @"Classes must to be same size to swizzle");
    object_setClass(self, aClass);
}
@end
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center yc_setClass:[MyNotificationCenter class]];
[center addObserver:self selector:@selector(test) name:@"test" object:nil];

这样在该类中的center实例就变成我们自己的实例,可以在添加观察者或调用其他方法时加入我们自己的代码。Cococa框架中的KVC也是基于此原理,当你开始观察一个object时,Cocoa会创建这个object的class的subclass,subclass重写监听属性的set方法,然后将这个object的isa指向新创建的subclass,从而属性改变时都会调用我们创建的subclass的该属性的set方法。
有一点需要注意的是,我们创建的子类大小要和原本的类大小一致,也就是说不能生成ivar或属性,因为被混写的对象已经分配好,如果添加ivar,那么它们就会指向已分配内存外的区域,那么很容易覆盖内存中这个对象后面对象的isa指针。

我们来总结一下方法交换和ISA交换的特点,以便清晰它们的使用范围:

方法交换
  • 影响一个类的所有实例
  • 所有对象的类都不变
  • 需要特殊的覆盖方法来实现
ISA交换
  • 只影响目标实例
  • 对象的类会变化
  • 覆盖方法是用子类的方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容