目录

前言

本文会详细描述Objective-C运行时的各对象数底层据结构、类和原类、消息传递与转发、动态方法等技术方案. 文中底层代码实现均来自Apple open source; 本文篇幅较长, 文中描述加之有个人的一点理解, 主要用作记录和学习之用, 文笔粗陋, 技术菜鸡, 如有错误或不妥之处, 万望各位大佬不吝指教, 不胜感激!

runtime是什么

Objective-C runtime是一个动态运行库, 它给Objective-C语言的动态性提供了支撑. 所有的应用都会链接到该运行时库.

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 runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.

三个重要概念

在下述讲述过程中, 你应该注意三个非常重要的概念.即Class、SEL、IMP, 在这里我先把他们列出来, 后面我们会一一的深入讲到其内部结构和之间的关系.

typedef struct objc_class *Class;

typedef struct objc_object {

Class isa;

} *id;

typedef struct objc_selector *SEL;

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

一 各主要对象数据结构

1 objc_object

objc_object表示实例对象底层是结构体, 内部有一个私有的isa指针, 该指针指向了其类对象

struct objc_object {

private:

isa_t isa;

...

// isa相关操作

// 弱引用, 关联对象, 内存管理等等相关的操作

// 都是在此结构体中, 篇幅太长, 不再全部贴出

}

2 objc_class

objc_class继承自objc_object(所以肯定有isa指针), 表示类对象, 底层仍然是结构体, 其内部的isa指针, 指向了该类的元类对象. 同时, 内部的superclass指向了自身的父类对象, NSObject对象superclass指向了nil, cache是一个方法缓存结构体, bits是存储变量、属性、方法等的结构体

struct objc_class : objc_object {

// Class ISA;

Class superclass;

cache_t cache; // formerly cache pointer and vtable

class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {

return bits.data();

}

...

// 类相关的数据操作都是在此结构体中, 不再全部贴出

}

2.1 cache_t

缓存方法, 消息传递时, 会先通过哈希查找算法, 在此数据结构中查询是否有要执行的方法缓存, 如果有则快速执行该方法函数, 这样提高了消息传递的效率;

方法缓存策略, 是局部性原理的最佳应用;

本质是一个可增量的哈希表, 其内部维护了一个由bucket_t组成的结构体列表

struct cache_t {

struct bucket_t *_buckets;

mask_t _mask;

mask_t _occupied;

public:

struct bucket_t *buckets();

...

};

bucket_t内部存储了方法缓存key和无类型函数指针地址的映射关系, 在查找缓存时, 通知指定的key查找到具体的bucket_t, 再从bucket_t中查询到函数IMP地址, 进而去执行函数

struct bucket_t {

private:

cache_key_t _key;

IMP _imp;

public:

inline cache_key_t key() const { return _key; }

inline IMP imp() const { return (IMP)_imp; }

inline void setKey(cache_key_t newKey) { _key = newKey; }

inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);

};

2.2 class_data_bits_t

class_data_bits_t结构主要是对class_rw_r的封装

class_rw_r又是对class_ro_r的封装

struct class_rw_t {

// class_rw_t部分代码

uint32_t flags;

uint32_t version;

// 指向只读的结构体, 存储类初始内容

const class_ro_t *ro;

/*

三个可读写二维数组, 存储了类的初始化信息, 内容

*/

method_array_t methods;// 方法列表

property_array_t properties;// 属性列表

protocol_array_t protocols;// 协议列表

// 第一个子类

Class firstSubclass;

// 下一个同级类

Class nextSiblingClass;

};

class_ro_t结构

struct class_ro_t {

// class_ro_t部分代码

const char * name;

// class_ro_t存储的是类在编译期就确定的方法, 属性, 协议等

method_list_t * baseMethodList;

protocol_list_t * baseProtocols;

const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;

property_list_t *baseProperties;

method_list_t *baseMethods() const {

return baseMethodList;

}

};

需要注意的是, class_ro_t存储的是类在编译期就确定的内容信息, 而class_rw_t不仅包含了类在编译期的内容信息(其实是把class_ro_t的内容合并), 还包含了在运行时动态添加的类内容, 如分类添加的方法, 属性, 协议等内容; 一张图来表示上述结构之间的关系:

3 isa

在arm64为架构之前, isa指针存储了类或元类对象的地址信息, 从arm64架构开始对isa指针(非指针型指针)进行了优化, 用位域存储了除类或元类地址信息以外的其他信息, 如has_assoc表示是否设置关联对象

