美文网首页
iOS Runtime 基础原理

iOS Runtime 基础原理

作者: 羽裳有涯 | 来源:发表于2018-12-10 10:12 被阅读7次

    关于 Runtime ,网上已经有很多很好的文章,写得很详尽。本篇主要是从新手的角度出发,逐步介绍 Runtime 的原理、常用方法、应用场景等。

    相关链接:

    苹果维护的Runtime开源代码
    GNU维护一个开源的runtime 版本
    官方Api

    一、Runtime 是什么

    C 语言中,将代码转换为可执行程序,一般要经历三个步骤,即编译、链接、运行在链接的时候,对象的类型、方法的实现就已经确定好了

    而在 Objective-C 中,却将一些在编译链接过程中的工作,放到了运行阶段。也就是说,就算是一个编译好的 .ipa 包,在程序没运行的时候,也不知道调用一个方法会发生什么。这也为后来大行其道的「热修复」提供了可能。因此我们称 Objective-C为一门动态语言。

    这样的设计使 Objective-C 变得灵活,甚至可以让我们在程序运行的时候,去动态修改一个方法的实现。而实现这一切的基础就是 Runtime
    简单来说, Runtime 是一个库,这个库使我们可以在程序运行时创建对象、检查对象,修改类和对象的方法。
    至于这个库是怎么实现的,请紧张刺激地往下看。

    二、Runtime 是怎么工作的

    要了解 Runtime 是怎么工作的,首先要知道类和对象在 Objective-C 中是怎么定义的。

    注意:以下会用到 C 语言中结构体的内容,包括结构体的定义、为结构体定义别名等。如果你对这块不熟悉,建议先复习一下这块的语法。传送门

    1. Class 和 Object

    objc.h 中, Class 被定义为指向 objc_class 的指针,定义如下:

    typedef struct objc_class *Class;
    
    

    objc_class 是一个结构体,在 runtime.h 中的定义如下:

    struct objc_class {
        Class isa;                                // 实现方法调用的关键
        Class super_class;                        // 父类
        const char * name;                        // 类名
        long version;                             // 类的版本信息,默认为0
        long info;                                // 类信息,供运行期使用的一些位标识
        long instance_size;                       // 该类的实例变量大小
        struct objc_ivar_list * ivars;            // 该类的成员变量链表
        struct objc_method_list ** methodLists;   // 方法定义的链表
        struct objc_cache * cache;                // 方法缓存
        struct objc_protocol_list * protocols;    // 协议链表
    };
    
    

    为了方便理解,我这里去掉了一些声明,主要是和 Objective-C 语言版本相关,这里可以暂时忽略。完整的定义可以自己去 runtime.h 中查看。

    提示:在 Xcode 中,使用快捷键 command + shift + o ,可以打开搜索窗口,输入 objc_class 即可看到头文件定义。

    可以看到,一个类保存了自身所有的成员变量( ivars )、所有的方法( methodLists )、所有实现的协议( objc_protocol_list )。

    比较重要的字段还有 isacache ,它们是什么东西,先不着急,我们来看下 Objective-C 中对象的定义。

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

    这里看到了我们熟悉的 id ,一般我们用它来实现类似于 C++ 中泛型的一些操作,该类型的对象可以转换为任意一种对象。在这里 id 被定义为一个指向 objc_object 的指针。说明 objc_object 就是我们平时常用的对象的定义,它只包含一个 isa 指针。

    也就是说,一个对象唯一保存的信息就是它的 Class 的地址 isa。当我们调用一个对象的方法时,它会通过 isa 去找到对应的 objc_class,然后再在 objc_classmethodLists 中找到我们调用的方法,然后执行。

    再说说 cache ,因为调用方法的过程是个查找 methodLists 的过程,如果每次调用都去查找,效率会非常低。所以对于调用过的方法,会以 map 的方式保存在 cache 中,下次再调用就会快很多。

    2. Meta Class 元类

    上一小节讲了 Objective-C 中类和对象的定义,也讲了调用对象方法的实现过程。但还留下了许多问题,比如调用一个对象的类方法的过程是怎么样的?还有 objc_class 中也有一个 isa 指针,它是干嘛用的?

    现在划重点,在 Objective-C 中,类也被设计为一个对象

    其实观察 objc_classobjc_object 的定义,会发现两者其实本质相同(都包含 isa 指针),只是 objc_class 多了一些额外的字段。相应的,类也是一个对象,只是保存了一些字段。

    既然说类也是对象,那么类的类型是什么呢?这里就引出了另外一个概念 —— Meta Class(元类)。

    Objective-C 中,每一个类都有对应的元类。而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。并且类的 isa 指针指向对应的元类。因此上面的问题答案就呼之欲出,调用一个对象的类方法的过程如下:

    1. 通过对象的 isa 指针找到对应的类。
    2. 通过类的 isa 指针找到对应元类。
    3. 在元类的 methodLists 中,找到对应的方法,然后执行。

    注意:上面类方法的调用过程不考虑继承的情况,这里只是说明一下类方法的调用原理,完整的调用流程在后面会提到。

    这么说来元类也有一个 isa 指针,元类也应该是一个对象。的确是这样。那么元类的 isa 指向哪里呢?为了不让这种结构无限延伸下去, Objective-C 的设计者让所有的元类的 isa 指向基类(比如 NSObject )的元类。而基类的元类的 isa 指向自己。这样就形成了一个完美的闭环。

    下面这张图可以清晰地表示出这种关系。


    1852765-244b037923a6c2aa.jpg

    同时注意 super_class 的指向,基类的 super_class 指向 nil

    3. Method

    上面讲到,「找到对应的方法,然后执行」,那么这个「执行」是怎样进行的呢?下面就来介绍一下 Objective-C 中的方法调用。

    先来看一下 Method 在头文件中的定义:

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

    Method 被定义为一个 objc_method 指针,在 objc_method 结构体中,包含一个 SEL 和一个 IMP ,同样来看一下它们的定义:

    // SEL
    typedef struct objc_selector *SEL;
    
    // IMP
    typedef id (*IMP)(id, SEL, ...); 
    
    

    1、先说一下 SELSEL 是一个指向 objc_selector 的指针,而 objc_selector 在头文件中找不到明确的定义。

    我们来测试以下代码:

    SEL sel = @selector(viewDidLoad);
    NSLog(@"%s", sel);          // 输出:viewDidLoad
    SEL sel1 = @selector(viewDidLoad1);
    NSLog(@"%s", sel1);         // 输出:viewDidLoad1
    
    

    可以看到, SEL 不过是保存了方法名的一串字符。因此我们可以认为, SEL 就是一个保存方法名的字符串

    由于一个 Method 只保存了方法的方法名,并最终要根据方法名来查找方法的实现,因此在 Objective-C 中不支持下面这种定义。

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

    2、再来说 IMP 。可以看到它是一个「函数指针」。简单来说,「函数指针」就是用来找到函数地址,然后执行函数。(「函数指针」了解一下

    这里要注意, IMP 指向的函数的前两个参数是默认参数, idSEL 。这里的 SEL 好理解,就是函数名。而 id ,对于实例方法来说, self 保存了当前对象的地址;对于类方法来说, self 保存了当前对应类对象的地址。后面的省略号即是参数列表。

    3、到这里, Method 的结构就很明了了。 Method 建立了 SELIMP 的关联,当对一个对象发送消息时,会通过给出的 SEL 去找到 IMP ,然后执行。

    Objective-C 中,所有的方法调用,都会转化成向对象发送消息。发送消息主要是使用 objc_msgSend 函数。看一下头文件定义:

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

    可以看到参数列表和 IMP 指向的函数参数列表是相对应的。 Runtime 会将方法调用做下面的转换,所以一般也称 Objective-C 中的调用方法为「发送消息」。

    [self doSomething];
    
    
    objc_msgSend(self, @selector(doSomething));
    
    

    4、上面看到 objc_msgSend 会默认传入 idSEL 。这对应了两个隐含参数, self_cmd 。这意味着我们可以在方法的实现过程中拿到它们,并使用它们。下面来看个例子:

    - (void)testCmd:(NSNumber *)num {
    
        NSLog(@"%ld", (long)num.integerValue);
    
        num = [NSNumber numberWithInteger:num.integerValue-1];
    
        if (num.integerValue > 0) {
            [self performSelector:_cmd withObject:num];
        }
    }
    
    

    尝试调用:

    [self testCmd:@(5)];
    
    

    上面会按顺序输出 5, 4, 3, 2, 1 ,然后结束。即我们可以在方法内部用 _cmd 来调用方法自身。

    5、上面已经介绍了方法调用的大致过程,下面来讨论类之间继承的情况。重新回去看 objc_class 结构体的定义,当中包含一个指向父类的指针 super_class

    当向一个对象发送消息时,会去这个类的 methodLists 中查找相应的 SEL ,如果查不到,则通过 super_class 指针找到父类,再去父类的 methodLists 中查找,层层递进。最后仍然找不到,才走抛异常流程。

    下面的图演示了一个基本的消息发送框架:


    1852765-d5c23b880cf2a7c5.jpg

    6、当一个方法找不到的时候,会走拦截调用和消息转发流程。我们可以重写 +resolveClassMethod:+resolveInstanceMethod: 方法,在程序崩溃前做一些处理。通常的做法是动态添加一个方法,并返回 YES 告诉程序已经成功处理消息。如果这两个方法返回 NO ,这个流程会继续往下走,完整的流程如下图所示:

    1852765-3a683919c57a9cda.jpg

    4. Category

    我们来看一下 Category 在头文件中的定义:

    typedef struct objc_category *Category;
    
    struct objc_category {
        char * category_name;
        char * class_name;
        struct objc_method_list * instance_methods;
        struct objc_method_list * class_methods;
        struct objc_protocol_list * protocols;
    }   
    

    Category是一个指向 objc_category结构体的指针,在 objc_category 中包含对象方法列表、类方法列表、协议列表。从这里我们也可以看出, Category 支持添加对象方法、类方法、协议,但不能保存成员变量。

    注意:在 Category 中是可以添加属性的,但不会生成对应的成员变量、 gettersetter 。因此,调用 Category中声明的属性时会报错。

    我们可以通过「关联对象」的方式来添加可用的属性。具体操作如下:

    • 1、在 UIViewController+Tag.h 文件中声明property
    @property (nonatomic, strong) NSString *tag;
    
    • 2、在 UIViewController+Tag.m中实现 gettersetter。记得添加头文件 #import <objc/runtime.h> 。主要是用到 objc_setAssociatedObjectobjc_getAssociatedObject 这两个方法。
    static void *tag = &tag;
    
    @implementation UIViewController (Tag)
    
    - (void)setTag:(NSString *)t {
        
        objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSString *)tag {
        
        return objc_getAssociatedObject(self, &tag);
    }
    
    @end
    
    • 3、在子类中调用。
    // 子类 ViewController.m
    - (void)testCategroy {
        
        self.tag = @"TAG";
        NSLog(@"%@", self.tag);   // 这里输出:TAG
    }
    

    注意:当一个对象被释放后, Runtime 回去查找这个对象是否有关联的对象,有的话,会将它们释放掉。因此不需要我们手动去释放。

    注:
    深入理解Objective-C:Category

    相关文章

      网友评论

          本文标题:iOS Runtime 基础原理

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