美文网首页iOS 艾欧艾斯
Runtime 那些事儿 总结

Runtime 那些事儿 总结

作者: David_Cap | 来源:发表于2015-08-26 12:01 被阅读444次

    引言

    Objective-C 是一门动态的语言,这Runtime 是主要的功臣啊。

    下面我们举个🌰来初探一下 Runtime。

    [testClass testFunction];
    

    当我们调用上面那段话的时候,很多人觉得是一个简单的方法调用而已。然而 OC是“发送消息”机制的。

    也就是编译器会转化成:

    //没有参数
    objc_msgSend(testClass,selector);
    
    //有参数
    objc_msgSend(testClass,selector,arg1,agr2,...);
    

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

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

    Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。

    Runtime的数据结构

    下面是对Runtime数据结构的了解,算是铺垫吧。

    例如objc_msgSend:,里面是这样的

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

    现在分别来了解一下里面那些变量分别是什么东西吧。

    SEL

    objc_msgSend函数第二个参数类型为SEL,它就相当于@selector在Objec中的表示类型。SEL可以理解是主键ID的概念,为区分不同方法的一个ID。

    //Objec
    typedef struct objc_selector *SEL;
    

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

    不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器.

    id

    objc_msgSend第一个参数类型为id,它是一个指向类实例的指针。例如在引言中,这个id self 就是 指向testClass的一个指针。

    typedef struct objc_object *id;
    

    那objc_object又是啥呢:

    struct objc_object { Class isa; };
    

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

    通俗的说isa指针表示的就是,如果 你有一个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_ivar_list和objc_method_list分别是成员变量列表和方法列表:

    
    //objc_ivar_list
    struct objc_ivar_list {
        int ivar_count                                           OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    
    //objc_method_list
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }
    

    这个可以理解为,属性(ivar)和方法(method)的2张分发表。

    cache吧,顾名思义就是缓存的意思。这个比较犀利,等等单独讲吧。

    不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。你会说 NSObject 的子类时,你的类就会指向NSObject 做为其超类。但是所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

    Meta_Class.png

    上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

    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;
    }    
    

    方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同.

    方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型

    method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到

    Ivar

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

    typedef struct objc_ivar *Ivar;
    

    而objc_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
    }                                                            OBJC2_UNAVAILABLE;
    

    ivar_name 属性名称

    ivar_type 属性的类型,NSString,NSData.......

    IMP

    IMP 简单的说就是指向SEL 实现方法 的函数指针

    IMP在objc.h中的定义是:

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

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

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

    Cache

    在runtime.h中Cache的定义如下:

    typedef struct objc_cache *Cache
    

    还记得之前objc_class结构体中有一个struct objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:

    struct objc_cache {
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        Method buckets[1]                                        OBJC2_UNAVAILABLE;
    };
    

    Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,而我猜苹果为提高Cache命中率应该也做了努力吧。

    消息发送

    objc_msgSend函数

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

    1. 首先根据testClass对象的isa指针获取它对应的class;
    2. 优先在class的cache查找message方法,如果找不到,再到methodLists查找;
    3. 如果没有在class找到,再到super_class查找;
    4. 一旦找到message这个方法,就执行它实现的IMP。
    Method_Send.jpg

    self与super区别

    先来show一道经典题目吧。

    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    

    其实这俩输出都是 Son。那为什么会这样呢??

    首先我们知道 调用方法,编译器就会用 objc_msgSend方法去发送消息。

    那么当调用[self class] 的时候就是这样的。
    objc_msgSend(self,class);
    
    而当调用 [super class] 的时候就是这样的。
    objc_msgSendSuper(self,class);
    

    看出来了吧,super只是一个编译标示符。

    无论是[self class]还是[super class],接受消息者都是Son对象,但super与self不同的是,self调用class方法时,是在子类Son中查找方法,而super调用class方法时,是在父类Father中查找方法。

    方法解析与消息转发

    [receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。

    1. Method Resolution
    2. Fast Forwarding
    3. Normal Forwarding
    Message.jpg

    Method Resolution

    首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

    举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:

    @interface Message : NSObject
    - (void)sendMessage:(NSString *)word;
    @end
    
    @implementation Message
    - (void)sendMessage:(NSString *)word
    {
        NSLog(@"normal way : send message = %@", word);
    }
    @end
    

    如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:

    - (void)viewDidLoad {
        [super viewDidLoad];
        Message *message = [Message new];
        [message sendMessage:@"Sam Lau"];
    }
    

    控制台会打印以下信息:

    normal way : send message = Sam Lau
    

    但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:

    #pragma mark - Method Resolution
    /// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if (sel == @selector(sendMessage:)) {
            class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
                NSLog(@"method resolution way : send message = %@", word);
            }), "v@*");
        }
        return YES;
    }
    

    控制台就会打印以下信息:

    method resolution way : send message = Sam Lau
    

    注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考 Type Encodings

    如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)。

    Fast Forwarding

    如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。

    继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:

    #pragma mark - Fast Forwarding
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if (aSelector == @selector(sendMessage:)) {
            return [MessageForwarding new];
        }
        return nil;
    }
    

    此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:

    @interface MessageForwarding : NSObject
    - (void)sendMessage:(NSString *)word;
    @end
    
    @implementation MessageForwarding
    - (void)sendMessage:(NSString *)word
    {
        NSLog(@"fast forwarding way : send message = %@", word);
    }
    @end
    

    此时,控制台会打印以下信息:

    fast forwarding way : send message = Sam Lau
    

    这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

    Normal Forwarding

    如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。

    继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:

    #pragma mark - Normal Forwarding
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
        if (!methodSignature) {
            methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
        }
        return methodSignature;
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        MessageForwarding *messageForwarding = [MessageForwarding new];
        if ([messageForwarding respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:messageForwarding];
        }
    }
    

    dome Link;

    参考致谢

    http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1
    http://justsee.iteye.com/blog/2163777

    相关文章

      网友评论

        本文标题:Runtime 那些事儿 总结

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