union isa_t {

isa_t() { }

isa_t(uintptr_t value) : bits(value) { }

Class cls;

uintptr_t bits;

struct {

// 标记位, 0 代表指针型isa, 1代表非指针型isa

uintptr_t indexed : 1;

// 是否有关联对象

uintptr_t has_assoc : 1;

// 是否有C 析构函数

uintptr_t has_cxx_dtor : 1;

// 存储当前对象的类或元类的内存地址

uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000

// 判断对象是否已经初始化

uintptr_t magic : 6;

// 对象是否有弱引用指针

uintptr_t weakly_referenced : 1;

// 当前对象是否有dealloc操作

uintptr_t deallocating : 1;

// 当前isa指针是否有外挂引用表

// 引用计数值大于isa所能存储最大值时

// 就会绑定一个sidetable散列表属性, 来存储更多的引用计数信息

uintptr_t has_sidetable_rc : 1;

// 额外的引用计数值

uintptr_t extra_rc : 19;

};

}

这里需要注意

isa所属对象是实例对象, 则其指向实例对象的类对象

isa所属对象是类对象, 则其指向类对象的元类对象

4 method_t

method_t是函数的底层数据结构, 是对函数的封装, Apple对函数的介绍在这里

struct method_t {

SEL name;// 函数名称

const char *types;// 函数返回值和参数

IMP imp;// 无类型函数指针指向函数体

struct SortBySELAddress :

public std::binary_function

const method_t&, bool> {

bool operator() (const method_t& lhs,

const method_t& rhs)

{ return lhs.name < rhs.name; }

};

};

4.1 函数的四要素

名称

返回值

参数

函数体

4.2 types

Apple使用了Type Encodings技术, 来实现类型编码, Objective-C 运行时库内部利用类型编码帮助加快消息分发.

结构是个列表, 包含了函数的返回值, 参数1, 参数2, 参数3 …, 其中函数的返回值存储在第0个位置, 因为函数只有一个返回值(Go支持多返回值), 而参数可以有多个.

对于一个无类型无参数的函数, 其types值为 “V@:”

- (void)method {

// 其中

// V对应返回值, 代表返回值类型为void

// @对应第一个参数, id类型代表一个对象, 默认第一个参数是对象本身(self), 且该参数是固定的

// :对应SEL, 代表该参数是个方法选择器, 且该参数是默认的第二个固定参数

}

5 一张图表明各数据结构之间的关系

二 实例对象、类对象和元类对象

一大佬(膜拜)画的一张图, 足以说明三者之间的关系.( Apple官网也有类似的描述,但是个人感觉没有下面这张图更精彩)

实例对象的isa指针指向其类对象

类对象的isa指针指向其元类对象

任何元类对象的isa指针都指向根元类对象

类对象的superclass指针指向其父类对象, 根类对象指向nil

元类对象的superclass指针指向其父元类对象, 根元类对象指向根类

其中, 根类在Objective-C中即为NSObject. 实例对象其实就是objc_object(), 类对象就是objc_class(); 上面讲到, objc_class()是继承自objc_object(), 因此类对象中也有isa指针

typedef struct objc_object {

Class isa;

} *id;

从底层数据结构可以看出, 类对象中存储了实例对象方法列表, 成员变量等内容; 同时, 元类对象中存储了类对象的类方法列表等内容;

1 实例方法调用时是如何查找的

当一个实例对象调用一个实例方法时

首先会根据该对象的isa指针, 查到到其类对象, 在类对象方法列表中查询是否有所调用方法的实现;

如果没有, 则类对象会根据自身的superclass指针查找其父类对象, 在父类对象方法列表中查询是否有所调用方法的同名方法实现;

递归调用直至根类对象, 如果中间有任何一步查询到了具体的方法实现, 就去执行具体的函数调用;

如果直至根类, 仍然没有找到方法实现, 则会调用系统两个方法, 然后走系统调用流程;

(BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

(BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

具体的消息传递流程请往下看

2 self和super

self是当前类的因此参数, 指向类的实例对象, 进行方法调用时, 代表从当前类开始进行查找

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

super本质是个编译器标识符, 仅代表方法查找时从当前对象所属父类开始查找方法实现

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

三 消息传递与消息转发

在开发中, 我们经常会碰见这样子一个错误 unrecognized selector sent to instance xx

大致意思是, 你调用了一个并不存在的方法. 现在, 我们会深入的探究一下为什么会出现这个异常.

其实上面这个异常会正好就是我们要讲的, 在Objective-C的消息机制中, 用OC消息机制来说: 如果消息在传递的过程中找不到具体的IMP, 内部就触发了消息转发机制, 而系统的消息转发机制默认实现是抛出上述的异常. 接下来, 我们分别讲述消息的传递和转发.

我们知道Objective-C是动态语言, 方法的调用并不像C的静态绑定一样, 在编译的时候就确定了程序运行时该调用哪个函数(C中没有方法实现会报错), 而是在运行时基于runtime这个动态运行时库通过一系列的查找才决定调用哪个函数, 这样的调用方式更加灵活, 我们甚至可以在运行时动态的修改某个方法的实现, 与当下流行的"热更新"技术有些类似. 而这个查找过程就是Objective-C的消息机制.

1 消息传递流程

在Objective-C中, 方法调用其实就是给某个对象发送消息, 在编译后的文件中我们发现, 底层都转变为函数调用

// 返回值, 参数1: 固定self, 参数2: 固定SEL, 后面是参数3, 参数4....

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

从代码中可以看到, 消息发送函数有两个默认的参数, 第一个是消息接受者receiver, 默认就是当前对象self; 第二个默认参数是SEL, SEL的本质的方法选择器selector; (阅读运行时的文档你会发现, 几乎所有方法调用都和selector有关系)! 所以, 我们的方法调用可以这样表示** [receiver selector] **, 那么这个selector究竟是何方神圣, 遗憾的是我在Apple和GNU提供的runtime代码中,都只找到了这一行代码.

typedef struct objc_selector *SEL;

不过Apple给了说明, 方法选择器selector就是个映射到C中的字符串. 根据我翻阅的各种资料都显示, selector就是个C字符串类型的方法名称.

Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

说了半天, 跟我们的运行时有什么关系(objc_msgSend()是[receiver selector]编译阶段实现的)?那么, objc_msgSend()函数在运行时是如何进一步调用的呢?

首先, 通过 recevier的isa指针寻找到recevier的class(类);

其次, 先在class中的cache list(缓存列表)查找是否有对应的缓存selector;

如果在缓存列表中查找到, 那么就根据selector(key)直接执行方法对应的IMP(value);

否则, 继续在 class的method list(方法列表)中查找对应的 selector;

如果没有找到对应的selector, 就继续在它的 superclass(父类)中寻找;

最后, 如果找到对应的 selector, 直接执行 recever 对应 selector 方法实现的 IMP(方法实现)

否则, 系统进入默认消息转发机制.

我们用一张图来表示上述流程

有时候, 我们会通过super调用, 其实道理是一样的, 编译后会生成objc_msgSendSuper()函数

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

而objc_super结构体内部, 消息接受者仍然是receiver当前实例对象, 与上面不唯一不同的是, self是从当前对象的类对象中开始查找对应实现, 而super则是跨过当前对象的类对象直接从类对象的父类对象开始查找方法实现;

struct objc_super {

/// Specifies an instance of a class.

__unsafe_unretained id receiver;

};

2 消息转发流程

在消息的传递过程中, 我们讲到, 如果receiver 找不到对应的selector的IMP实现, 则会进入系统的默认消息转发流程. 而系统默认处理消息转发的机制就会抛出unrecognized selector sent to instance xx异常, 然后结束整个消息转发. 如果想要避免这种情况的发生, 我们就需要在如果selector找不到的情况下在运行时动态的给receiver添加实现.

幸运的是虽然系统默认默认流程是抛异常, 但是在抛异常的方法调用过程中, 系统给我们开了口子, 让我们可以通过 动态解析、receiver重定向、消息重定向等对消息进行处理, 流程如下图:

2.1 消息动态解析

在系统处理消息转发的过程中, 首先会根据调用对象类型不同分别调用如下两个api, 我们可以通过重载在这两个方法内部动态添加方法, 进而避免crash

// 找不到类方法, 重载此类方法添加类方法实现

(BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

// 找不到实例方法, 重载此类方法添加实例方法实现

(BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

我们以实例方法为例举个

runtime 分类结构体_iOS 读懂runtime基础(一)相关推荐

  1. runtime 分类结构体_水性木器涂料的5大分类+4大配方事项

    水性涂料,又称水基涂料.用于木材涂饰的水性涂料有多种分类方法,常见的有按外观.用途.施工和固化方式.乳液类型.化学结构以及设备方法来分类命名的. 水性木器漆最重要的成分是其中的乳液和构成乳液的树脂类型 ...

  2. runtime 分类结构体_几种常见滚动轴承的分类方法

    一.按滚动轴承结构类型分类 1. 轴承按其所能承受的载荷方向或公称接触角的不同,分为: ①向心轴承----主要用于承受径向载荷的滚动轴承,其公称接触角从0到45.按公称接触角不同,又分为:径向接触轴承 ...

  3. c#往结构体里面读数据_C# 结构体和ListT类型数据转Json数据保存和读取

    1 一.结构体转Json2 3 public structFaceLibrary4 {5 public stringface_name;6 public byte[] face_Feature;7 } ...

  4. python attention机制_[深度应用]·Keras实现Self-Attention文本分类(机器如何读懂人心)...

    [深度应用]·Keras实现Self-Attention文本分类(机器如何读懂人心) 笔者在[深度概念]·Attention机制概念学习笔记博文中,讲解了Attention机制的概念与技术细节,本篇内 ...

  5. c++ 结构体赋值_《零基础看得懂的C语言入门教程》—(十二)结构体是这么回事

    一.学习目标 了解C语言的结构体的使用方法 了解C语言结构体的结构的赋值 了解多种C语言结构体变量的赋值方法和取值方法 目录 <零基础看得懂的C语言入门教程>--(二)简单带你了解流程 & ...

  6. ios runtime重要性_iOS:学习runtime的理解和心得

    作者:兴宇是谁 授权本站转载. Runtime是想要做好iOS开发,或者说是真正的深刻的掌握OC这门语言所必需理解的东西.最近在学习Runtime,有自己的一些心得,整理如下, 一为 查阅方便 二为 ...

  7. Go语言结构体指针为nil时的小坑

    结构体空指针的小坑 当我们使用其他包的结构体,方法的时候,通常需要先创建一个该结构体的对象,再去使用该对象的方法.但是如果我们声明的对象是一个结构体的指针的时候,就不能简单的使用var了.使用var的 ...

  8. Golang之funcval结构体

    Go语言中,函数是头等对象,将函数作为参数变量或返回值的情况称为function value.function value本质上是一个指针,指向runtime.funcval结构体,这个结构体里只有一 ...

  9. 7.IDA-创建结构体

    结构体分类 结构体的一个显著特点在于,结构体中的数据字段是通过名称访问,而不是像数组那样通过索引访问.不好的是,字段名称被编译器转换成了数字偏移量.结果,在反汇编代码清单中,访问结构体字段的方式看起来 ...

最新文章

  1. 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五)...
  2. 基于链表实现队列(基于Java实现)
  3. 基于 MongoDB 及 Spring Boot 的文件服务器的实现
  4. playbook 实例
  5. 基于jquery的上传插件Uploadify 3.1.1在MVC3中的使用
  6. 马斯克再带货狗狗币:超7成网友票选狗狗币为未来货币
  7. keras embeding设置初始值的两种方式
  8. git原理和常用操作
  9. c语言内部超链接,HTML5中文本元素超链接的属性
  10. Vue之安装Google开发插件
  11. 图片轮播的JS写法,通用涉及多个轮播
  12. 小白R语言数据可视化进阶练习一
  13. 第七次人口普查数据可视化---pyecharts
  14. python实现逻辑回归算法
  15. 教你如何把软件转移到另一台电脑?
  16. win8.1产品安装临时密钥
  17. Unity FPS显示工具
  18. 估值调整 - 凸性调整
  19. 有关图像生成的函数 .
  20. php计算用户留存,利用Python计算新增用户留存率

热门文章

  1. Hibernate 发展之路
  2. echo回声不能用了_回声消除的昨天、今天和明天
  3. 蓝桥杯2016省赛真题-剪邮票(dfs)
  4. A Simple Math Problem(矩阵快速幂)
  5. java二维数组合并_java怎么合并二维数组
  6. java nio ssl_java连接MQTT+SSL服务器
  7. 深度学习(3)手写数字识别问题
  8. STM32 (Cortex-M3) 中NVIC(嵌套向量中断控制)的理解
  9. PADS 中的 PIN TYPE 说明
  10. C#中的thread和task之 Thread ThreadPool