iOS Developer - RuntimeiOS DeveloperiOS Developer

细说objc中的Runtime

2016-08-15  本文已影响429人  ZhengLi

前言

这篇文章讲解了一些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;

如下图:


对象,类,元类

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;

IMP

IMP在objc.h中的定义是:

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

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

上一篇下一篇

猜你喜欢

热点阅读