iOS基础篇(二) Runtime详解

前言

之前一篇文章里,我详细的讲解了一些基本关键词以及基本概念,这里再简要列出来,再温故一下。

  • SEL 方法的名字,可以理解为字符串指针类型
  • id 指向一个类的实例对象
  • isa 每个类的示例对象都保存的指针,指向类对象
  • Class 指向类对象
  • _cmd 每个OC方法都具有的参数

什么是Runtime?

Objective C语言把能在编译期做的事情就推迟到运行期再决定。这就意味着,Objective C不仅需要一个编译器,而且需要一个运行期环境。这个运行期环境就是Runtime。

Runtime是用C和汇编写的,并且它是开源的。所有的Apple开源代码都可以在http://www.opensource.apple.com上找到。

Objective C中的实例方法

在OC中,方法调用称为向对象发送消息,为什么这么叫呢?我们先看个例子

[receiver message]

那么,[receiver message]编译后是什么呢?
编译后是这个样子的

objc_msgSend(receiver, selector)

我们看看这个方法的文档

id objc_msgSend(id self, SEL op, ...)

参数

  • self 消息的接收者
  • op 消息的selector,一个C的字符串用来定位
  • … 消息传入参数的数组

Runtime如何找到实例方法执行体?

上文提到了,一个OC方法被编译成objc_msgSend,那么,Runtime如何找到方法的执行体呢?

因为OC中存在一种对象叫做类对象(Class Object),类对象中保存了方法的列表,superClass等信息。

类对象的定义

typedef struct objc_class *Class;

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 **methodLists                    OBJC2_UNAVAILABLE;

那么,OC的实例对象又是如何找到类对象呢?从上文的第一部分,我们可以看到,objc_msgSend这个C函数输入参数包括,id类型的self,SEL类型的_cmd,以及参数列表。很直观,id类型中一定存在一个可以找到类对象的指针。我们来看看id定义

typedef struct objc_object *id;
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
}

现在很清楚了,实例对象通过isa找到类对象。
我们知道了

  • OC的对象通过isa找到类对象
  • 类对象查找自己存储的方法列表来找到对应的方法执行体
  • 方法执行体执行具体的代码,并返回值给调用者。

那么,实际的方法执行体(Method)到底是什么东西?
看看Method的定义

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

Method保存了

  • 方法的名字
  • 方法的类型(参数列表)
  • 方法的具体实现,由IMP指向

其中IMP的定义

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

我们来看一个例子,
定义一个

@interface CustomObject : NSObject
-(NSString *)returnMeHelloWorld;
@end
@implementation CustomObject

-(NSString *)returnMeHelloWorld{
    return @"hello world";
}
@end

我们先只看调用这一行

 NSString * helloWorld =  [obj returnMeHelloWorld];
  • 编译成如下id objc_msgSend(self,@selector(returnMeHelloWorld:));
  • 在self中沿着isa找到CustomObject的类对象
  • 类对象查找自己的方法list,找到对应的方法执行体Method
  • 把参数传递给IMP实际指向的执行代码
  • 代码执行返回结果给helloWorld

类方法又是如何处理的?

上文提到了,实例方法,那么类方法又是如何处理的呢?

由上文我们知道,实例对象由类对象创建,类对象保存了实例对象的方法列表。

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;

可以看到,这一行

Class isa  OBJC_ISA_AVAILABILITY;

这个isa指向的一个Class类型,就是保存了类方法的地方。这个Class类型的东西就是类元对象

类对象由类元对象(meta class)创建,类元对象保存了类对象的类方法,类名字等信息。

类元对象和类对象都是Class类型,只不过服务的对象不同罢了。
到这里,有同学可能要问了,类元对象由什么创建呢?

答案: NSObject类元对象

那么NSObject类元对象由什么创建呢?(就喜欢刨根问底)

答案: NSObject类元对象自身

我们来看一个例子,验证下这一些列的讲解,也加深一下印象

 Class class = [CustomObject class];//类对象

    Class metaClass = object_getClass(class);//类元对象
    Class metaOfMetaClass = object_getClass(metaClass);//NSObject类元对象
    Class rootMataClass = object_getClass(metaOfMetaClass);//NSObject类元对象的类元对象

    NSLog(@"CustomObject类对象是:%p",class);
    NSLog(@"CustomObject类元对象是:%p",metaClass);
    NSLog(@"metaClass类元对象:%p",metaOfMetaClass);
    NSLog(@"metaOfMetaClass的类元对象的是:%p",rootMataClass);
    NSLog(@"NSObject类元对象%p",object_getClass([NSObject class]));

