ios高级-runtime详解

Objective-C runtime介绍
简介
Objective-C是一门运行时语言,这意味着代码执行可以更加灵活:我们动态的创建一个新的类,还可以转发消息给其他的消息。(消息转发是runtime的一个重要组成部分,后面会介绍)。

这种特性要求Objective-C语言会尽可能把决定从编译和链接的阶段延迟到运行时(runtime)阶段,因此Objective-C不仅有一个编译器,还有一个runtime系统来执行被编译过的代码。

版本和平台
runtime是有个两个版本的: legacy 、 modern
在Objective-C 1.0使用的是legacy,在2.0使用的是modern。这里简单介绍下区别:

在legacy runtime,如果你改变了实例变量的设计,需要重新编译它的子类。支持 32bit的OS X 程序
在modern runtime,如果你改变了实例变量的设计,不需要重新编译它的子类。支持iphone程序和OS X10.5之后的64bit程序
因为legacy是如此的古老,我们基本可以忽略legacy版本。

Runtime交互
有三种方式可以使用Runtime:

Objective-C 源代码
NSObject 方法
Runtime 方法
Objective-C 源代码
一般来说,runtime都是默默地在后台运行工作,我们只是写Objective-C源代码,就使用了runtime。当我们编译包含Objective-C的类和方法时,编译器就会生成包含runtime特性的数据结构和方法。数据结构中包含了从类、category、协议中定义的的信息,如:selector、变量等等。主要的runtime方法是发送信息的方法Message,在下一章节会讲到。

NSObject方法
Cocoa中大部分的类都是继承自NSObejct,所以他们都会集成了NSObject定义的方法。(值得注意的例外是NSProxy类)因此NSObject的方法就决定了其他类的行为。(当然,在少数情况下,这样说并不正确,在这些情况下,NSObject会仅仅定义了方法,没有实现必须的代码。比如description,如果子类没有重写,那么会返回类名和地址,这是因为NSObject没法获得更多的信息。)

一些NSObject类的方法可以直接查询runtime系统的信息,从而获得自身的信息。比如class,isKindeOfClass,respondsToSelector等方法。

Runtime方法
Runtime系统是一个共享的library,包含了许多方法和数据结构,地址在/usr/include/objc。其中的很多方法,允许你使用C来重写编译行为,其他的方法是通过NSObject类使用。有了这些方法,我们就可以为runtime系统增加接口,或者工具。

Message
先抛出来一个问题,这句话代表什么?

[receiver message]

receiver执行message函数? 是这个作用,但是more than that,等价于这行代码

objc_msgSend(receiver, @selector(message))
1
所以其实message是iOS中非常重要的一环,尤其是在动态绑定中。下面我们具体看下:

objc_msgSend
上面讲了,iOS中的函数调用其实是给实例发送了message,有参数的函数其实执行了:

objc_msgSend(receiver, selector, arg1, arg2, ...)
1
objec_msgSend的方法定义如下:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
1
那么,消息转发的过程究竟发生了什么呢?
我们先看下这里用到的三个类:对象(object),类(class),方法(method)

//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

if !OBJC2

Class super_class                                        OBJC2_UNAVAILABLE;
const char *name                                         OBJC2_UNAVAILABLE;
long version                                             OBJC2_UNAVAILABLE;
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;

//方法列表
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_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
系统首先找到消息的接收对象,然后通过对象找到它的类。
在它的类中查找method_list,是否有selector方法。
没有则查找父类的method_list
找到对应的method,执行它的IMP
转发IMP的return值
selector和IMP、method等的区别,可以参考我的另一篇博客Method,SEL,IMP

注意:编译器会生成messaging方法,所以你永远都不应该手动调用这个方法。

dispatch table
messaging的关键在于编译器给每个类创建的数据结构,每个类的数据结构都包含两个要素:

执行父类的指针
一个类分发表(dispatch table)。表中有所有的相关方法和这些方法的地址和id。
runtime系统,要求对象必须等价于objc_object,NSObject和NSProxy都自动包含isa属性。

结构和原理如图所示:

当消息被发送给对象的时候,消息就按照上图的路径寻找对应的selector,直到达到NSObject,如果在NSObject中还是没有找到对应的方法,则会走到消息转发机制中,下文会介绍。

cache
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,runtime系统实际上非常快,接近直接执行内存地址的程序速度。

Message Forwarding
如果在dispatch table中没有找到对应的method呢? 系统仍然会给你补救的机会:

