美文网首页程序员
第一篇:runtime的一些理论知识

第一篇:runtime的一些理论知识

作者: 意一ineyee | 来源:发表于2017-08-16 16:55 被阅读59次

    目录

    一、语言类型简介
    二、什么是runtime
    三、类和对象的本质,以及元类
    四、成员变量和属性
    五、方法的本质
    六、消息发送机制和消息转发机制
    七、小结

    一、语言类型简介


    我们常说OC是一门编译型动态语言,为什么这么说呢?为了回答这一问题,我们先得搞明白什么是编译型语言,什么又是动态语言,并由此引入我们这一系列的主题----runtime。

    1、编译型语言和解释型语言

    计算机是不能直接理解高级语言的,所以我们只能把高级语言翻译成机器语言来让计算机识别,而根据翻译的时机不同,高级语言可以分为编译型语言和解释型语言。

    (1)编译型语言是指程序在编译时就被翻译成了机器语言。我们Xcode常用的cmd+b-->build命令,就是在做compile(编译)和link(链接)两个操作,compile用来把源代码翻译成机器语言,link用来把各模块的机器语言和库链接起来生成可执行文件;而cmd+r-->run则是build+运行可执行文件的命令。

    • 优点:整个翻译过程只在编译时做一次,运行时不会再翻译,所以编译型语言的程序执行效率高

    • 缺点:当然如果修改了代码,我们是需要重新编译的,编译生成机器语言的时候是依赖于操作系统的,不同的操作系统会生成不同的机器语言,所以移植性会差一些

    • 代表语言:C、C++、OC等。

    (2)解释型语言是指程序在运行时才被翻译成机器语言,需要专门的解释器来翻译。

    • 优点:只要安装了解释器(虚拟机),在任何环境中都可以运行,所以不同平台的移植性更好一些修改代码后,可以快速部署,不用停机维护

    • 缺点:因为每个语句都是在执行的时候才翻译,所以每执行一次代码就会重新翻译一次,所以解释型语言的执行效率会低一些

    • 代表语言:PHP、Python、JS等。

    (3)混合型语言,既然编译型语言和解释性语言各有各的好处和缺点,所以就有人就把这两种语言的优点给整合了起来,如C#和Java,它们在编译的时候不会直接翻译成机器语言,而是会生成一种中间码,然后再由解释器(运行库或者虚拟机)解释执行。

    2、动态语言和静态语言

    (1)静态语言是指数据类型和代码结构在编译期时就确定下来的语言,C、C++、C#、Java等都是静态语言。

    (2)动态语言是指程序在运行时才去做类型检查、甚至还能改变代码结构的语言,(例如把NSNumber类型的对象赋值给NSString类型的变量,那么在编译时该变量是NSString类型,而在运行时它却是NSNumber类型;又比如代码在运行时我们还可以改变已有方法的实现等。)OC、PHP、Python、JS等都是动态语言。

    3、小结

    综上,我们知道OC需要编译和链接后生成可执行文件才能执行,所以说OC是一门编译型语言;OC是在运行时才进行类型检查和确定代码结构,所以说OC是一门动态语言,而正是runtime使得OC具备了动态特性

    二、什么是runtime


    runtime动态库的目录

    runtime其实就是一个动态库,这个库是用C语言和汇编写的,正是由于这个库的存在,才使得OC得以基于C语言实现,并使得OC成为一门动态语言,具体点说:

    • 在这个库中,runtime函数会把C结构体封装成OC对象,把C函数封装成OC方法,当然还封装出了一堆其它的OC特性,这就使得OC得以基于C语言实现。

    • 我们之所以说OC是一门动态语言,是因为它将静态语言一些编译时和链接时做的事情搬到了运行时来处理,比如数据类型的检查、方法实现的确定等,这样我们写代码就会非常灵活,而这一特点的实现也正是由runtime库来提供支撑的。

    三、类和对象的本质及元类


    1、对象的本质

    (1)在runtime库中,我们可以看到OC对象的定义如下:

    struct objc_object {
        Class isa;
    };
    

    可见OC对象的本质是一个objc_object类型的结构体,这个结构体里只有唯一的一个成员变量isa指针,指向该对象所属的类。

    (2)此外,我们也可以看到id的定义如下:

    typedef struct objc_object *id;
    

    可见id是一个指向objc_object的指针,换句话说id已经是一个指针了,它可以指向任意一个OC对象

    2、类的本质

    (1)在runtime库中,我们可以看到OC类的定义如下:

    struct objc_class {
        Class isa;
        
    #if !__OBJC2__
        Class super_class;
        const char *name;
        long version;
        long info;
        long instance_size;
        struct objc_ivar_list *ivars;
        struct objc_method_list **methodLists;
        struct objc_cache *cache;
        struct objc_protocol_list *protocols;
    #endif 
    };
    

    可见OC类的本质是一个objc_class类型的结构体,只不过这个结构体里的成员变量比较多,其中有几个是我们比较关注的:

    • isa指针指向该类所属的类。我们不是常说万事万物皆对象嘛,OC类当然也是一个对象了,所以类结构体objc_class里也有一个isa指针,指向该类所属的类。(那类的类又是什么呢?元类,下面我们会补充。)

    • super_class指向该类的父类。

    • ivars用来存放该类所有的成员变量和属性。

    • methodLists用来存放该类所有的实例方法。(不过要注意一个方法只有实现了才会被添加到该类的methodLists中,光声明是不会被添加的。)

    • cache用来缓存当前类调用过的实例方法。当一个对象接收到一个消息后,它会根据自己的isa指针先找到自己所属的类,然后遍历methodLists查找相应的方法来执行。但是在实际中一个类的实例方法只有一小部分是常用的,大多数的方法很少用或根本用不到,所以如果每次给一个对象发消息都去methodLists里遍历一遍,性能势必很差。这时cache就派上用场了,它里面缓存着当前类调用过的实例方法,那么当一个对象接收到一个消息后,就会首先去cache找这个方法,如果找不到才会遍历methodLists查找,执行后再把这个方法添加到cache中,从而提高方法调用的效率。

    (2)此外,我们也可以看到Class的定义如下:

    typedef struct objc_class *Class;
    

    可见Class是一个指向objc_class的指针,换句话说Class已经是一个指针了,它可以指向任意一个OC类

    3、元类MetaClass

    什么是元类?类的类就是元类,每个类都有自己的元类,元类也是一个objc_class结构体,元类之所以重要是因为它内部存储着一个类所有的类方法。

    那元类也是一个对象啊,元类的isa指针又指向哪里呢?如果元类的isa指针又指向元类的类,那这样一直延伸下去,那岂不是无穷无尽了嘛,总得有一个头吧!光这么说的话有点绕,我们不妨举个例子来说明。假设现在有三个类BoyPerson和根类NSObjectBoy类继承自PersonPerson类又继承自NSObject,并且分别创建了一个对象boypersonobject,那么它们的isa指针指向和继承关系如下图:

    红条isa指向,蓝条继承关系
    • isa指针的指向:

    boy对象的isa指针指向Boy类,Boy类的isa指针指向Boy元类,Boy元类的isa指针指向NSObject元类。

    person对象的isa指针指向Person类,Person类的isa指针指向Person元类,Person元类的isa指针指向NSObject元类。

    object对象的isa指针指向NSObject类,NSObject类的isa指针指向NSObject元类,NSObject元类的isa指针指向它自己。

    所以我们可以回答上面的问题:所有元类的isa指针都指向根类NSObject的元类。(当然,OC中根类不止有NSObejct一个)

    • 元类的继承关系:

    同时我们也发现,元类和普通的类一样也具有继承关系,一层一层的继承上去,直到NSObject的元类为止,而NSObject的元类继承自NSObject

    四、成员变量和属性


    上面我们已经了解了类和对象的本质,这一部分我们讲的是类结构体里的成员变量和属性,我们主要看下成员变量、实例变量和属性的区别。

    1、成员变量、实例变量

    在iOS5之前MRC的年代,是没有属性这个概念的,如果我们想要为一个类包装一些数据,就只能给这个类添加成员变量。如下:

    -----------Person.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject {
        
        NSString *_name;
        NSString *_sex;
        NSInteger _age;
    }
    
    @end
    

    大括号里的_name、_sex、_age就称为Person类的成员变量,至于为什么前面要加下划线,是OC官方的命名约定,为了避免变量泄漏,而_name、_sex又是对象类型的,也就是说是它们是某个类的实例,所以它们又可称为实例变量。所以说:成员变量 = 实例变量 + 基本数据类型的变量

    而如果我们想要用点语法来访问这些成员变量,就得按指定的格式,为每个成员变量手动添加setter、getter方法的声明和实现。如下:

    -----------Person.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject {
        
        NSString *_name;
        NSString *_sex;
        NSInteger _age;
    }
    
    - (void)setName:(NSString *)name;
    - (NSString *)name;
    
    - (void)setSex:(NSString *)sex;
    - (NSString *)sex;
    
    - (void)setAge:(NSInteger)age;
    - (NSInteger)age;
    
    @end
    
    
    -----------Person.m-----------
    
    - (void)setName:(NSString *)name {
        
        // 先保留新值
        [name retain];
        // 然后释放旧值
        [_name release];
        // 最后把成员变量指向新值
        _name = name;
    }
    
    - (NSString *)name {
        
        return _name;
    }
    
    - (void)setSex:(NSString *)sex {
        
        [sex retain];
        [_sex release];
        _sex = sex;
    }
    
    - (NSString *)sex {
        
        return _sex;
    }
    
    - (void)setAge:(NSInteger)age {
        
        // 简单的赋值操作
        _age = age;
    }
    
    - (NSInteger)age {
        
        return _age;
    }
    
    2、属性

    看到上面了吧,项目中有多少成员变量,你就得相应的为它们手动添加多少setter、getter方法的声明和实现。哇,这不崩盘了吗,累都累死了。所以iOS5之后引入ARC的同时也引入了属性这个概念,引入属性就是为了解决我们要为大量成员变量手动添加setter、getter方法声明和实现的问题,会牵涉到三个比较重要的关键词@property(属性)、@synthesize(合成)和@dynamic(动态)。

    有了属性,我们不再直接创建成员变量,而是使用@property来创建属性。如下:

    -----------Person.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property NSString *name;
    @property NSString *sex;
    @property NSInteger age;
    
    @end
    

    然后再配合一步@synthesize var = _var把成员变量绑定到属性身上,正是这一步编译器会自动为我们创建一堆与属性名字相同但是前面带下划线的成员变量,并自动生成这些成员变量setter、getter方法的声明和实现,从而支持我们使用点语法来访问成员变量,这的确减轻了我们不少的工作量。如下:

    -----------Person.m-----------
    
    #import "Person.h"
    
    @implementation Person
    
    @synthesize name = _name;
    @synthesize sex = _sex;
    @synthesize age = _age;
    
    @end
    

    再后来,@property更牛了,连@synthesize都吃了。也就是说,只要我们使用@property来创建属性,编译器会自动为我们创建一堆与属性名字相同但是前面带下划线的成员变量,并自动生成这些成员变量setter、getter方法的声明和实现,一步到位。

    除非在某些情况下,我们偏偏不需要编译器默认为我们这样做,这时就可以使用@dynamic,它恰恰是告诉编译器在创建属性时的时候,不要为我们自动创建与属性相关的成员变量,可以自动生成setter、getter方法的声明,但是不要自动生成setter、getter方法的实现,setter、getter方法由我们自己来实现。

    注:以上说的一切,都可以通过获取一个类的成员变量列表、属性列表、实例方法列表来观察验证,有兴趣的可以自行验证。

    3、关于成员变量和属性,我们常用的runtime函数有:

    // 获取成员变量列表
    Ivar * class_copyIvarList(Class class, unsigned int *outCount);
    
    // 获取属性列表
    objc_property_t * class_copyPropertyList(Class class, unsigned int *outCount);
    

    五、方法的本质


    第四部分我们了解了类结构体里的成员变量和属性,这一部分我们讲的是类结构体里的方法,我们主要看下方法的本质。

    (1)在runtime库中,我们可以看到OC方法的定义如下:

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

    可见OC方法的本质是一个objc_method类型的结构体,结构体里有三个成员变量来共同描述一个方法:

    • SEL:方法的选择子,OC在编译时,会为每个方法根据其方法名hash出一个选择子,来标识唯一的一个方法,runtime就是根据这个选择子来查找对应的方法的。之所以要为每个方法hash出一个选择子,是因为只要方法的方法名相同,那么不管这些方法分别声明和实现在多少个类里,它们的选择子都是一样的,而且是唯一的一个,这样项目中一个选择子就可以对应多个方法,极大的提升方法查找的效率。

    • method_types:用来存放方法的参数和返回值信息。

    • IMP:函数指针,指向方法的实现。

    (2)此外,我们也可以看到Method的定义如下:

    typedef struct objc_method *Method;
    

    可见Method是一个指向objc_method的指针,换句话说Method已经是一个指针了,它可以指向任意一个OC方法。

    (3)关于方法,我们常用的runtime函数有:

    // 获取实例方法
    Method class_getInstanceMethod(Class class, SEL selector);
    // 获取类方法
    Method class_getClassMethod(Class class, SEL selector);
    // 获取实例方法列表
    Method * class_copyMethodList(Class class, unsigned int *outCount);
    
    // 获取方法的选择子
    SEL NSSelectorFromString(方法名的字符串);
    SEL @selector(方法名);
    // 获取方法的实现
    IMP method_getImplementation(Method method);
    // 获取方法的参数和返回值信息
    const char * method_getTypeEncoding(Method method);
    
    // 为某个类添加一个方法
    BOOL class_addMethod(Class class, SEL selector, IMP implementation, const char *types);
    // 替换某个方法的实现
    IMP class_replaceMethod(Class class, SEL selector, IMP implementation, const char *types);
    // 交换两个方法的实现
    void method_exchangeImplementations(Method method1, Method method2);
    
    • class_getInstanceMethod函数用来获取实例方法,如果在当前类中没有找到指定的方法,会去父类里查找。

    • class_getClassMethod函数用来获取类方法,如果在当前类中没有找到指定的方法,会去父类里查找,不过这个类要传入类的元类objc_getMetaClass(object_getClassName(Class class))

    • class_copyMethodList函数用来获取实例方法列表,但是该函数只会查找当前类的实例方法列表,不会去父类里查找。如果我们想获取某个类的类方法列表,则把类传入类的元类objc_getMetaClass(object_getClassName(Class class))即可。

    • class_addMethod函数用来为某个类添加一个方法,但是如果一个类自己已经实现了某个方法,那我们再用该函数去为这个类添加同名方法的话,是添加不成功的,函数会返回NO,方法依旧保持它原来的实现。如果一个类自己没有实现某个方法,那我们用该函数为这个类添加方法,就能添加成功,函数返回YES

    • class_replaceMethod函数用来替换某个方法的实现,如果某个类已经实现了某个方法,那么该函数会替换掉方法原来的实现,如果某个类没有实现某个方法,那么该函数就会为该类添加这个方法。

    • method_exchangeImplementations函数用来交换两个方法的实现,但是必须是两个方法都实现了替换才能成功,但凡其中一个方法没有实现就替换不成功。

    六、消息发送机制和消息转发机制


    1、消息发送机制(或者说OC方法调用的流程)

    消息发送机制其实就是指所有OC对象调用方法的操作都会转换成runtime函数id objc_msgSend(id object, SEL selector, ...)的调用,就像下面这样:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // OC代码
        Person *person = [[Person alloc] init];
        [person eat];
        
        // C底层实现
        Person *person = objc_msgSend([Person class], @selector(alloc));
        person = objc_msgSend(person, @selector(init));
        objc_msgSend(person, @selector(eat));
    }
    

    消息发送机制的核心就是objc_msgSend这个函数,它完成了消息发送机制的所有事情,我们也把消息发送机制通俗的称作OC方法调用的流程,那OC方法调用的流程是怎样的呢?

    当一个OC对象调用方法[object method]时:

    • 编译阶段编译器就会把OC对象对方法的调用转换为对objc_msgSend(object, @selector(method))函数的调用,然后在运行时方法被调用的时候才去找方法的具体实现。

    • 因此在编译结束到方法真正被调用之前这段空档期(这个空档期当然也是运行时的一小段),就给了我们机会来修改一个方法的实现。

    • 那么在运行时objc_msgSend(object, @selector(method))函数会首先根据objectisa指针找到object所属的类,然后拿着methodselector去类结构体的methodLists中遍历查找,如果找到了对应的方法,那么objc_msgSend函数就会拿着这个方法的具体实现、方法的参数和返回值信息来执行。如果在本类中没找到selector对应的方法,objc_msgSend函数就会根据类结构体里的super_class找到该类的父类,继续在父类的methodLists中遍历查找,如此一直到根类NSObject为止。如果找到根类NSObject还是没找到selector对应的方法,就会触发消息转发机制。当然了,runtime为了提高消息发送的效率,使用了cache机制,这个我们在说类的本质时已经提到,不再重复。

    2、消息转发机制

    消息转发机制其实就是指当一个对象调用了一个它没有实现的方法,即我们向一个对象发送了一个它无法识别的消息时,程序就会崩掉。但在程序崩掉之前,系统其实会触发一系列的方法给我们机会来告诉对象该怎么处理这个它无法识别的消息,从而避免程序崩掉。

    消息转发机制其实有三种实现方案,此处我们只讲解完整消息转发。完整消息转发就是指程序在崩掉前会触发 methodSignatureForSelectorforwardInvocation两个方法,我们可以在这两个方法里做一些事情来把消息转发给其它的对象执行。下面我们举个例子,来看一下完整消息转发怎么实现,假设Person类没有实现eat实例方法,所以为了避免程序崩溃,我们要在person对象调用eat方法时,把eat方法转发给boy对象来执行,Boy类实现了eat实例方法:

    -----------Person.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @end
    
    
    -----------Person.m-----------
    
    #import "Person.h"
    #import "Boy.h"
    
    @implementation Person
    
    // 这个方法用来构建未实现方法的方法签名并返回,消息转发机制会根据这个方法返回的方法签名来构建下面forwardInvocation:方法的anInvocation参数
    // aSelector:未实现方法的选择子
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        
        NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
        if (methodSignature == nil) {
            
            Boy *boy = [[Boy alloc] init];
            // 获取同名方法的方法签名
            methodSignature = [boy methodSignatureForSelector:aSelector];
        }
        
        return methodSignature;
    }
    
    // 这个方法用来完成消息转发
    // anInvocation:未实现方法的全部细节都封装在这个参数里
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        
        // 转发给boy执行
        Boy *boy = [[Boy alloc] init];
        [anInvocation invokeWithTarget:boy];
    }
    
    @end
    
    -----------Boy.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Boy : NSObject
    
    @end
    
    
    -----------Boy.m-----------
    
    #import "Boy.h"
    
    @implementation Boy
    
    - (void)eat {
        
        NSLog(@"boy eat");
    }
    
    @end
    
    -----------ViewController.m-----------
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Person *person = [[Person alloc] init];
        [person performSelector:@selector(eat)];
    }
    

    打印:

    2018-10-11 16:58:32.472417+0800 Runtime[5183:522719] boy eat
    

    七、小结


    至此,我们就了解了runtime的一些理论知识及runtime真正强大的消息发送机制和消息转发机制,虽然我们在实际开发中很少会直接用到这两大机制来做什么事情,但了解它们有助于我们了解OC底层的实现,有助于我们感知OC的动态特性并利用这一特性在实际开发中做些文章,比如对成员变量和属性的应用、对方法的应用等,后面几篇我们会讲解一下runtime一些简单的实际应用场景。

    此外,runtime的东西还有很多(比如在运行时动态创建类和对象等等),此处只是做了一些基本的学习和实际开发中用到runtime地方的总结,可以做更深入的学习。


    参考博客:南峰子讲解runtime


    相关文章

      网友评论

        本文标题:第一篇:runtime的一些理论知识

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