可以看到Log

CustomObject类对象是:0x10248aed0
CustomObject类元对象是:0x10248aea8
metaClass类元对象:0x102ce5198
metaOfMetaClass的类元对象的是:0x102ce5198
NSObject类元对象0x102ce5198

这个可以作图为


看看SuperClass

在面相对象中,我们知道,子类调用一个方法,如果子类没有实现,会查找基类。OC作为一种面相对象的语言,当然支持这些。
我们依然写一段示例代码

Class class = [CustomObject class];//类对象
    Class superClass = class_getSuperclass(class);//基类对象NSObject
    Class superOfNSObject = class_getSuperclass(superClass);//NSObject类元对象

    NSLog(@"CustomObject类对象是:%p",class);
    NSLog(@"CustomObject类superClass是:%p",superClass);
    NSLog(@"NSObject的superClass是:%p",superOfNSObject);

可以看到log

CustomObject类对象是:0x101792e88
CustomObject类superClass是:0x101fec170
NSObject的superClass是:0x0

由此,我们可以绘制继承关系图(图中nil的地址不对,不重新绘制了)



综合上述两个例子,我们绘制出完整的isa和superClass图


提高方法查找机制效率-Class Cache

通过上文我们知道,在OC中,方法是通过isa指针,查找Class中的Method list来查询的。而一个类往往会实现很多方法,每次调用都查询一次Method list的分发表(dispatch table)的代价是很高的(因为,这种查询可能每个RunLoop就执行上亿次)。这也就引入了Class Cache.

Class Cache认为,当一个方法被调用,那么它之后被调用的可能性比较大。

举个例子,我们常见的alloc,init,调用顺序如下

 CustomObject * obj = [[CustomObject alloc] init];
  • alloc是类方法,沿着isa找到CustomObject类元对象,发现没有实现alloc
  • 沿着super,找到NSObject类元方法,执行alloc方法,并把alloc加入到NSObject类元对象的Class Cache中
  • init是实例方法,沿着isa找到CustomObject的类对象,发现没有实现init
  • 沿着super,找到NSObject类对象,执行init,并把init加入到NSObject的类对象Class Cache中

这里,再提一下alloc,init两个方法

初始化isa,其他所有属性被设为0

init

NSObject的init返回self,其余的子类要调用[super init]进行必要的初始化工作。

为什么要放在一起写?

因为alloc和init有可能返回不同的对象

 id a = [NSMutableArray alloc];
    id b = [a init];
    NSLog(@"%p",a);
    NSLog(@"%p",b);

输出
0x7fc550400fb0
0x7fc5505523a0

来看看为什么绝大部分对象都要继承自NSObject?

NSObject根类除了定义了一些基本的方法,例如,description,alloc。也定义了一些Runtime的基础方法,这里把一些不常用的列出来。并且,从上文的isa和superclass图来看,NSObject是整个消息机制的核心。

 //两个Runtime的接口-这个之后在method Swizzling中详细讲解
+initialize //在一个类接收第一条消息之前的
+load //在一个类对象加载到Runtime的时候调用
//检查是否可以向实力对象发送某消息
+(BOOL)instancesRespondToSelector:(SEL)aSelector
-respondsToSelector:

//向对象发送消息
- (id)performSelector:(SEL)aSelector
- performSelector:withObject:
- performSelector:withObject:withObject:
...
//动态消息转发处理机制
+resolveInstanceMethod:
- forwardingTargetForSelector:
- forwardInvocation:

到现在觉得这些函数熟悉吗?不熟悉,我们去查看一下NSObject的文档,里面的方法很详细。

假如一个对象没办法处理实例方法怎么办?

这里,我们再一次抢到,Objective C的方法就是前两个参数是self和_cmd的C方法

void ocMethod(id self, SEL _cmd) {
    // implementation ....
}