resolveInstanceMethod/resolveClassMethod
fast forwarding
normal forwarding
resolveInstanceMethod
系统没有在dispatch_table中找到对应的方法,会看你是否重写了resolveInstanceMethod,resolveClassMethod,这里两个方法是否用来添加动态方法的,一个是实例方法,一个是类方法。

举个例子:我们有个ClassA

import <Foundation/Foundation.h>

@interface ClassA : NSObject

@end

@implementation ClassA

@end
1
2
3
4
5
6
7
8
9
希望执行ClassA并不存在的foo方法

ClassA *a = [ClassA new];
[a performSelector:@selector(foo)];
1
2
系统会直接报错:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ClassA foo]: unrecognized selector sent to instance 0x600000004e00'
1
如果我们想用resolveInstanceMethod来补救,该怎么做呢?

include <objc/runtime.h>

void foo(id self, SEL _cmd) {
NSLog(@"resolveInstanceMethod add method foo ");
}

@implementation ClassA

  • (BOOL)resolveInstanceMethod:(SEL)sel
    {
    if (sel == @selector(foo)) {
    class_addMethod([self class], sel, (IMP)foo, "v@:");
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    注意:一定要记得import <objc/runtime.h>,否则会报错:class_addMethod is valid in C99

执行下,log:

resolveInstanceMethod add method foo
1
这里的return YES 或者 return NO,是告诉系统是否实现了这个方法,如果return YES,但是并没有增加方法,还是会报错,并且不会走到forward,因为系统默认你已经在这一步做了resolveInstanceMethod这个事情。

forwardingTargetForSelector
如果上一步骤的resolveInstanceMethod return no,系统会走forwardingTargetForSelector,这一步被称为快速转发,是因为相对下面要介绍的normal fastward,这一步直接转发了消息,而normal fastward生成了NSInvocation,相对直接转发慢一些。

先看下如何实现,比如,我想把消息转发给有能力的classB:

@interface ClassB : NSObject

  • (void)foo;

@end

@implementation ClassB

  • (void)foo
    {
    NSLog(@"ClassB foo run");
    }
    @end
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    A中需要实现forwardingTargetForSelector方法:

  • (id)forwardingTargetForSelector:(SEL)aSelector
    {
    if(aSelector == @selector(foo)){
    ClassB *b = [ClassB new];
    return b;
    }
    return [super forwardingTargetForSelector:aSelector];
    }
    1
    2
    3
    4
    5
    6
    7
    8
    run一下,log:

ClassB foo run
1
苹果的文档里,讲述了这一个消息转发的出发点,其实是为了实现类似C多继承的功能。我们知道,在C中如果一个类想要具有多个类的功能,是可以直接继承多个类的。而Objective-C是单继承,如果想实现类似的功能,就用消息转发,将消息转发给有能力处理的类。苹果是这样描述他们的思想的:C的多继承,是加法,在多继承的同时,其实也增加了很多不需要的功能,而苹果通过消息转发,实现了减法的思想,只留有用的方法,而不去增加过多内容。

forwardInvocation
如果你的类没有实现forwardingTargetForSelector方法,系统会调用methodSignatureForSelector方法,如果这个方法返回一个函数的签名,则执行forwardInvocation方法,否则执行doesNotRecognizeSelector。

如果希望在这一步补救,如何做呢?

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [ClassB instanceMethodSignatureForSelector:aSelector];
    }

  • (void)forwardInvocation:(NSInvocation *)invocation
    {
    SEL sel = invocation.selector;
    ClassB *b = [ClassB new];

    if([b respondsToSelector:sel]) {
    [invocation invokeWithTarget:b];
    }
    else {
    [self doesNotRecognizeSelector:sel];
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    流程图
    我自己画了个消息转发的流程图:

其他
隐藏参数
刚才讲了,在一个对象执行一个函数的时候,其实是:

objc_msgSend(receiver, selector, arg1, arg2, ...)
1
那其实在函数中,receiver和selector是两个隐藏的参数,这两个参数是可以使用的。

  • (void)run
    {
    [self performSelector:_cmd]; //self: 当前对象 _cmd : "run"
    }
    1
    2
    3
    4
    获取method的地址
    如果你要连续执行同一个method,但是觉得每次都要遍历一遍分发表会效率低,可以直接获取地址(methodForSelector),然后直接执行函数.

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
1
2
3
4
5
6
7
个人觉得,这样意义不大,因为其实系统会做缓存。

runtime实际应用
runtime的应用,主要有几种:

AOP,切面编程,做打点
method swizzling,黑魔法做崩溃等的保护

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

推荐阅读更多精彩内容