Objective-C Runtime浅析

580 阅读6分钟

在初学Objective-C的时候,觉得有很多陌生且奇怪的语法和特性。

比如NSObject *obj = [[NSObject alloc] init];这种语法;比如尝试调用空指针的函数并不会导致crash这种特性。直到有机会深入了解Objective-C Runtime,才多少有了一些理解。

如果你也对这种看似奇怪的写法感到好奇的话,相信这篇文章能够解答你的疑问。

背景

动态编程语言

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically.

Objective-C是一门“动态编程语言”,也就是尽可能把决策推迟到运行时。

什么是Objective-C Runtime

苹果官方文档给出的定义如下:

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.

Objective-C的动态性主要体现在如下3个方面 :

  • 动态类型:在运行时才确认对象的类型
  • 动态绑定:在运行时才确定对象的调用方法
  • 动态加载:在运行时才加载需要的资源或代码

runtime就是为Objective-C提供上述动态特性的库,runtime赋予了C语言面向对象的能力。

为什么要了解Runtime

Runtime为Objective-C提供的动态特性,给开发者提供了更多灵活性,这种灵活性可以在解决一些复杂问题时提供更多方案。 Method Swizzling就是一个很好的例子,大家不妨思考一下下面问题的解决方案:

为现有项目中所有view设置统一的背景颜色。(或者更具实际意义的问题,在某些情况下需要将App中所有页面置灰)

一般想到的方案可能是统一继承一个父类,在父类中实现上述要求。基于Runtime的Method Swizzling可以提供更方便的解决方案 。

另外了解Runtime也可以从底层辅助Debug。

总之,Runtime是一个重要知识点,接下来我们一起深入了解Runtime的运行机制。

Runtime

源码面前,了无秘密 ——《STL源码剖析》

对象和类的实现

上面提到Runtime为Objective-C提供动态类型、动态绑定等动态特性,在运行时才确定对象的类型、调用方法等信息。为此,我们首先需要了解Objective-C中对象是如何表示的,源码objc.h 中利用结构体表示对象:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa;
};

可以看出,对象只是对Class(类)的简单封装,我们继续探究Class的含义:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

objc_class在runtime.h 中给出了定义 :

struct objc_class {
    Class _Nonnull isa;
    Class _Nullable super_class;
    const char * _Nonnull name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list * _Nullable ivars;
    struct objc_method_list * _Nullable * _Nullable methodLists;
    struct objc_cache * _Nonnull cache;
    struct objc_protocol_list * _Nullable protocols;
};
/* Use `Class` instead of `struct objc_class *` */

上述结构体也就是Objective-C中类的表示方式,其中比较重要的成员包括:

  • ivars ,表示该Objective-C类的成员变量。

    struct objc_ivar {
        char * _Nullable ivar_name;
        char * _Nullable ivar_type;
        int ivar_offset;
    };
    
    struct objc_ivar_list {
        int ivar_count;
        /* variable length structure */
        struct objc_ivar ivar_list[1];
    };
    
  • methodLists,表示该Objective-C类中的函数

    struct objc_method {
        SEL _Nonnull method_name;
        char * _Nullable method_types;
        IMP _Nonnull method_imp;
    };
    
    struct objc_method_list {
        struct objc_method_list * _Nullable obsolete;
        int method_count;
        /* variable length structure */
        struct objc_method method_list[1];
    };
    
  • cache,用于缓存函数,提升性能。关于cache在性能优化方面的作用,美团技术团队的文章《 深入理解 Objective-C:方法缓存》值得一看 。

    struct objc_cache {
        unsigned int mask /* total = mask + 1 */;
        unsigned int occupied;
        Method _Nullable buckets[1];
    };
    
  • protocols:该Objective-C类中的协议(链表)

    struct objc_protocol_list {
        struct objc_protocol_list * _Nullable next;
        long count;
        __unsafe_unretained Protocol * _Nullable list[1];
    };
    

