iOS Runtime的理解

作者: PierceDark | 来源:发表于2018-02-26 13:20 被阅读462次
    字数5000+,预计阅读时间 30分钟
    

    主要参考自:
    1、iOS运行时(Runtime)详解+Demo
    2、Objective-C Runtime
    3、神经病院Objective-C Runtime出院第三天——如何正确使用Runtime

    运行时简介

    Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。
    对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。Runtime基本上是用C汇编写的,这个库使得C语言有了面向对象的能力。
    Runtime中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,让OC的面向对象编程变为可能。
    找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething)Runtime会根据消息接收者是否能响应该消息而做出不同的反应。

    一、类与对象基础数据结构

    1、object_class 类
    Objective-C类是由Class类型来表示的,它实际上是一个指
    objc_class结构体的指针。

    typedef struct object_class *Class
    

    它的定义如下:

    struct object_class{
        Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
         Class super_class                        OBJC2_UNAVAILABLE;  // 父类
         const char *name                         OBJC2_UNAVAILABLE;  // 类名
         long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
         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;
    
    

    2、objc_object 实例
    objc_object是表示一个类的实例的结构体
    它的定义如下:

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

    可以看到,这个结构体只有一个字体,即指向其类的isa指针。这
    样,当我们向一个Objective-C对象发送消息时,运行时库会根据
    实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类
    的方法列表及父类的方法列表中去寻找与消息对应的selector指向
    的方法,找到后即运行这个方法。

    3、元类(Meta Class)
    meta-class是一个类对象的类。
    在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
    既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?

    答案是,为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。

    所以,调用类方法的这个类对象的isa指针指向的就是meta-class
    当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

    再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-classisa指向基类的meta-class,以此作为它们的所属类。

    即,任何NSObject继承体系下的meta-class都使用NSObjectmeta-class作为自己的所属类,而基类的meta-classisa指针是指向它自己。

    通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下:

    OC 中的继承体系.png

    4、Category
    Category是表示一个指向分类的结构体的指针,其定义如下:

    typedef struct objc_category *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; // 分类所实现的协议列表
    }
    
    

    这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
    可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性,(!除非使用关联对象,而且Category中的属性,只会生成setter和getter方法,不会生成成员变量)例子如下

    #import "UIButton+ClickBlock.h"
    #import static const void *associatedKey = "associatedKey";
    @implementation UIButton (ClickBlock)
    //利用关联对象在Category中设置的属性,只会生成setter和getter方法,不会生成成员变量
    -(void)setClick:(clickBlock)click{
        objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
        [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        if (click) {
            [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        }
    }
    -(clickBlock)click{
        return objc_getAssociatedObject(self, associatedKey);
    }
    -(void)buttonClick{
        if (self.click) {
            self.click();
        }
    }
    @end
    

    然后在代码中,就可以使用 UIButton 的属性来监听单击事件了:

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = self.view.bounds;
    [self.view addSubview:button];
    button.click = ^{
        NSLog(@"buttonClicked");
    };
    

    关联对象的使用

    1.设置关联值
    参数说明:
    object:与谁关联,通常是传self
    key:唯一键,在获取值时通过该键获取,通常是使用static
    const void *来声明
    value:关联所设置的值
    policy:内存管理策略,比如使用copy
    
    void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)
    
    2.获取关联值
    参数说明:
    object:与谁关联,通常是传self,在设置关联时所指定的与哪个对象关联的那个对象
    key:唯一键,在设置关联时所指定的键
    id objc_getAssociatedObject(id object, const void *key)
    
    3.取消关联
    void objc_removeAssociatedObjects(id object)
    
    关联策略
    使用场景:
    可以在类别中添加属性
    
    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
    OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用关联,通常是基本数据类型
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示强引用关联对象,是线程安全的
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示关联对象copy,是线程安全的
    OBJC_ASSOCIATION_RETAIN = 01401,         // 表示强引用关联对象,不是线程安全的
    OBJC_ASSOCIATION_COPY = 01403            // 表示关联对象copy,不是线程安全的
    };
    
    

    二、方法与消息

    1、SEL
    SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

    typedef struct objc_selector *SEL;
    

    方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
    两个类之间,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行
    如在某一个类中定义以下两个方法: 错误

    - (void)setWidth:(int)width;
    - (void)setWidth:(double)width;
    

    当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP
    工程中的所有的SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!
    本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

    通过下面三种方法可以获取SEL:
    a、sel_registerName函数
    b、Objective-C编译器提供的@selector()
    c、NSSelectorFromString()方法
    

    2、IMP
    IMP实际上是一个函数指针,指向方法实现的地址。
    其定义如下:

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

    第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
    第二个参数:是方法选择器(selector)
    接下来的参数:方法的参数列表。

    前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

    3、Method
    Method用于表示类定义中的方法,则定义如下:

    typedef struct objc_method *Method
    struct objc_method{
        SEL method_name      OBJC2_UNAVAILABLE; // 方法名
        char *method_types   OBJC2_UNAVAILABLE;
        IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
    }
    

    我们可以看到该结构体中包含一个SELIMP,实际上相当于在SELIMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

    4、消息
    Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

    有关消息发送和消息转发机制的原理,可以查看这篇文章


    面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把 [receiver message] 当成简单的方法调用,而无视了“发送消息”这句话的深刻含义。其实 [receiver message] 会被编译器转化为:

    objc_msgSend(receiver, selector)
    

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

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

    如果消息的接收者能够找到对应的 selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个 selector 对应的实现内容,要么就干脆玩完崩溃掉。

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

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

    1、检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
    2、检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
    3、如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
    4、如果 cache 找不到就找一下方法分发表。
    5、如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
    6、如果还找不到就要开始进入动态方法解析了,后面会提到。
    PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。
    
    方法调用流程.gif

    其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。

    值得一提的是在i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret函数来进行处理,这是因为返回类型为浮点数的函数对应的ABI(Application Binary Interface)与返回整型的函数的ABI不兼容。此时objc_msgSend不再适用,于是objc_msgSend_fpret被派上用场,它会对浮点数寄存器做特殊处理。不过在PPCPPC64 平台是不需要麻烦它的。

    PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”“return”“fpret”就是“fp”+“ret”,分别代表“floating-point”“return”

    5、动态方法解析
    你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:

    @dynamic propertyName;
    

    这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当Runtime系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

    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
    

    上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

    PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO

    备注:解析类方法等具体做法 例:
    .h

    #import <Foundation/Foundation.h>
    
    @interface Student : NSObject
    + (void)learnClass:(NSString *) string;
    - (void)goToSchool:(NSString *) name;
    @end
    

    .m

    #import "Student.h"
    #import <objc/runtime.h>
    
    @implementation Student
    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(learnClass:)) {
            class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
            return YES;
        }
        return [class_getSuperclass(self) resolveClassMethod:sel];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if (aSEL == @selector(goToSchool:)) {
            class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
    
    + (void)myClassMethod:(NSString *)string {
        NSLog(@"myClassMethod = %@", string);
    }
    
    - (void)myInstanceMethod:(NSString *)string {
        NSLog(@"myInstanceMethod = %@", string);
    }
    @end
    

    需要深刻理解[self class]object_getClass(self) 甚至 object_getClass([self class]) 的关系,其实并不难,重点在于 self 的类型:

    1、当 self 为实例对象时,[self class] 与 object_getClass(self) 等价,因为前者会调用后者。
    object_getClass([self class]) 得到元类。
    
    2、当 self 为类对象时,[self class] 返回值为自身,还是 self。
    object_getClass(self) 与 object_getClass([self class]) 等价。
    

    6、消息转发

    消息转发

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

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

    毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,如果此方法返回nilself,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

    如果想替换类方法的接受者,需要覆写 + (id)forwardingTargetForSelector:(SEL)aSelector方法,并返回类对象:

    + (id)forwardingTargetForSelector:(SEL)aSelector {
        if(aSelector == @selector(xxx)) {
            return NSClassFromString(@"Class name");
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    转发
    当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        if ([someOtherObject respondsToSelector:
                [anInvocation selector]])
            [anInvocation invokeWithTarget:someOtherObject];
        else
            [super forwardInvocation:anInvocation];
    }
    

    该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

    这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。

    当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。

    forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

    注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

    转发与多继承
    转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。

    forwarding.gif

    这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中WarriorDiplomat没有继承关系,但是Warriornegotiate消息转发给了Diplomat后,就好似DiplomatWarrior的超类一样。

    消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。

    转发与继承
    尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector:isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息:

    if ( [aWarrior respondsToSelector:@selector(negotiate)] )
        ...
    

    结果是NO,尽管它能够接受negotiate消息而不报错,因为它靠转发消息给Diplomat类来响应消息。

    如果你为了某些意图偏要“弄虚作假”让别人以为Warrior继承到了Diplomatnegotiate方法,你得重新实现 respondsToSelector:isKindOfClass:来加入你的转发算法:

    - (BOOL)respondsToSelector:(SEL)aSelector
    {
        if ( [super respondsToSelector:aSelector] )
            return YES;
        else {
            /* Here, test whether the aSelector message can     *
             * be forwarded to another object and whether that  *
             * object can respond to it. Return YES if it can.  */
        }
        return NO;
    }
    

    除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::

    - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
    {
        NSMethodSignature* signature = [super methodSignatureForSelector:selector];
        if (!signature) {
           signature = [surrogate methodSignatureForSelector:selector];
        }
        return signature;
    }
    

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

    Swizzling原理
    在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。
    所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。
    
    每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。
    IMP有点类似函数指针,指向具体的Method实现
    
    

    这是参考Mattt大神在NSHipster上的文章自己写的代码。

    #import <objc/runtime.h> 
     
    @implementation UIViewController (Tracking) 
     
    + (void)load { 
        static dispatch_once_t onceToken; 
        dispatch_once(&onceToken, ^{ 
            Class aClass = [self class]; 
                // When swizzling a class method, use the following:
            // Class aClass = object_getClass((id)self);
            
            SEL originalSelector = @selector(viewWillAppear:); 
            SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
     
            Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
            Method swizzledMethod = class_getInstanceMethod(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); 
            } 
        }); 
    } 
     
    #pragma mark - Method Swizzling 
     
    - (void)xxx_viewWillAppear:(BOOL)animated { 
        [self xxx_viewWillAppear:animated]; 
        NSLog(@"viewWillAppear: %@", self); 
    } 
     
    @end
    

    上面的代码通过添加一个Tracking类别到UIViewController类中,将UIViewController类的viewWillAppear:方法和Tracking类别中xxx_viewWillAppear:方法的实现相互调换。Swizzling 应该在+load方法中实现,因为+load是在一个类最开始加载时调用。dispatch_once是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

    如果类中不存在要替换的方法,那就先用class_addMethodclass_replaceMethod函数添加和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations函数交换了两个方法的 IMP,这是苹果提供给我们用于实现Method Swizzling的便捷方法。

    可能有人注意到了这行:

    // 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);
    
    

    object_getClass((id)self)[self class]返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 selfClass 而不是实例.注意 [NSObject class][object class] 的区别:

    + (Class)class {
        return self;
    }
    
    - (Class)class {
        return object_getClass(self);
    }
    

    PS:如果类中没有想被替换实现的原方法时,class_replaceMethod相当于直接调用class_addMethod向类中添加该方法的实现;否则调用method_setImplementation方法,types参数会被忽略。method_exchangeImplementations方法做的事情与如下的原子操作等价:

    IMP imp1 = method_getImplementation(m1);
    IMP imp2 = method_getImplementation(m2);
    method_setImplementation(m1, imp2);
    method_setImplementation(m2, imp1);
    
    

    最后xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们与viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环。
    PS:看到有人说+load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神去掉了dispatch_once 部分。

    扩展阅读:
    1、iOS---防止UIButton重复点击的三种实现方式
    2、Swift Runtime分析:还像OC Runtime一样吗?

    相关文章

      网友评论

        本文标题:iOS Runtime的理解

        本文链接:https://www.haomeiwen.com/subject/eztexftx.html