在Objective中,对一个对象发送一个它没有实现的Selector是完全合法的,这样做可以隐藏某一个消息背后实现,也可以模拟多继承(OC不支持多继承)。这个机制就是动态转发机制。

主要包括三个函数

+resolveInstanceMethod:
- forwardingTargetForSelector:
- forwardInvocation:

动态方法的机制第一步,类自己处理

使用resolveInstanceMethod
这个方法的文档

动态为实例方法提供一个实现
这个方法在Objective C消息转发机制之前被调用。如果 respondsToSelector或者instancesRespondToSelector: 被调用,可以为改Selector提供动态的实现者。

举个例子

#import "ViewController.h"
#import <objc/runtime.h>

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    [self performSelector:@selector(dynamicSelector) withObject:nil];
#pragma clang diagnostic pop

}

void myMehtod(id self,SEL _cmd){
    NSLog(@"This is added dynamic");
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (sel == @selector(dynamicSelector)) {
#pragma clang diagnostic pop
        class_addMethod([self class],sel, (IMP)myMehtod,"v@:");
        return YES;
    }else{
        return [super resolveInstanceMethod:sel];
    }
}

@end

简单介绍下,class_addMethod这个函数

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
1

作用,为一个类添加实例方法
参数

  • cls 想要添加的类对象
  • name 添加后的方法Selector名字
  • imp 具体的方法实现
  • types 方法参数的编码,编码方式见文档

返回值

  • YES 表示本类已经能够处理,NO表示需要消息转发机制。

第二部(第一步不能处理的情况下),调用forwardingTargetForSelector来简单的把执行任务转发给另一个对象,到这里,还是廉价调用
查看代码

#import "ViewController.h"
#import <objc/runtime.h>

@interface CustomObject : NSObject
-(void)dynamicSelector;
@end
@implementation CustomObject

-(void)dynamicSelector{
    NSLog(@"hello world");
}
@end
@interface ViewController ()
@property (strong,nonatomic)CustomObject * myObj;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.myObj = [[CustomObject alloc] init];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    [self performSelector:@selector(dynamicSelector) withObject:nil];
#pragma clang diagnostic pop

}

-(id)forwardingTargetForSelector:(SEL)aSelector{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (aSelector == @selector(dynamicSelector) && [self.myObj respondsToSelector:@selector(dynamicSelector)]) {
        return self.myObj;
    }else{
        return [super forwardingTargetForSelector:aSelector];
    }
#pragma clang diagnostic pop

}
@end

第三步,当前两步都不能处理的时候,调用forwardInvocation转发给别人,返回值仍然返回给最初的Selector
我们看代码

#import "ViewController.h"
#import <objc/runtime.h>

@interface CustomObject : NSObject
-(void)dynamicSelector;
@end
@implementation CustomObject

-(void)dynamicSelector{
    NSLog(@"hello world");
}
@end
@interface ViewController ()
@property (strong,nonatomic)CustomObject * myObj;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.myObj = [[CustomObject alloc] init];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    [self performSelector:@selector(dynamicSelector) withObject:nil];
#pragma clang diagnostic pop

}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    if ([self.myObj respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:self.myObj];
    }else{
        [super forwardInvocation:anInvocation];
    }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [CustomObject instanceMethodSignatureForSelector:aSelector];
}
@end

小结

消息转发机制使得OC可以进行”多继承”,比如有一个消息中心负责处理消息,这个消息中心很多个类都要用,继承或者聚合都不是很好的解决方案,使用单例看似可以,但单例的缺点也是很明显的。这时候,把消息转发给消息中心,无疑是一个较好的解决方案。

实际开发中Runtime如何应用?

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,135评论 0 9
  • 今天有心情补记参加表弟婚礼的随感啦! 表弟是我小舅的儿子,小舅只比我大了两岁,打娃子班出身就跟在姨父身边。娶小舅妈...
    凡阳nihao阅读 344评论 0 2
  • 人生规划 请用好你的英语和口语以及相关的写作能力 想想自己来上海已经有一年半的时间了,但是你的生活圈子基本上都是单...
    deep7blue阅读 269评论 0 1
  • 前言 最近在Github上找到一个中国城市的json文件,虽然也有db文件,但是想通过这个json文件生成自己的数...
    Goach阅读 1,635评论 0 0