Runtime详解

前言

上面文章主要讲了iOS中类、实例、和元类的概念以及在runtime中是如何定义以及之间的联系,这篇文章接着把runtime的整个概念给讲清楚,包括runtime在实战中的应用。

各个数据项的结构体定义

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父类
     const char *name                         OBJC2_UNAVAILABLE;  // 类名
     long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
     long info                                OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
     long instance_size                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定义的链表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法缓存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 协议链表
#endif
}OBJC2_UNAVAILABLE;

上篇文章我们讲了isa指针是如何连接实例、类、和元类的,以及super_class指向的什么,其实这些上面都注释的很清楚,接下来就来说说下面几个数据项的定义以及应用。

至于定义,比如成员变量链表objc_ivar_list以及方法列表objc_method_list的结构体定义都可以在runtime.h中找到,这里就不在重复展示了,具体到用的时候再去逐个分析。

SEL : 又叫选择器,是表示一个方法的selector的指针,其定义如: typedef struct objc_selector *SEL;
方法的selector用于表示运行时方法的名字, 编译器会根据方法的名称和参数序列生成唯一的一个标识符 SEL(int类型的一个地址),这也是不允许方法重名的原。

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。 通过下面三种方法可以获取SEL

  • sel_registerName函数
  • Objective-C编译器提供的@selector()
  • NSSelectorFromString()方法

IMP : 实际上是一个函数指针,指向方法实现的地址。id (*IMP)(id, SEL,...)
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针) 第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。

Method : 表示类定义中的方法

typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
}

方法调用的流程

oc中方法调用会有一个中间转换,转换为一个c函数才能在被runtime执行

[objc methodName] ==> objc_msgSend(id self, SEL op, ...)

objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象不为空的时候,就去方法缓存列表(objc_cache)里面找,找到了就分发,否则:

  1. 如果没找到就要去方法列表里找objc_method_list,如果找到就缓存起来
  2. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector 否则
  3. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
  4. 进行消息转发

方法缓存的设计

缓存方法是苹果在代码设计上的一个方法,很好的避免了大量调用方法造成的性能消耗。

struct objc_cache {
    //可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    //实际占用bucket 因为缓存采用的是散列表,所以会有空的bucket
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    //指向Method数据结构指针的数组,这个数组的总数不能超过mask+1,数组会随着时间增长。
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};

问:方法的缓存全部放到struct objc_cache *cache这里,那么我们调用父类的方法的时候会把方法缓存到子类中吗?
答:会缓存。而当用一个父类对象去调用那个方法的时候,也会在父类的cache里缓存一份.

问:为什么方法列表不做成哈希表,而做成list,然后单独再创建缓存哈希表?
答:散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。另一个原因是哈希表是有空位的浪费空间。

消息转发

既然上面提到了消息转发那就放到这里讲一下到底消息是如何转发的。

这里我们先明白一个概念就是,消息转发是当一个消息在运行时发送的过程中找不到目标方法的实现地址时候,系统做的一个不就措施,这个措施总共分三个步骤:

  • 动态方法解析

+ (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod: 这两个方法作用一样,区别是一个处理实例方法,一个处理类方法

实例:

void spareMethod(id obj, SEL _cmd)
{
    NSString * className = NSStringFromClass([obj class]);
    NSString * selName   = NSStringFromSelector(_cmd);
    NSLog(@"%@:没有实现%@的方法",className,selName);
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(sendMessage:)){
        class_addMethod([self class], aSEL, (IMP)spareMethod, "v@:"); //动态增加方法
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

顺便贴一个方法参数的签名含义:

*          代表  char * 
char BOOL  代表  c
:          代表  SEL 
^type      代表  type *
@          代表  NSObject * 或 id
^@         代表  NSError ** 
#          代表  NSObject 
v          代表  void
  • 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *selStr = NSStringFromSelector(aSelector);
    
    if ([selStr isEqualToString:@"sendMessage:"]) {
        return [[Other alloc] init];        // 这里返回Other类对象,让Other去处理sendMessage消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
  • 完整转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *sel = NSStringFromSelector(aSelector);
    // 判断要转发的SEL
    if ([sel isEqualToString:@"sendMessage:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; //必须要有方法签名
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector = [anInvocation selector];
    // 新建需要转发消息的对象 这里可以创建多个对象,同时转发
    Other *obj = [[Other alloc] init];
    if ([obj respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:obj];
    }
}

完整转发有时候可以让object-c 拥有多继承能力.

在一个函数找不到时,OC提供了三种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  2. 调用forwardingTargetForSelector让别的对象去执行这个函数
  3. 调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

如果都不中,调用doesNotRecognizeSelector抛出异常。

Category类别又是如何定义和实现的

这里顺便提一下category和extension的区别:

  • extension更像是一个匿名的category,但是和有名字的category是完全两个东西,extension一般用来隐藏类的私有信息,在编译期间是和class一起编译的,是class不可分割的一部分,你必须有一个类的源码才能为一个类添加extension
  • category完全是运行期决定的,但是category是不能添加成员变量和属性的(增加属性可通过runtime技术实现),因为运行期class的布局已经确定,添加成员变量会破坏class的内存布局。category中定义的方法如果和class同名优先级高于class中定义的方法
//可以看到类别中没有 ivar 字段 也就是不能添加成员变量。
typedef struct objc_category *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; // 分类所实现的协议列表
}

runtime 实战应用

方法交换(Method Swizzling)

方法交换一定要在+load()方法中执行,确保在类初始化中一定能执行,并且搭配dispatch_one 防止多线程重复执行

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(methodA);
        SEL swizzledSelector = @selector(xxx_methodA);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

通过方法交换的这种黑魔法你能够实现hook一些系统的方法,实现中间件的功能

给category添加属性

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

我们知道category是不能添加成员变量和属性的,但是通过上面runtime方法就能实现增加属性(其实还是增加set和get方法),至于怎么使用就实际开发中的场景需求了

给OC语言实现多继承

目前网上实现好多文章给出实现多继承的方案无非就三种:

  • 通过组合实现
  • 通过协议
  • 通过category
  • 消息转发机制

这里要说的就是上面我们刚介绍过的消息转发,大致思路是:

假如我们想让C继承自A和B,理论上我们是无法实现的,但是我们可以先让C继承自A,然后C就可以使用A公开的方法了,然后B公开的方法我们C对象不能调用,但是可以再C类里面,通过消息转发来把方法转发给B,让B去实现,从而就实现了C不但可以调用A类的方法,调用B类的方法时,B依然可以响应,不就实现了多继承吗。具体代码其实还是上面消息转发的实现代码,在消息转发中吧处理方法的对象换成你想要的对象就行了(比如B)**

模型转换

其实模型转换和自动化归档这些小技巧也都是通过runtime的强大功能实现的,可以节省很多冗余代码,让代码更简练。具体实现可以翻看写实现这种类库的源码。

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

推荐阅读更多精彩内容

  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,134评论 0 9
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 简介 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 O...
    专业男神经阅读 906评论 0 2
  • 简介 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 O...
    郑军红阅读 591评论 1 2
  • 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门...
    DanieX阅读 16,363评论 7 16