Objective-C Runtime 运行时一:从内存布局说起

本系列文章旨在简述iOS 中runtime为保障Objective-C动态特性所做的一些列操作,总体以Apple官方文档为基准,加以对其他大神文章的总结,以及笔者在学习使用过程中的一点点心得体会,尽量删繁就简只讲干货,尽量通俗易懂。水平有限,还望读者朋友们多多指正。

Objective-C Runtime 系列文章:

Objective-C Runtime 运行时一:内存布局

一、 Runtime使Objective-C成为一门动态语言

  • 编译型语言 VS 解释型语言(百度百科)

计算机只能直接理解机器语言,我们必须要把高级语言翻译成机器语言,才能执行高级语言编写的程序。编译,解释,两种方式只是翻译的时间不同。

  • 编译型语言:程序执行之前,需要专门的编译过程,把程序编译成机器语言的文件(exe文件),以后运行,直接使用编译的结果,不用重新翻译。
  • 解释性语言:程序不需要编译,在运行程序的时候才翻译。

Objective-C是编译型语言。

  • 编译型语言翻译只做一次,运行时不需要翻译,所以一般程序执行效率高;解释型语言每执行一次就要翻译一次,一般效率比较低。

  • 编译型语只翻译一次,意味着一次性编译完所有代码,耗费大量时间;解释型语言一般是翻译一句执行一句,等待时间较短。

  • 静态语言是怎么样的?

一般高级语言程序编译的过程:预处理、编译、汇编、链接。
一般高级语言(如C、C++)在编译、链接时就已经完成并确定程序的绝大多数决策,如类,类的属性、操作,方法的调用等,一切在程序编译完成之后运行之前都是清晰可见并确定的,这些事都需要一个好的编译器来完成(如gcc、clang)。

  • 动态语言Objective-C是怎么样的?

Objective-C总是尽可能地要把编译、链接阶段做的事放到运行时来做,显然编译器控制不到程序的运行阶段,这就是说还需要一个运行时系统(runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

Runtime库主要做下面几件事:

  • 封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
  • 找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。

二、 Runtime应用的时机及方式

Objective-C程序从三个层次与运行时系统交互:通过Objective-C源代码; 通过在Foundation框架的NSObject类中定义的方法; 并通过直接调用运行时函数。

1. Objective-C 源代码

runtime编译包含OC的类和方法的代码时,编译器会创建数据结构和函数来实现语言的动态特性。数据结构捕获类、类别、协议中的信息(变量、方法等)。runtime动态特性的核心功能就是发送消息。

在大多数情况下,运行时系统会自动运行并在幕后工作。您只需编写和编译Objective-C源代码就可以使用它。说白了就是runtime会根据需要在程序运行时自动插入runtime代码。

2. NSObject 的方法

Cocoa 中大多数类都是NSObject类的子类,也就继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,有时可用它来避免循环引用。

方法 作用
description 重载为你定义的类提供描述内容
isKindOfClass: 是否是当前类或其派生类成员
isMemberOfClass: 是否是当前类成员
respondsToSelector: 对象能否响应指定的消息
conformsToProtocol: 对象是否实现指定协议类的方法
methodForSelector: 返回指定方法实现的地址
3. Runtime 库函数

Runtime 系统是由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

说人话就是:

  • Runtime是一函数库,纯C或汇编语言的,你可以直接调用其中的函数来实现你想要的功能。
  • Runtime太底层,一般时候一般人都用不到,万一用到了可以猛戳链接细看。

三、 类、对象、元类、类对象及内存布局

1. Class

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

//1. NSObject.h
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//2. objc.h
typedef struct objc_class *Class;

objc_class结构体:

//3. objc/runtime.h
struct objc_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;
objc_class 变量名 作用
isa Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)
super_class 指向该类的父类,如果该类已经是最顶层的根类(如NSObjectNSProxy),则super_classNULL
cache 用于缓存最近使用的方法(类似于计算机中的高命中率缓存)
version 我们可以使用这个字段来提供类的版本信息。

2. id & objc_object

// usr/include/objc.h
/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

objc_object结构体有且只有一个字段:isa指针,指向对象所属的类。当创建一个特定类的实例对象时,分配的内存包含objc_object数据结构+类的实例变量的数据。NSObject类的allocallocWithZone:方法使用函数class_createInstance来创建objc_object数据结构.

id是一个objc_object结构类型的指针。类似于C++中的泛型。

3. Meta Class(元类)

所有的类自身也是一个对象,我们可以向这个对象发送静态消息(即调用类方法)。

  • 问题1
    既然类自身也是对象,那么它也是一个objc_object指针,也包含isa指针。那么问题来了,这个isa指针指向什么呢?为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的objc_class结构体。由此meta-class应运而生。
  • 问题2
    既然meta-class也是一个类,是类就是另一个类的类对象,可以向类对象发送一个消息,那么问题又来了,类对象的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-classisa指向基类的meta-class,以此作为它们的所属类。

即,任何NSObject继承体系下的meta-class都使用NSObjectmeta-class作为自己的所属类,而基类的meta-classisa指针是指向它自己。这样就形成了一个完美的闭环。

4. 内存布局

综上所述,再加上对objc_class结构体中super_class指针的分析(考虑父类的情况),我们就可以描绘出类及相应meta-class类的一个继承体系了,上一副经典的内存布局图。

OC内存布局(考虑父类)继承体系图.png

图实线是 super_class 指针,虚线是isa指针。

是不是又似懂非懂、似晕非晕了,没关系专业术语说完了,接下来说人话:

程序运行时,运行时系统通过自身的库函数在内存中(推测是代码区或静态区)为源代码中的每个类创建了类对象即meta-class(元类)的实例对象,类对象(类的代表)包含有类函数列表等。
如果我们要创建某个类的实例对象,对象的isa指针指向类对象(类的代表),类对象(类的代表)的isa指针指向根类(NSObject)的类对象(根类的代表),根类的isa指针指向根类的元类,而根类的元类是自身(NSObject)。 根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

也就是说,类对象是元类的实例,元类是根元类的实例,而根元类对象是自身的实例。类对象(即类)存储着一个类的所有实例方法,元类存储着一个类的所有类方法。

一个消息发送时:

  • 实例方法:发送给实例对象,根据对象的isa指针找到类对象访查cache和methodLists
  • 类方法:发送给类对象,根据类对象的isa指针找到元类访查cache和methodLists

这里未考虑父类存在的情况,详细内容后续文章单独介绍,敬请期待!!!

他山之石

本文特别感谢包括并不限于以下优秀文章:
Runtime奇技淫巧之类(Class)和对象(id)以及方法(SEL)
杨萧玉的博客(@杨萧玉HIT)

推荐阅读更多精彩内容