Objective-C中的Runtime(三) 文章总结

简要简介

Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和转发。

当执行[object doSomething]会被编译器转化为:objc_msgSend(receiver, selector) 如果消息含有参数,则为,objc_msgSend(receiver, selector, arg1, arg2, ...)

如果消息的接受者能够找到对应的selector,那么就直接执行这个方法;否则,消息要么被转发,或者临时向接受者动态添加这个selector对应的实现内容;如果最后没有找到任何解决办法,就干脆崩溃掉。

最后,可以看出[object doSomething]真不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接受者发送doSomething这条信息,而object如何响应这条信息,那就要看运行时发生的情况来决定了。

iOS RunTime之二:数据结构
objc_class、元类(Meta Class)、SEL、IMP、Method、Ivar

SEL:Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个唯一的整型标识(int类型的地址)这个标识就是SEL。在本质上,SEL只是一个指向方法的指针(被hash化得KEY值),能提高方法的查询速度。

IMP:就是Implementation的缩写,本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

Method

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

方法名 类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
method_imp指向了方法的实现,本质上是一个函数指针。

Ivar:是一种代表类中实例变量的类型。

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
}

Cache:是一个存储Method的链表,主要是为了优化方法调用的性能。

iOS RunTime之三:消息发送

objc_msgSend它具体是如何发送消息:

  • 检查selector是否需要忽略。
  • 检查target是否为nil。如果为nil,直接cleanup,然后return。这就是我们可以向nil发送消息的原因。
  • 然后再target的Class中根据Selector去找IMP。
  • 寻找IMP的过程:
    1、先从当前class的cache方法列表(cache methodLists)中去找;
    2、找到了,就调到对应函数实现;
    3、没找到,没就从class的方法列表(methodLists)中找;
    4、还没找到,就到super class中重复上述步骤找,直到找到基类(NSObject)为止
    5、最后再找不到,就会进入动态方法解析和消息转发的机制。

理解:
Objective-C的动态绑定:消息的发送其实就是先确定object接受者对象,然后根据isa指针查找其方法然后跳转过去并执行。但是编译期间,是无法确定object接受者对象。只有在程序运行期间,object接受者对象才能得到确定。<u>这种在运行期间才确定object接受者对象,Objective-C称为动态绑定</u>。

C++或者Java调用对象的函数,函数与对象之间的关系,在编译期间就必须严格确定。

消息发送的机制使得在不重新编译的情况下,在运行期间,干预或者说hook原来的target(方法、变量等)变得更易于实现,更有实际应用价值,这个是需要依赖于消息发送和动态绑定的实现机制——Runtime。

iOS RunTime之四:消息转发

消息发送和消息转发流程可以概括为:消息发送是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;<u>消息转发是在查找 IMP 失败后执行一系列转发流程的慢速通道</u>,如果不作转发处理,则会打日志和抛出异常。

消息转发过程:

1、动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

举例:
void dynamicMethodIMP(id self, SEL _cmd){
    //实现...
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL{
    if(aSEL == @selector(resolveThisMethodDynamically)){
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

2、重定向

- (id)forwardingTargetForSelector:(SEL)aSelector;

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

- (id) forwardingTargetForSelector:(SEL)aSelector{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

3、转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

实现转发简单代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel isEqualToString:@"fly"]) {
        //signatureWithObjcTypes 手动生成签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector: aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    Car *car = [[Car alloc] init];
    if([car respondsToSelector:[anInvocation selector]]){
        [anInvocation  invokeWithTarget:car];
    }
}

1、methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。

2、unrecognized selector sent to instance,原来就是因为methodSignatureForSelector这个方法中,由于没有找到fly对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。

消息转发流程简图

iOS RunTime之五:Category不能动态添加成员变量

  • 为什么Category中不能动态添加成员变量?

在runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是阅读过苹果的官方文档的人应该会看到:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

大概的意思说,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。

  • 为什么不能为一个类动态的添加成员变量,可以给类动态增加方法和属性?

因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。

iOS RunTime之六:Category

代码介绍:

在runtime.h中查看定义:

typedef struct objc_category *Category;

是一个objc_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;
}        

objc源代码,在objc-runtime-new.h中我们可以发现:

struct category_t { 
    const char *name; //是指 class_name 而不是 category_name
    classref_t cls;  //要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
    struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;//category中所有给类添加的实例方法的列表
    struct protocol_list_t *protocols;//category实现的所有协议的列表
    struct property_list_t *instanceProperties;
    //表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject
    //和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
};

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Category和Extension的区别
1、Extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension一般用来隐藏类的私有信息,你必须有一个类才能为这个类添加Extension,所以你无法为系统的类比如NSString添加Extension。
2、Category则完全不一样,它是在运行期决议的。
3、Extension可以添加属性、成员变量,而Category一般不可以。
总之,就Category和Extension的区别来看,Extension可以添加实例变量,而Category是无法添加实例变量的。因为Category在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。

面试题
一般面试官有时候会问到这样的问题:
在类和Category中都可以有study方法,那么有两个问题:

  • 在类的study方法调用的时候,我们可以调用Category中声明的study方法么?
  • 如果一个类有多个分类的时候study方法,调用顺序是咋样的呢?

答:study方法的执行顺序是先类,后Category,而Category的study方法执行顺序是根据编译顺序决定的。

Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,159评论 0 7
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 719评论 0 2
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 788评论 0 4
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 901评论 0 6