细说objc中的Runtime

前言

这篇文章讲解了一些runtime的基本知识,需要有一定的objc基础及开发经验。为了更好的阅读体验,推荐戳我的博客阅读。

从理解发送消息讲起

为什么objc叫消息发送?为什么不叫函数调用?在objc中,发送消息仅仅表示一种行为 ,不能理解为像C语言中那样的函数调用。原因就是在发送消息的背后,runtime帮我们做了非常多的事情。这样是objc能真正成为一门动态语言的真正原因。


要学习runtime所要掌握的几个基本概念

在开始学习runtime之前,有几个基本的概念是必须要了解的:

SEL

SELselectorobjc中的表示类型,selector是方法选择器,可以理解为方法的ID。而这个ID的数据结构是SEL

typedef struct objc_selector *SEL

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id

那么objc_object又是啥呢:

struct objc_object {
    Class isa;
};

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_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;
  • 类对象的概念在此不再表述,只说一些之前自己不懂的部分,你也可以查看我的这篇深入理解objc中的对象与类博客:

    1. 类对象的isa指针指向的是类对象的元类,每个类对象都有自己的元类。
    2. 类对象里存放时的实例对象的对象方法,属性,协议列表等信息。注意objc_cache *cache这个东西,存的是匹配信息,比如消息来到时该对象能不能处理此消息,方法对应的实现等都收纳在此中。
    3. 只有类对象才有super_class这个指针。
    4. NSObjectsuper_class指针指向nil
  • 关于元类的概念:

    1. 元类是类对象的类对象,类对象的对象方法(即实例对象的类方法)列表就存在此处。
    2. 所有元类的isa指针指向NSObject的元类,即根元类。super_class指针指向NSObject

如下图:


对象,类,元类

Method

Method是一种代表类中的某个方法的类型。

typedef struct objc_method *Method;

objc_method:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • 方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
  • method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

IMP

IMP在objc.h中的定义是:

typedef id (*IMP)(id, SEL, ...);

  • 它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

  • 你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含idSEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址;反之亦然。

runtime做的那些事

首先需要明确的是,在objc中,直到运行时才将消息与方法实现绑定。而这些工作都是runtime为我们做的。

runtime之消息转发

其实[receiver message]会被编译器转化为:

objc_msgSend(receiver, selector)

如果消息含有参数,则为:

objc_msgSend(receiver, selector, arg1, arg2, ...)

在平时的调用中,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表(即class里的method_list表)。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

当然,objc_msgSend函数只是一般情况下的调用,还有会有例如给父类发送消息,返回值是结构体而不是数值等情况,会采用其他的例如objc_msgSendSuper函数等,在此不再叙述。

消息转发的第一步: 动态方法解析

所谓动态方法解析是发生在objc_msgSend函数查找完所有类对象or元类方法列表后仍未找到方法实现第一个调用的方法,动态方法解析发生在消息转发之前:

+ (BOOL)resolveClassMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相应的类方法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相应的对象方法调用

在平时的开发中,最常用的情况就是声明某个属性为@dynamic后,需要我们自己提供settergetter方法时。如:

@dynamic propertyName;

添加如上关键字修饰属性表示告诉编译器我们会动态的提供存取方法,此时动态方法解析就是个不错的选择:

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

第二步: 重定向

动态方法解析虽然强大,可以动态地为一个类添加方法,但它也有个很大的弊端:只能为当前类添加方法。如果我们想在消息无法解读时调用其他类的方法呢?此时就要用到重定向了。