对象、类和元类

从上述源码中可以看到,对象结构体中只有一个类结构体指针,对象可以通过该指针找到与之对应的类,进而可以在类结构体中找到成员变量、成员函数等信息。 在这里插入图片描述

类结构体中还包含两个类结构体指针:

  • isa
  • super_class

super_class比较容易理解,是指向父类的指针,利用该指针可以实现继承。

isa指针存在的作用又是什么呢?个人理解,一个重要作用是实现类函数

In Objective-C, a class is itself an object with an opaque type calledClass. Classes can’t have properties defined using the declaration syntax shown earlier for instances, but they can receive messages.

类函数,顾名思义就是类拥有的函数。如果类本身也是一个对象的话,只要找到它对应的类结构体(meta-class)的话,就可以找到类函数了。 在这里插入图片描述

类这个“对象”对应的类,称为元类(meta-class),每个类都有一个与之对应的元类。 这样设计虽然方便,但也带来了一些问题,比如元类中isa指向哪里?元类的super_class指向哪里? 下图给出了答案: 在这里插入图片描述

  • 元类的isa指向哪里? 所有元类的isa指针都指向基类的元类。如果一个类没有父类,其元类就指向自身。
  • 元类的super_class指向哪里? 类对应元类的super_class指向该类父类的元类。

函数调用

In Objective-C, messages aren’t bound to method implementations until runtime. The compiler converts a message expression [receiver message] into a call on a messaging function,objc_msgSend. This function takes the receiverand the name of the method mentioned in the message—that is, the method selector—as its two principal parameters objc_msgSend(receiver, selector). Any argumentspassed in the message are also handed toobjc_msgSend objc_msgSend(receiver, selector, arg1, arg2, ...)

我们知道,在Objective-C中,[object methodName]表示调用对象objectmethodName方法。和C语言中直接按函数地址取用不同,Objective-C中的函数调用是通过Runtime中的objc_msgSend()实现的,也就是会将[object methodName]翻译成objc_msgSend(id self, SEL op, ...)

也就是说,Objective-C将函数调用,转化成了消息的传递,也正是这种转化,造成了文章开头提到的,尝试调用空指针函数不会导致crash的反常现象。 调用某个对象的函数,就是给对象发送消息,消息中携带的SEL可以将其理解为方法的ID,通过SEL可以在objc_class中的methodLists查找到方法的具体实现,进而执行。 在这里插入图片描述

如果在类中没有找到该函数,会通过类的super_class指针,去父类中查找,如图所示,循环往复直到基类。 在这里插入图片描述

如果基类中也没有找到,该消息就会被丢弃,但不会引发崩溃。

  • 类函数调用 上面提到过,Objective-C中类也是对象,对应元类中存储的就是类函数。所以类函数的调用,就是通过元类查找并执行。

到这里,我们对Objective-C的类和函数调用有了浅显的理解,现在看文章开头提到的语法NSObject *obj = [[NSObject alloc] init];,你有没有自己的理解呢?

[NSObject alloc]是给NSObject发送消息,也就是调用它的alloc方法:

Returns a new instance of the receiving class.

根据官方文档,作用是得到类NSObject的实例。然后调用这个实例的初始化函数init

参考

  1. Objective-C Runtime Programming Guide developer.apple.com/library/arc…
  2. iOS程序员面试笔试宝典 weread.qq.com/web/bookDet…
  3. Method Swizzling: What, Why & How medium.com/@grow4gaura…
  4. github.com/apple-oss-d…
  5. github.com/apple-oss-d…
  6. github.com/apple-oss-d…
  7. 深入理解 Objective-C:方法缓存 tech.meituan.com/2015/08/12/…
  8. Programming with Objective-C developer.apple.com/library/arc…
  9. Objective-C Runtime Programming Guide developer.apple.com/library/arc…
  10. alloc developer.apple.com/documentati…
  11. init developer.apple.com/documentati…