重定向是发生在动态方法解析之后,完整的消息转发之后的。在此处我们就可以动态的将一条消息转换为其他类的调用:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(intstanceNoImpMethod)) {
        
        return [testClass class];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

需要注意的时,此时我们将消息转发给了testClass这个类,就表示我们希望这个不能被当前类解读的消息中的sel能被testClass执行。如果testClass类中没有这个sel的类方法,程序一样会crash:

+[testClass intstanceNoImpMethod]: unrecognized selector sent to class 0x103122ea8

如果我们在此方法中直接返回self或者nil,都会跳过这个步骤直接进入完整的消息转发机制。

所以如果你想把这个消息解读为其他类的对象方法,就要返回这个类的对象,如果想解读为类方法,就要返回类对象

同理,如果你想转发一个含有类方法的消息,就应该调用:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(intstanceNoImpMethod)) {
        
        return [testClass new];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

于是你可以大开脑洞:把一个类的类方法or对象方法换成另一个类的对象方法or类方法,怎么组合随你,只要注意sel的名称一致即可。有木有感觉很酷😃

ps:我在此处混用了类与类对象,其实这俩是一个东西,如果你不能理解,戳这里:深入理解objc中的对象与类

可见,利用重定向是在objc模拟多继承的一种方法。但是重定向和动态方法解析一样都有弊端,那就是还是不够“灵活”。消息中sel不能被我们更换。设想这种情景:如果我们又想更换sel,又想更换消息的接受者,此时我们该怎么做?

第三步:完整的消息转发机制

上文我们降到了如果动态方法解析失败,进入重定向,那么重定向也失败了,就来到了完整的消息转发机制:

这里遇到了些小困难,接下来在填坑吧


runtime健壮的实例变量

@property大家每天都在用,但也许你不知道,runtime在你为类添加了一个属性时,它会将这个成员变量(iva)存放在类对象里。这和比如JAVA等语言有着很大的不同:

objc将实例变量当做一种存储偏移量(offset)所用的特殊变量交由类对象保管。偏移量会运行期间查找,如果类的定义变了,那存储的偏移量也就变了。这样的话,无论何时访问实例变量,都能获取到正确的值。

基于这个原因,我们才能该动态的给一个类添加属性,因为属性列表本来就是动态查找的。

8月21日更新:关于健壮的实例变量,还有这样的说法:

当一个类定义了某些成员变量后编译一次后,再次改变该类的成员变量,会导致偏移量发生改变。在有runtime的情况下,它会自动帮你调整偏移量,以保证不用再次编译文件。


关联对象

关联对象指的是动态的为一个对象添加变量,之前有写过介绍的短文:在分类中给类添加属性


神奇的Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

上一个🌰:

+ (void)load{
    
    Class aClass = [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(cumtomMethod:);
    
    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
    
    // When swizzling a class method, use the following:
    // Class aClass = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(aClass, originalSelector);
    // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
    
    BOOL didAddMethod =
    class_addMethod(aClass,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(aClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
}

调用:

- (void)cumtomMethod:(BOOL)animted{
    
    [self  cumtomMethod:animted];
    
    NSLog(@"1111");
}

输出:

2016-08-18 00:39:04.658 总结测试[80016:3763455] 1111

我刚开始看这段代码也是晕的一塌糊涂,现在回想起来是没能立即SELIMP,下面是具体的调用过程:

系统调用viewWillAppear:(SEL) ----> 来到了customMethodIMP -----> 我们自己调用customMethod:SEL -----> 系统viewWillAppear:IMP

目前为止小弟也只是知道有这么个东西,还真没用到过这玩意。真有兴趣可以看看这篇:Objective-C的hook方案(一): Method Swizzling

8-21凌晨补充下:最近基友分享了一篇关于Method Swizzling的应用方案,是腾讯一面提到的。有兴趣可以看下。


扯扯淡😪

这篇文章花了不少心血,也通过撰写这篇文章彻底重新认识了Runtime这个之前小白时看都不敢看的东西。也观摩了不少大神的博客,感觉平时应该多注意这些好的资源,有时候比闷头写代码强不少😃。
在此放一下我特别喜欢的一位博主的博客:玉令天下的博客,就像引言所说,这篇文章不过是我读这为大神博客的学习笔记罢了。作为同龄的开发者,很是汗颜啊,共勉吧~~

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,132评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 729评论 0 2