Objective-C底层汇总

用这篇文章来结束 Objective-C的底层总结。
既然是底层,那么一定是需要一定的Objective-C编程基础,阅读本文的前提,

知晓对象,类对象,元类(只要知道概念就好)
知晓类是struct(同样有概念就好)

本文所使用的runtime版本为756.2。

背景

Objective-C是C语言的严格超集--任何C语言程序不经修改就可以直接通过Objective-C编译器,在Objective-C中使用C语言代码也是完全合法的。Objective-C的面向对象语法源于Smalltalk消息传递风格。
通过这句话,我们可以得到两个信息:

  • Objective-C是C语言的严格超集。
  • Objective-C的面向对象语法源于Smalltalk消息传递风格。

那么我们来思考两个问题:

  • 如何使C语言实现一个 OOP 对象模型。
  • 如何 Smalltalk 风格的 Message 机制。

类的结构

我们平时使用的对象都是id类型定义的,都是objc_object这样的结构体,而后我们可以把id强转成其他任意的class类型,是因为class本质是objc_class结构体。而objc_class是继承objc_object的。也就是里式替换原则

objc_object

那么objc_object里都放了些什么呢?objc_object结构体内部可以分为五个部分,如图。
img
可以看出objc_object结构体主要提供了管理自身对象的一些接口,其中比较重要的就是isa_t结构了。

isa_t

下面来看一下isa_t的结构,其本质为联合体。以64位为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
union isa_t {
//isa 的两种初始化方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//两种互斥类型的isa。
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

//64位CPU架构 bits 的定义如下
uintptr_t nonpointer : 1; //是否是指针类型
uintptr_t has_assoc : 1; //是否有关联对象
uintptr_t has_cxx_dtor : 1; //是否有C++相关的析构方法
uintptr_t shiftcls : 44; //class 的地址
uintptr_t magic : 6; //系统架构标识
uintptr_t weakly_referenced : 1; //是否有弱引用
uintptr_t deallocating : 1; //是否正在释放
uintptr_t has_sidetable_rc : 1; //是否使用 SideTable 管理引用计数。当extra_rc 放不下时(2^8)使用
uintptr_t extra_rc : 8 //引用计数

联合体在公用内存的同时,其内部的成员也是内存互斥的,也就是说,如果cls存在,则bits不存在,反之相同。
因此isa_t有两种表现形式。一种为classcls指针,另种为bits。其区别如下图
isa
关于何时使用指针形式的isa_t,可以通过两个方面控制

  • 通过 Environment Variables 设置变量 OBJC_DISABLE_NONPOINTER_ISA 为 YES 或 NO
  • 通过 class 的 flag 设置。(下文会说明)

实验

下面我们通过lldb来证明下上面的结果。首先我们设置OBJC_DISABLE_NONPOINTER_ISANO
isa
我们先来获取person对象的class。分成如下几个步骤

  1. 获取实例的 isa
  2. 获取isa 的二进制形式
  3. 根据bits的定义和isa的二进制,我们要取其中的44位(3~47)的值。

实际上在定义bits的地方同样定义了一些宏,称为mask,我们可以通过&来获取相应的值。如图。
img
下我们把OBJC_DISABLE_NONPOINTER_ISAYES
img

其他

在最新的系统下(ios13或mac10.15下)对isa直接使用了指针类型,并未启用 指针优化,即使设置了OBJC_DISABLE_NONPOINTER_ISA也无效。

objc_class

objc_class作为objc_object的子类,实现了把各个类(NSObject)串联(继承)到了一起,并为runtime提供了基石。
通过objc_object基类提供的接口我们发现,其实对象(实例)是没有能力来“调用”方法的,因为在objc_object的定义中并没有方法相关的结构。是objc_class赋予了对象调用方法的能力。也是由objc_classruntime使得Objective-C具有了面向对象的能力
objc_object结构体内部可以分为四个部分,如图。
img

其中主要的结构为cache_tclass_data_bits_t

cache_t

cache_t是一种用来快速查找执行函数的一种机制。我们知道Objective-C为了实现其动态性,将函数地址的调用,包装成了SEL寻找IMP的过程 ,随之带来的负面影响就是降低了方法调用的效率,为了解决这一问题。Objc采用了方法缓存的机制来提高调用效率。
cache_t的特点如下。

  • 用于快速查找执行函数
  • 是可增量扩容的哈希表结构
  • 局部性原理的应用

cache_t结构体的定义如下。

1
2
3
4
5
6
struct cache_t {
struct bucket_t *_buckets; //缓存数组
mask_t _mask; //当前数组的需要扩容的临界值
mask_t _occupied; //数组的中被使用的容量
...
};

初步验证

下面我们来借助lldb还原下上面的场景。
img
我们将上面的代码执行step over
img
通过上面的例子我们能验证缓存确实存在bucket_t中,还有occupied代码缓存已使用的数量,但是,我们也发现并不是在数组的第一位按顺序存储的,而且数组的大小及存储规则还不清楚,下面我们结合源码和lldb来验证。

  1. 缓存如何命中。
  2. buckets如何创建。
  3. buckets如何管理缓存。

cache_fill_nolock

cache_fill_nolock是缓存体系提供给外部使用的api,主要服务于msgSend。通过调用方法,来触发cache_fill_nolock,此函数的作用为,查找,并填充缓存。
首先来看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();

// Never cache before +initialize is done
if (!cls->isInitialized()) return;

// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;

cache_t *cache = getCache(cls);
//SEL强转为 uintptr_t 类型的 无符号整形数字。
cache_key_t key = getKey(sel);

// Use the cache as-is if it is less than 3/4 full
//在已经使用数量的基础上+1,获得将要更新缓存之后的已使用数量
mask_t newOccupied = cache->occupied() + 1;
//获取当前缓存的总容量 capacity = mask + 1
mask_t capacity = cache->capacity();
//如果是空的缓存(occupied==0 && capacity==0)
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
// 重新开辟新的cache,第一次到这里capacity,mask = 0 所以使用 INIT_CACHE_SIZE,为4
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
//小于3/4 可以继续使用
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
// 超过3/4 要扩容
cache->expand();
}

// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
//根据Key(SEL)查找cache
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}

我们可以看到该方法的流程主要分为3个阶段。

  1. 通过getKey方法获取到一个正整型的key
  2. 缓存空间的判断策略(初始化,或者扩容,或者直接使用)。
  3. 通过key查找bucket

第一个阶段比较简单,就是通过类型转换来得到key。我们来看第二个阶段。

reallocate

如果是第一次使用缓存,一定会进入reallocate流程,进行初始化,开辟容量为4buckets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
//获取旧的buckets
bucket_t *oldBuckets = buckets();
//开辟新的buckets
bucket_t *newBuckets = allocateBuckets(newCapacity);

// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this

assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

//根据新的buckets,更新buckets和mask
setBucketsAndMask(newBuckets, newCapacity - 1);

if (freeOld) {
//释放就的buckets
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}

这个方法也比较简单。我们需要关注四点。

  1. mask被设置的值为开辟空间的newCapacity-1
  2. 在开辟新的数组时,会释放旧buckets (加入回收数组)
  3. 旧的缓存不会被计入新的数组中。保持局部性原理为最佳
  4. 选择在3/4的时间扩容,是为了保持hash 的高效查找,因为hash table的剩余容量越小,意味着冲突也越多。

    回到cache_fill_nolock的第二阶段,上面分析了第一个ifreallocate,在下面的else if中,也就是如果当前设置后的缓存数仍然小于总量的3/4,则继续使用当前buckets,否则else进行扩容expend

    expend

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void cache_t::expand()
    {
    cacheUpdateLock.assertLocked();
    //获取旧的容量
    uint32_t oldCapacity = capacity();
    //计算需要开辟新空间的大小 = 旧的大小 * 2
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
    // 溢出不进行容量加大,但是仍会从新申请。
    // mask overflow - can't grow further
    // fixme this wastes one bit of mask
    newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
    }

这个方法比较简单。我们需要关注两点。

  1. 在不超过uint32_t的情况下,每次扩容为原来大小的2
  2. 如果超过了uint32_t,则重新申请跟原来一样大小的buckets

接下来就是做后一个阶段了,查找缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
//参数k, SEL强转为 uintptr_t 类型的 无符号整形数字。
assert(k != 0);
//拿到buckets数组
bucket_t *b = buckets();
//获取mask = 总容量 - 1。
mask_t m = mask();
//k & mask,哈希算法
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {//找到的key是空的bucket,或者找到了目标k(SEL)
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
//循环条件为 最后尝试的位置 = 开始位置。
} while ((i = cache_next(i, m)) != begin);

// hack
// 按照正常的查找逻辑,不会走到这里,因为buckets的可使用容量为总容量的3/4,那么上面的while一定会命中一个空的桶,然后返回。
// 所以到这里一定是缓存出现了一些问题,具体的问题会在bad_cache中 log 出来
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}

从这里我们可以发现,cache_t采用哈希表的方式来查找对应的bucket,哈希函数为cache_hash,目标数组为buckets。步骤如下:

  1. 准备哈希查找的k和目标数组buckets
  2. 线性探测的方式查找目标k,直到找到k或者空的bucket
  3. 异常处理。

到这里大体的流程已经梳理清晰,但还有一个比较重要的问题。我们知道msgSend是可多线程并发执行的,那么cache_t在更新缓存时,如何处理线程安全的问题呢。

  1. 在每次执行缓存填充,和扩充都会添加对应的互斥锁。
  2. 在更新bucketsmask时,会使用mega_barrier 来保证buckets的更新一定早于mask。(如果不保证会有数组越界的问题)
  3. 在回收旧的buckets时,会把需要释放的buckets加入一个全局的数组garbage_refs中。等待真正没有其他线程使用数组中的元素时,在进行释放。

代码验证
img
step over
img
继续 step over
img

总结

img

class_data_bits_t

class_data_bits_t是一个uintptr_t 类型的指针,在编译阶段会指向class_ro_t,在运行时会封装成class_rw_t

class_ro_t

class_ro_t的结构如下
img
class_ro_t存储了很多在编译时期就确定的类的信息。其内部包含了类名、ivar、方法、属性等。是只读类型的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_ro_t {
uint32_t flags; //配合mask 可以用来判断,元类,根类等
uint32_t instanceStart; //non-fragile判断依据
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
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;
}
};

其中instanceStartinstanceSize是为了实现Non Fragile特性。

In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.

下面我们来读取下class_ro_t的信息,为了证明class_ro_t是在编译期期确定,我们要在runtime的入口下个符号断点objc_init(这也可以先运行一次,先得到类对象的地址,第二次符号断点直接使用)。由于objc_initruntime入口(runtime还没有处理类的元数据)。
img

class_rw_t

从上面的章节看出class_ro_t存储的大多是类在编译时就已经确定的信息,但是Objective-C又是一门动态语言,因此需要另一个可以运行时读写数据,也就是class_rw_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags; //同样结合mask 使用
uint32_t version;

const class_ro_t *ro; //ro的指针

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName; //是计算机语言用于解决实体名称唯一性的一种方法,做法是向名称中添加一些类型信息,用于从编译器中向链接器传递更多语义信息

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif

...
};

值得注意的是method_array_t的结构会有三种变化。

  • 空。
  • 一个一维数组的指针。(默认为一维数组,就算一个类有category,如果没有实现+load,就也是一维数组,没有实现+load,就意味这个类可以懒加载)
  • 指向列表的指针数组。(当一个类存在category且这个类无法懒加载,或者动态修改方法列表如addMethod。就会生成二维列表,)

如果从二进制的角度讲,只要这个类存在于objc_catlist或者objc_nlcatlist,那么他的method_array_t就为二维数组。
那么class_rw_t是什么时候被赋值的呢。

realizeClass

首先简单看一个runtime初始化的流程。

  1. objc_init
  2. read_images。
    1. 初始化全局class map,读取macho的 objc_classlist 初始化全局class map, objc_classrefs。
    2. 读取 __objc_selrefs等段读取相关。进行 sel fix up
    3. 读取 protocol
    4. 读取所有 非懒加载 类,并调用 realizeClass进行初始化,进一步调用 methodizeClass(Attach categories)
    5. 读取categories,把读取出来的categories放到全局的map 里,如果cat 关联的cls已经被realize则进行remethodizeClass
  3. 对所有非懒加载的类 和 category 执行 +load。

类的可读写元数据的初始话主要发生在 realizeClassmethodizeClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static Class realizeClass(Class cls)
{
runtimeLock.assertLocked();
...

// fixme verify class is not in an un-dlopened part of the shared cache?
//RO_FUTURE 标志位来记录是否可以直接强转。
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
//不能转换,则开辟空间。
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}

isMeta = ro->flags & RO_META;

rw->version = isMeta ? 7 : 0; // old runtime went up to 6
...
优先执行supercls 和metacls 的realize
检测isa 优化
更新实例大小,关联子类父类,元类根类。
...
// Attach categories
methodizeClass(cls);

return cls;
}

realizeClass方法中会根据ro中的flagsRO_FUTURE来判断是否可以直接进行类型转换,如不可以则需要另外给rw申请内存。同时也注意到,在生成rw后,并没有进行方法属性等列表的赋值,而方法等列表的拼接是在methodizeClass
下面我们来尽代码验证。
img
img

methodizeClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void methodizeClass(Class cls)
{
runtimeLock.assertLocked();
//获取realizeClass 中设置好的rw
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;

// Install methods and properties that the class implements itself.
//开始拼接method,注意经过拼接之后仍然是1维数组。
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
...
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
...
}

由于此时执行到methodizeClass时,rw中的methods 还为初始化,所以,在加下来的attachLists过程中一定产生的是一维数组(因为使用 ro->baseMethods中的方法列表拼接,但是 ro->baseMethods 一定是一维数组。)
同时我们截取一段rwmethods也就是code>list_array_tt的attachLists实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
//二维数组已经存在
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
//当前list 还未初始化,需要添加的数组(二维)的元素=1,此时直接使用这个addedLists[0]
// 0 lists -> 1 list
list = addedLists[0];
}
else {
//如果list已经存在,且二维数组不存在,那么构建二维数组结构。
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}

继续验证。
img
下面我们来查看methods里面的内容。
img
那么什么时候才会变成二维数组呢,答案是 attachCategories之后,注意存在category不一定就会调用attachCategories的呦,有兴趣的可以看下源码。下面我们直接看lldb
img
接下来 step over
img
注意如果你的category直接是基于NSObject那么调试结果会有不一样,因为NSObject非懒加载的类,如果是NSObject的子类(Person)的category又没有实现+load那么一样不会执行attachCategories,因为Person的category此时不会被编译到objc_catlist或者objc_nlcatlist段,runtime也就获取不到对应的Categories,如果Person实现了+load,那么代表此category为非懒加载,需要生成对应的data 段,之后就可以被attachCategories

总结

至此我们对于开篇提出的第一个问题

Objective-C是如何基于C语言来设计对象模型。

通过上面的梳理,我们可以认为,Objective-C为了使C具有面向对象以及动态性的能力,会通过如下手段来实现:

  1. 使用struct来构建对象模型。
  2. struct中通过定义isa来区分对象的“类型”,从而实现内省等特性。
  3. struct中通过定义supercls来实现继承的能力。
  4. struct中定义ro来存储对象的属性,方法。
  5. struct中定义rw来实现动态性。使得开发者有运行时修改的能力。

下面我们来看看“消息”。

消息查找

介绍完Objective-C的基础数据结构,接下来就是方法的调用了。在讨论“消息”之前。我们先来思考一下,我们总是在说在objc中,调用方法实际上是发送消息,那么这个消息的机制是什么? 函数调用的本质应该是什么?我们可以通过一段C的代码来看下

1
2
3
4
5
6
7
8
9

void func(){

}

int main(int argc, char * argv[]) {

func();
}

下面通过clangobjdump命令来生成可执行文件并查看汇编实现
img
img
我们可以看到,函数的调用,实际上就是“地址”的跳转。而函数的地址是在编译期就能确定的(除一些动态库函数外),关于如何在编译期确定地址可以查看之前的静态链接文章

但是Objective-C是一门动态性语言,如果直接使用编译得到的地址,就不存在动态性,那么Objective-C是如何解决这个问题的呢

msgSend

此汇编结果为iOS13.2.3真机版本的runtime。可参考arm64汇编文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
libobjc.A.dylib`objc_msgSend:
//判断self 是否 = nil
-> 0x1810aa080 <+0>: cmp x0, #0x0 ; =0x0
//如果是0或者小0,则跳转至 0x1810aa0f8 处
0x1810aa084 <+4>: b.le 0x1810aa0f8 ; <+120>

//读取x0的“值”存入x13中。x13=isa
0x1810aa088 <+8>: ldr x13, [x0]

//x16 = x13 & isamask
//xl6 = cls
0x1810aa08c <+12>: and x16, x13, #0xffffffff8

//读取x16偏移16字节的值,存入 x11
//x11 = cache_t和mask_t共同组成,前48位为cache_t
0x1810aa090 <+16>: ldr x11, [x16, #0x10]

/* CacheLookup */
//苹果开源的runtime(mac10.14)中,cache存储在cls偏移16字节处
//但在目前的版本中(10.15),cache_t会和mask_t公用这8个字节。使用0xffffffffffff获取cache_t。
//x10 = bucket_t = (x11 & 0xffffffffffff)
0x1810aa094 <+20>: and x10, x11, #0xffffffffffff

//x11 >> 48获取mask_t,
//x12 = mask_t & SEL = cache_hash(buckets的索引)
0x1810aa098 <+24>: and x12, x1, x11, lsr #48

//获取索引值相对buckets首地址的偏移量,cache_hash << 4,因为每个bucket占16字节,所以把索引 * 16;
//x12:取索引对应的bucket = buckets首地址+索引偏移量
0x1810aa09c <+28>: add x12, x10, x12, lsl #4

//读取bucket的内容,分别存入 x17,x9
//x17 = imp, x9 = cache_key(SEL)
0x1810aa0a0 <+32>: ldp x17, x9, [x12]

//cachekey == SEL
0x1810aa0a4 <+36>: cmp x9, x1

//cachekey != SEL,跳转到 0x1810aa0b4
0x1810aa0a8 <+40>: b.ne 0x1810aa0b4 ; <+52>


/* CacheHit */
0x1810aa0ac <+44>: eor x17, x17, x16
//直接执行cache中的IMP
0x1810aa0b0 <+48>: br x17

/* CheckMiss */
//调用x9 是否为0 。如果为0 ,这说明这个bucket是空桶,就跳转 _objc_msgSend_uncached->lookupIMPOrForward
0x1810aa0b4 <+52>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached

//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0b8 <+56>: cmp x12, x10

//如果相等,说明找了一圈,仍没有找到,则跳转至 0x1810aa0c8。
0x1810aa0bc <+60>: b.eq 0x1810aa0c8 ; <+72>

//bucket_t的地址详情偏移16,相当于 取当前bucket_t的前一个元素。
//把imp和key分别赋值给 x17 x9
0x1810aa0c0 <+64>: ldp x17, x9, [x12, #-0x10]!

//跳转回0x1810aa0a4,相当于 向前循环遍历查找buckets
0x1810aa0c4 <+68>: b 0x1810aa0a4 ; <+36>

//到这里说明 x12 是buckets中的第一个bucket。
//x11 为mask和buckets,这里右移 44位,说明了buckets实际只是用了 44bit而不是48
//这里相当于把x12重新指向buckets的最后一个bucekt。
//x12 = buckets->last
0x1810aa0c8 <+72>: add x12, x12, x11, lsr #44

//将bucket的imp和key 写入 x17 和 x9
0x1810aa0cc <+76>: ldp x17, x9, [x12]

//对比 SEL == key
0x1810aa0d0 <+80>: cmp x9, x1

//不相等 跳转到 0x1810aa0e0,判断cache_key 是否为空
0x1810aa0d4 <+84>: b.ne 0x1810aa0e0 ; <+96>

/* CacheHit */
//如果相等则
0x1810aa0d8 <+88>: eor x17, x17, x16
//执行x17:IMP
0x1810aa0dc <+92>: br x17

/* CheckMiss */
//如果此时 x9 == 0 则执行_objc_msgSend_uncached
0x1810aa0e0 <+96>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached

//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0e4 <+100>: cmp x12, x10

//如果等于则跳转0x1810aa0f4 -> _objc_msgSend_uncached
0x1810aa0e8 <+104>: b.eq 0x1810aa0f4 ; <+116>

//不相等则 向前查找
0x1810aa0ec <+108>: ldp x17, x9, [x12, #-0x10]!
//跳转0x1810aa0d0
0x1810aa0f0 <+112>: b 0x1810aa0d0 ; <+80>
//跳转_objc_msgSend_uncached
0x1810aa0f4 <+116>: b 0x1810aa3c0 ; _objc_msgSend_uncached

//msgSeng刚开始的第二行判断,这里是nil或者tagged
0x1810aa0f8 <+120>: b.eq 0x1810aa130 ; <+176>
0x1810aa0fc <+124>: adrp x10, 294986
0x1810aa100 <+128>: add x10, x10, #0x340 ; =0x340
0x1810aa104 <+132>: lsr x11, x0, #60
0x1810aa108 <+136>: ldr x16, [x10, x11, lsl #3]
0x1810aa10c <+140>: adrp x10, 294986
0x1810aa110 <+144>: add x10, x10, #0x2a8 ; =0x2a8
0x1810aa114 <+148>: cmp x10, x16
0x1810aa118 <+152>: b.ne 0x1810aa090 ; <+16>

// ext tagged
0x1810aa11c <+156>: adrp x10, 294986
0x1810aa120 <+160>: add x10, x10, #0x3c0 ; =0x3c0
0x1810aa124 <+164>: ubfx x11, x0, #52, #8
0x1810aa128 <+168>: ldr x16, [x10, x11, lsl #3]
0x1810aa12c <+172>: b 0x1810aa090 ; <+16>
0x1810aa130 <+176>: mov x1, #0x0
0x1810aa134 <+180>: movi d0, #0000000000000000
0x1810aa138 <+184>: movi d1, #0000000000000000
0x1810aa13c <+188>: movi d2, #0000000000000000
0x1810aa140 <+192>: movi d3, #0000000000000000
0x1810aa144 <+196>: ret
0x1810aa148 <+200>: nop
0x1810aa14c <+204>: nop
0x1810aa150 <+208>: nop
0x1810aa154 <+212>: nop
0x1810aa158 <+216>: nop
0x1810aa15c <+220>: nop

整个objc_msgsend可分为四个阶段:

  1. 准备工作,包括获取类对象mask_tcash_hash(查找索引)buckets
  2. 获取cash_hash所在bucketsbucket,并向前遍历查找,这里有3中情况:
    1. 如果找到则调用IMP
    2. 如果找到的bucket.key == 0,即bucket为空桶,则调用 objc_msgSend_uncached
    3. 如果找到第一个bucket,则跳转到3。
  3. buckets的最后一个向前查找:
    1. 如果找到则调用IMP
    2. 如果找到的bucket.key == 0,即bucket为空桶,则调用 objc_msgSend_uncached
  4. 调用objc_msgSend_uncached

伪代码

由于汇编看起来不是很直观,可直接参考下面的伪代码,同时这部分伪代码没有处理关于参数和返回值的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void discover_msgSend(id self,SEL sel1){
if (self == nil) {
return;
}
SEL sel = sel1;
Class clsHasMask = *(Class *)self;
Class cls = (Class)((uintptr_t)clsHasMask & 0xffffffff8);
struct cache_t * cache = (struct cache_t *)((__bridge void *)cls + 0x10);

struct bucket_t *_buckets = (struct bucket_t *)((uintptr_t)cache->_buckets & 0xffffffffffff);
uintptr_t mask = (uintptr_t)cache->_buckets >> 48;
uintptr_t cache_hash = (uintptr_t)sel & mask;
struct bucket_t* bucket = (_buckets + cache_hash*1);//cache_hash*16
/* CacheLookup */
while ((*bucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*bucket)._key == 0) {
goto _objc_msgSend_uncached;
}
/* 查找到第一个元素了 */
if (bucket == _buckets) {
goto second_loop;
}
bucket = (bucket-1);//[x12, #-0x10]
}
if(((*bucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*bucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
goto _objc_msgSend_uncached;
}
return;

second_loop:
{
int64_t offset = ((uintptr_t)cache->_buckets >> 44);
struct bucket_t* lastBucket = (_buckets + offset/16);
/* CacheLookup */
while ((*lastBucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*lastBucket)._key == 0) {
goto _objc_msgSend_uncached;
}
lastBucket = (lastBucket-1);//[x12, #-0x10]
}
if(((*lastBucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*lastBucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
/* JumpMiss */
goto _objc_msgSend_uncached;
}
return;
}
_objc_msgSend_uncached:
{
printf("\n _objc_msgSend_uncached -> lookUpImpOrForward");
return;
}
}

通过上面的流程我们发现,objc_msgSend会有两次的查找行为,这又是为什么呢?我们可以通过runtime的源码中找到答案。

Clone scanning loop to miss instead of hang when cache is corrupt.
The slow path may detect any corruption and halt later.

我们将第一个循环称为:原始循环,第二个称为:检查循环。
首先假设出现了缓存系统出现了异常,则此时有两种情况:

  • 仍然有一部分缓存是可用的,那么此检查的存在是不会影响该可用缓存部分的,因为arm64下缓存本来就是在buckets中向前进行写入的。所以当遍历到buckets的第一个bucket时的正常逻辑就是跳转到最后一项继续查找
  • 有一部分不可用的缓存,为了避免在原始循环中再次被重新使用,可以使用该检查循环跳出查找过程。

objc_msgSend_uncached

objc_msgSend的汇编查找过程中,我们发现如果在缓存中找不到IMP。则会调用objc_msgSend_uncached。最终会调用到lookUpImpOrForward

lookUpImpOrForward

lookUpImpOrForward实现了一套查找 IMP 的标准路径,也就是在消息转发(Forward)之前的逻辑。类似的方法还有lookUpImpOrNil,这两个API的区别在于lookUpImpOrNil不会返回含有forward类型的IMP,也就是说不会触发消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// Optimistic cache lookup
if (cache) {
//cache_getImp 同样为汇编实现。
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
//在方法搜索期间,将保持runtimeLock,以使方法查找+缓存填充相对于方法添加成为原子。
runtimeLock.lock();
checkIsKnownClass(cls);
//如果类还没有realize,则先进行realize,一般懒加载的类会走此方法
if (!cls->isRealized()) {
realizeClass(cls);
}
//如果类还没有初始化,则先进行初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}


retry:
runtimeLock.assertLocked();
//获取当前类缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;
//查找当前类的方法列表
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
//如果找到则填充到缓存
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

//查找父类的缓存和方法列表
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
//如果找到且imp !=_objc_msgForward_impcache, 则填充到缓存到当前类的缓存中
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}

// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//如果找到则填充到缓存到当前类的缓存中
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}

// No implementation found. Try method resolver once.
//尝试动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}

// No implementation found, and method resolver didn't help.
// Use forwarding.
//没有执行动态方法解析,则获取存入一个forward 类型的 IMP。
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlock();
//返回IMP,可能是真正的实现,也可能是_objc_msgForward_impcache。
return imp;
}

该方法的流程如下:

  1. 保证类已经经过realizeinitialize,关于realizeClass已经在类的结构中介绍过了。
  2. 获取当前类的缓存或查找当前类的方法列表。如果找到则写入缓存。
  3. 查找父类的缓存或父类的方法列表,如果找到则写入缓存。
  4. 如果父类也没有,则进入动态方法解析阶段,并返回 2 重新尝试。
  5. 如果没有实现动态方法解析,则存入一个forward类型的IMP
  6. 返回IMP,可能是真正的实现,也可能是forward

接下里我们来详细分析 2 ~ 4的流程,而5,6将在消息转发章节分析。而流程 2 ~ 4 可以分为两个主要的函数,

  1. 获取缓存 (当前类或父类)。
  2. 查找方法列表 (当前类或父类)。
  3. 缓存填充。

cache_getImp

cache_getImp仍然使用汇编实现。其实现跟objc_msgsend中的CacheLookup阶段相似,区别是objc_msgsend中的CacheLookupCheckMiss时会调用objc_msgSend_uncached,而cache_getImp则会在CheckMiss中返回 0。这里不在重复提供汇编代码,直接参考runtime源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义汇编入口
STATIC_ENTRY _cache_getImp
//同样先获取 cls,类对象
GetClassFromIsa_p16 p0
//进行缓存查找环节,在objc_msgsend中已经分析过了。这里参数传入 “GETIMP”
//如果CacheLookup查找失败则会返回LGetImpMiss。objc_msgsend是返回objc_msgSend_uncached
CacheLookup GETIMP

LGetImpMiss:
//把返回值写入0并返回。
mov p0, #0
ret

END_ENTRY _cache_getImp

cache_getImp调用过后,如果没有缓存,则会查找当前类的方法列表。

getMethodNoSuper_nolock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();

assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?

for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}

return nil;
}

经过上一章节对class 结构的讲解,我们知道,methods的结构可能是二维数组也有可能是一维数组。这里通过对methodsbeginListsendLists进行迭代,使得迭代时总能得到一个装满method的数组,从而减少类型判断。
接下来我们来看search_method_list

search_method_list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);

if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
... debug
return nil;
}

__builtin_expect表示对结果的一种期望:多数都是ture,也就是说,这里多数的时候都会继续调用findMethodInSortedMethodList,否则就会通过线性遍历来查找method。
需要注意的是,只有当MethodList已经是有序的时候,才能够进行有序的遍历方式,一下为对MethodList排序的时机:

  • methodizeClass
  • attachCategories
  • addMethod
  • addMethods

其实也就是当方法列表的结构发生改变的时候,就会对该列表进行排序(注意是方法所在的列表进行排序,而不是rw里面的所有方法列表,如果是二维数组,只需重新排列当前方法所在的列表即可),排序的规则为:按照 SEL 的地址升序排列。

findMethodInSortedMethodList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);

const method_t * const first = &list->first;
const method_t *base = first; //base为二分查找的左起点
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//每次对 count/2。
for (count = list->count; count != 0; count >>= 1) {
//左起点到count 的中间点。
probe = base + (count >> 1);

uintptr_t probeValue = (uintptr_t)probe->name;

if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
//如果目标sel > 当前的probeValue,则需要向后查找。
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}

return nil;
}

该函数为一个二分查找的流程。有下面几个主要标记构成:

  • 二分查找的左右节点:base,count。
  • 中间点:probe
  • 对比查找标记,觉得向左还是向右:keyValue probeValue。

因此方法列表的查找流程可以总结为:

log_and_fill_cache

在缓存查找和方法列表查找之后,就是进行缓存填充,这里如果在父类的方法列表找到了对应的IMP,则会把该IMP缓存在当前类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}

log_and_fill_cache非常简单,命名也非常的清晰,分为log和填充缓存的步骤。而缓存填充最终会调用到cache_fill_nolock
cache_fill_nolock在上面的类结构中已经说明过了。

下面我们来总结一下消息查找的流程:
img
接下来我们进入消息转发的流程处理。

动态解析&消息转发

在上文的objc_msgSend中我们发现,如果无法找到IMP则会尝试进行到 method resolver流程。

动态解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_resolveMethod(Class cls, SEL sel, id inst)
{ //类对象或元类, 实例或类对象
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

可以发现_class_resolveMethod的流程分为两步,按照是否为元类进行区分。首先我们来看非元类的情况

实例方法的动态解析

如果cls不是元类,就用实例方法的动态解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());

if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

  1. 首先确定现了Resolver 相关方法,如果没实现直接返回。
  2. 调用resolveInstanceMethod,获得返回值resolved
  3. 重新进行lookUpImpOrNil并禁用resolver,目的是如果resolveInstanceMethod对该SEL进行实现,则把添加的IMP缓存起来。
  4. 根据resolvedimp,还有环境变量进行判断(假设开启了打印日志变量):
    1. 如果实现了resolved方法,并且也添加了对应的imp,则根据环境环境变量打印日志
    2. 否则,则说明resolved方法虽然返回了tureimp却没有实现。

上面的环境环境变量可以通过OBJC_PRINT_RESOLVED_METHODS来控制是否开启。

类方法的动态解析

我们注意到在_class_resolveMethod中的类方法流程中有一段注释:

try [nonMetaClass resolveClassMethod:sel]
and [cls resolveInstanceMethod:sel]。

这里的nonMetaClass_class_resolveMethod参数中的inst(因为走得这里,cls一定是元类)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void resolveClassMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
assert(cls->isMetaClass());

if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//获取实例(类)对应的类对象,这个方法会根据传入的inst(也就是类),会判断是不是根元类等。最后返回一个类对象。
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveClassMethod总体流程与resolveInstanceMethod相仿,有个区别就是调用对应解析方法(resolveClassMethod/resolveInstanceMethod)的时候,类方法的动态解析要根据(类对象和元类)获取对应的类对象。然后在给对应的类对象发送动态解析的消息。

还有一个疑问点,再来看class_resolveMethod这个方法的后半部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_resolveMethod(Class cls, SEL sel, id inst)
{ //类对象或元类, 实例或类对象
if (! cls->isMetaClass()) {
...
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
//cls 为元类,该方法内部会调用:msgSend(cls,...),因此相当于到NSObject的根元类中调用resolveInstanceMethod。
}
}
}

我们注意到在调用类方法的动态解析之后,又进行了一步判断:当前类没有处理,则进行到根元类的。为什么要这样设计呢?
我们首先来回归一个调用类方法的场景:
当我们调用一个类方法时,如果该类方法没有实现,那么最终会调用NSObject的实例方法。为什么要这样设计请参考文末关于isa答疑部分。
因此:
如果当前类没有对该类方法进行动态解析,那么也应该到NSObject去找,看是否有同名的示例方法,而对于示例方法的处理要是用_class_resolveInstanceMethod

消息转发

lookUpImpOrForward环节中,如果没有进行动态方法解析,那么最终会返回_objc_msgForward_impcache。我们可以在arm64的汇编中找到该函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache


ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

通过上面汇编代码可知。转发的主要流程都是通过汇编来实现的。下面我们来还原真机的场景,首先加入objc_msgForward_impcache符号断点:

1
2
libobjc.A.dylib`_objc_msgForward_impcache:
-> 0x199436560 <+0>: b 0x199436580 ; _objc_msgForward

step into

1
2
3
4
libobjc.A.dylib`_objc_msgForward:
-> 0x199436580 <+0>: adrp x17, 294986
0x199436584 <+4>: ldr x17, [x17, #0xc50]
0x199436588 <+8>: br x17

由于 ___forwarding___内容过多,这里直接整理了一份流程图。
forward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
CoreFoundation`___forwarding___:
//进行相关准备,比如获取cls,clsname等
...

//class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))
0x1997157ec <+144>: bl 0x1994392e0 ; class_respondsToSelector
//和 0 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
//如果无法进转发,则跳转到0x199715810:进行签名转发流程
0x1997157f0 <+148>: cbz w0, 0x199715810 ; <+180>
0x1997157f4 <+152>: mov x0, x21
0x1997157f8 <+156>: mov x1, x26
0x1997157fc <+160>: mov x2, x23
//msgsend(receiverClass, @selector(forwardingTargetForSelector:))
-> 0x199715800 <+164>: bl 0x199436080 ; objc_msgSend
0x199715804 <+168>: cmp x0, #0x0 ; =0x0
0x199715808 <+172>: ccmp x0, x21, #0x4, ne
0x19971580c <+176>: b.ne 0x199715acc ; <+880>
//获取僵尸对象
0x199715810 <+180>: adrp x1, 137
0x199715814 <+184>: add x1, x1, #0x70e ; =0x70e
0x199715818 <+188>: mov x0, x24
0x19971581c <+192>: mov w2, #0xa
0x199715820 <+196>: bl 0x199773964 ; symbol stub for: cbRead.cold.2
//如果是僵尸对象,则跳转0x199715ba8:
0x199715824 <+200>: cbz w0, 0x199715ba8 ; <+1100>
0x199715828 <+204>: adrp x8, 227524
0x19971582c <+208>: add x24, x8, #0xb89 ; =0xb89
0x199715830 <+212>: mov x0, x22
0x199715834 <+216>: mov x1, x24
//class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))
0x199715838 <+220>: bl 0x1994392e0 ; class_respondsToSelector
//和 0 比较(Compare),如果结果为零(Zero)就转移
0x19971583c <+224>: tbz w0, #0x0, 0x199715bb8 ; <+1116>
0x199715840 <+228>: mov x0, x21
0x199715844 <+232>: mov x1, x24
0x199715848 <+236>: mov x2, x23
//msgsend(receiverClass, @selector(methodSignatureForSelector:))
0x19971584c <+240>: bl 0x199436080 ; objc_msgSend

//没有方法签名。跳转0x199715c24:SEL判断流程
0x199715850 <+244>: cbz x0, 0x199715c24 ; <+1224>
//存在方法签名
0x199715854 <+248>: mov x22, x0
0x199715858 <+252>: adrp x8, 227530
0x19971585c <+256>: add x1, x8, #0x3af ; =0x3af

//[methodSignature _frameDescriptor]
//返回一个struct,第一个成员为:struct NSMethodFrameArgInfo;
0x199715860 <+260>: bl 0x199436080 ; objc_msgSend
0x199715864 <+264>: mov x24, x0
//读取返回值(methodSignature)的第一个成员。
0x199715868 <+268>: ldr x8, [x0]
0x19971586c <+272>: ldrh w8, [x8, #0x22]
0x199715870 <+276>: ubfx x8, x8, #6, #1
//判断是否是返回值是否是isStruct。
0x199715874 <+280>: cmp x8, x25

//跳转到forwardingStackInvocation流程
0x199715878 <+284>: b.eq 0x1997158cc ; <+368>
0x19971587c <+288>: mov x0, x23
0x199715880 <+292>: bl 0x19944dc44 ; sel_getName
0x199715884 <+296>: ldr x8, [x24]
0x199715888 <+300>: ldrh w8, [x8, #0x22]
0x19971588c <+304>: adrp x9, 128
0x199715890 <+308>: add x9, x9, #0x1c2 ; =0x1c2
0x199715894 <+312>: adrp x10, 147
0x199715898 <+316>: add x10, x10, #0x1b4 ; =0x1b4
0x19971589c <+320>: tst w8, #0x40
0x1997158a0 <+324>: csel x8, x10, x9, eq
0x1997158a4 <+328>: cmp x25, #0x0 ; =0x0
0x1997158a8 <+332>: csel x9, x10, x9, eq
0x1997158ac <+336>: sub sp, sp, #0x20 ; =0x20
0x1997158b0 <+340>: stp x8, x9, [sp, #0x8]
0x1997158b4 <+344>: str x0, [sp]
0x1997158b8 <+348>: adrp x1, 249580
0x1997158bc <+352>: add x1, x1, #0xe10 ; =0xe10
0x1997158c0 <+356>: orr w0, wzr, #0x4
0x1997158c4 <+360>: bl 0x1996fe9d0 ; CFLog
0x1997158c8 <+364>: add sp, sp, #0x20 ; =0x20

/* forwardStackInvocation */
0x1997158cc <+368>: mov x0, x21
0x1997158d0 <+372>: bl 0x1994387e8 ; object_getClass
0x1997158d4 <+376>: adrp x8, 227530
0x1997158d8 <+380>: add x26, x8, #0x519 ; =0x519
0x1997158dc <+384>: mov x1, x26
//是否实现 forwardStackInvocation
0x1997158e0 <+388>: bl 0x1994392e0 ; class_respondsToSelector
//如果未实现,则跳转:0x1997159b4 forwardInvocation 环节
0x1997158e4 <+392>: cbz w0, 0x1997159b4 ; <+600>
0x1997158e8 <+396>: adrp x8, 294262
0x1997158ec <+400>: ldr x8, [x8, #0xa70]
0x1997158f0 <+404>: cmn x8, #0x1 ; =0x1
0x1997158f4 <+408>: b.ne 0x199715b8c ; <+1072>
0x1997158f8 <+412>: adrp x8, 284546
0x1997158fc <+416>: ldr x0, [x8, #0xe28]
0x199715900 <+420>: adrp x8, 227530
0x199715904 <+424>: add x1, x8, #0x532 ; =0x532
0x199715908 <+428>: mov x2, x22
0x19971590c <+432>: bl 0x199436080 ; objc_msgSend
0x199715910 <+436>: mov x25, x0
0x199715914 <+440>: adrp x8, 294262
0x199715918 <+444>: ldr x1, [x8, #0xa68]
0x19971591c <+448>: add x8, x1, #0xf ; =0xf
0x199715920 <+452>: and x8, x8, #0xfffffffffffffff0
0x199715924 <+456>: mov x9, x8
0x199715928 <+460>: adrp x16, 249510
0x19971592c <+464>: ldr x16, [x16, #0xbb0]
0x199715930 <+468>: blr x16
0x199715934 <+472>: mov x9, sp
0x199715938 <+476>: sub x23, x9, x8
0x19971593c <+480>: mov sp, x23
0x199715940 <+484>: mov x0, x23
0x199715944 <+488>: bl 0x1994200c0 ; __bzero
0x199715948 <+492>: add x8, x25, #0xf ; =0xf
0x19971594c <+496>: and x8, x8, #0xfffffffffffffff0
0x199715950 <+500>: mov x9, x8
0x199715954 <+504>: adrp x16, 249510
0x199715958 <+508>: ldr x16, [x16, #0xbb0]
0x19971595c <+512>: blr x16
0x199715960 <+516>: mov x9, sp
0x199715964 <+520>: sub x27, x9, x8
0x199715968 <+524>: mov sp, x27
0x19971596c <+528>: adrp x8, 294262
0x199715970 <+532>: ldr x0, [x8, #0xa60]
0x199715974 <+536>: mov x1, x23
0x199715978 <+540>: bl 0x19944b900 ; objc_constructInstance
0x19971597c <+544>: adrp x8, 227530
0x199715980 <+548>: add x1, x8, #0x551 ; =0x551
0x199715984 <+552>: mov x0, x23
0x199715988 <+556>: mov x2, x22
0x19971598c <+560>: mov x3, x20
0x199715990 <+564>: mov x4, x27
0x199715994 <+568>: mov x5, x25
0x199715998 <+572>: bl 0x199436080 ; objc_msgSend
0x19971599c <+576>: mov x0, x21
0x1997159a0 <+580>: mov x1, x26
0x1997159a4 <+584>: mov x2, x23
0x1997159a8 <+588>: bl 0x199436080 ; objc_msgSend
0x1997159ac <+592>: orr w21, wzr, #0x1
0x1997159b0 <+596>: b 0x199715a08 ; <+684>
/* forwardInvocation */ 环节
0x1997159b4 <+600>: mov x0, x21
0x1997159b8 <+604>: bl 0x1994387e8 ; object_getClass
0x1997159bc <+608>: adrp x8, 227524
0x1997159c0 <+612>: add x25, x8, #0xba5 ; =0xba5
0x1997159c4 <+616>: mov x1, x25
//是否响应 forwardInvocation
0x1997159c8 <+620>: bl 0x1994392e0 ; class_respondsToSelector
0x1997159cc <+624>: cbz w0, 0x199715c00 ; <+1188>
0x1997159d0 <+628>: adrp x8, 284546
0x1997159d4 <+632>: ldr x0, [x8, #0xe28]
0x1997159d8 <+636>: adrp x8, 227530
0x1997159dc <+640>: add x1, x8, #0x4b2 ; =0x4b2
0x1997159e0 <+644>: mov x2, x22
0x1997159e4 <+648>: mov x3, x20

0x1997159e8 <+652>: bl 0x199436080 ; objc_msgSend
0x1997159ec <+656>: mov x23, x0
0x1997159f0 <+660>: mov x0, x21
0x1997159f4 <+664>: mov x1, x25
0x1997159f8 <+668>: mov x2, x23
//调用 forwardInvocation
0x1997159fc <+672>: bl 0x199436080 ; objc_msgSend
0x199715a00 <+676>: mov x25, #0x0
0x199715a04 <+680>: mov w21, #0x0
0x199715a08 <+684>: ldrb w8, [x23, #0x34]
0x199715a0c <+688>: cbz w8, 0x199715a44 ; <+744>
0x199715a10 <+692>: ldr x8, [x24]
0x199715a14 <+696>: ldrb w9, [x8, #0x22]
0x199715a18 <+700>: tbz w9, #0x7, 0x199715a44 ; <+744>
0x199715a1c <+704>: ldr x9, [x23, #0x8]
0x199715a20 <+708>: ldr w10, [x8, #0x1c]
0x199715a24 <+712>: add x11, x20, x10
0x199715a28 <+716>: ldrb w12, [x8, #0x20]
0x199715a2c <+720>: add x9, x9, x10
0x199715a30 <+724>: ldr x0, [x11, x12]
0x199715a34 <+728>: ldr x1, [x9, x12]
0x199715a38 <+732>: ldr x8, [x8]
0x199715a3c <+736>: ldr w2, [x8, #0x10]
0x199715a40 <+740>: bl 0x1997731d8 ; symbol stub for: -[NSDictionary getKeys:].cold.1
0x199715a44 <+744>: adrp x8, 227530
0x199715a48 <+748>: add x1, x8, #0x454 ; =0x454
0x199715a4c <+752>: mov x0, x22
0x199715a50 <+756>: bl 0x199436080 ; objc_msgSend
0x199715a54 <+760>: ldrb w8, [x0]
0x199715a58 <+764>: cmp w8, #0x76 ; =0x76
0x199715a5c <+768>: b.eq 0x199715a74 ; <+792>
0x199715a60 <+772>: cmp w8, #0x56 ; =0x56
0x199715a64 <+776>: b.ne 0x199715a8c ; <+816>
0x199715a68 <+780>: ldrb w8, [x0, #0x1]
0x199715a6c <+784>: cmp w8, #0x76 ; =0x76
0x199715a70 <+788>: b.ne 0x199715a8c ; <+816>
0x199715a74 <+792>: cbz w21, 0x199715a80 ; <+804>
0x199715a78 <+796>: mov x0, x23
0x199715a7c <+800>: bl 0x199454130 ; objc_release
0x199715a80 <+804>: adrp x20, 284552
0x199715a84 <+808>: add x20, x20, #0x150 ; =0x150
0x199715a88 <+812>: b 0x199715b50 ; <+1012>
0x199715a8c <+816>: ldr x20, [x23, #0x10]
0x199715a90 <+820>: cbz w21, 0x199715b50 ; <+1012>
0x199715a94 <+824>: adrp x8, 284546
0x199715a98 <+828>: ldr x0, [x8, #0xcd8]
0x199715a9c <+832>: adrp x8, 227525
0x199715aa0 <+836>: add x1, x8, #0xa93 ; =0xa93
0x199715aa4 <+840>: mov x2, x20
0x199715aa8 <+844>: mov x3, x25
0x199715aac <+848>: bl 0x199436080 ; objc_msgSend
0x199715ab0 <+852>: adrp x8, 227524
0x199715ab4 <+856>: add x1, x8, #0x9f6 ; =0x9f6
0x199715ab8 <+860>: bl 0x199436080 ; objc_msgSend
0x199715abc <+864>: mov x20, x0
0x199715ac0 <+868>: mov x0, x23
0x199715ac4 <+872>: bl 0x199454130 ; objc_release
0x199715ac8 <+876>: b 0x199715b50 ; <+1012>
0x199715acc <+880>: tbz x0, #0x3f, 0x199715af8 ; <+924>
0x199715ad0 <+884>: adrp x8, 249510
0x199715ad4 <+888>: ldr x8, [x8, #0xcf8]
0x199715ad8 <+892>: ldr x8, [x8]
0x199715adc <+896>: eor x8, x8, x0
0x199715ae0 <+900>: ubfx x9, x8, #60, #3
0x199715ae4 <+904>: cmp x9, #0x7 ; =0x7
0x199715ae8 <+908>: ubfx x8, x8, #52, #8
0x199715aec <+912>: add x8, x8, #0x8 ; =0x8
0x199715af0 <+916>: csel x8, x9, x8, ne
0x199715af4 <+920>: cbz w8, 0x199715b04 ; <+936>
0x199715af8 <+924>: str x0, [x20]
0x199715afc <+928>: mov x20, #0x0
0x199715b00 <+932>: b 0x199715b50 ; <+1012>
0x199715b04 <+936>: mov x21, x0
0x199715b08 <+940>: adrp x8, 294261
0x199715b0c <+944>: ldrb w8, [x8, #0xe78]
0x199715b10 <+948>: tbnz w8, #0x4, 0x199715c94 ; <+1336>
0x199715b14 <+952>: mov x0, x21
0x199715b18 <+956>: bl 0x19971e5c0 ; getAtomTarget
0x199715b1c <+960>: mov x22, x0
0x199715b20 <+964>: str x0, [x20]
0x199715b24 <+968>: adrp x0, 249510
0x199715b28 <+972>: ldr x0, [x0, #0xd00]
0x199715b2c <+976>: mov w4, #0x0
0x199715b30 <+980>: mov x1, x20
0x199715b34 <+984>: mov x2, x20
0x199715b38 <+988>: orr w3, wzr, #0x400
0x199715b3c <+992>: bl 0x199717b80 ; __invoking___
0x199715b40 <+996>: ldr x8, [x20]
0x199715b44 <+1000>: cmp x8, x22
0x199715b48 <+1004>: b.ne 0x199715b50 ; <+1012>
0x199715b4c <+1008>: str x21, [x20]
0x199715b50 <+1012>: ldur x8, [x29, #-0x58]
0x199715b54 <+1016>: adrp x9, 249510
0x199715b58 <+1020>: ldr x9, [x9, #0xbc8]
0x199715b5c <+1024>: ldr x9, [x9]
0x199715b60 <+1028>: cmp x9, x8
0x199715b64 <+1032>: b.ne 0x199715ba4 ; <+1096>
0x199715b68 <+1036>: mov x0, x20
0x199715b6c <+1040>: sub sp, x29, #0x50 ; =0x50
0x199715b70 <+1044>: ldp x29, x30, [sp, #0x50]
0x199715b74 <+1048>: ldp x20, x19, [sp, #0x40]
0x199715b78 <+1052>: ldp x22, x21, [sp, #0x30]
0x199715b7c <+1056>: ldp x24, x23, [sp, #0x20]
0x199715b80 <+1060>: ldp x26, x25, [sp, #0x10]
0x199715b84 <+1064>: ldp x28, x27, [sp], #0x60
0x199715b88 <+1068>: ret
0x199715b8c <+1072>: adrp x0, 294262
0x199715b90 <+1076>: add x0, x0, #0xa70 ; =0xa70
0x199715b94 <+1080>: adrp x1, 249532
0x199715b98 <+1084>: add x1, x1, #0x4d0 ; =0x4d0
0x199715b9c <+1088>: bl 0x199772c5c ; symbol stub for: -[NSMutableSet removeObjectsInOrderedSet:range:].cold.1
0x199715ba0 <+1092>: b 0x1997158f8 ; <+412>
0x199715ba4 <+1096>: bl 0x19935a190 ; __stack_chk_fail
0x199715ba8 <+1100>: mov x0, x21
0x199715bac <+1104>: mov x1, x24
0x199715bb0 <+1108>: mov x2, x23
0x199715bb4 <+1112>: bl 0x19976d908 ; ___forwarding___.cold.2
//获得父类
0x199715bb8 <+1116>: mov x0, x22
0x199715bbc <+1120>: bl 0x199439370 ; class_getSuperclass
//把父类 存入 x22
0x199715bc0 <+1124>: mov x22, x0
0x199715bc4 <+1128>: mov x0, x21
//获得类名
0x199715bc8 <+1132>: bl 0x1994389a0 ; object_getClassName
//类名存入x20
0x199715bcc <+1136>: mov x20, x0
//父类如果不为空则跳转0x199715c0c:打印log *** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead
0x199715bd0 <+1140>: cbnz x22, 0x199715c0c ; <+1200>

//如果没有父类,和的当前类名及日志信息
0x199715bd4 <+1144>: mov x0, x21
0x199715bd8 <+1148>: bl 0x1994389a0 ; object_getClassName
0x199715bdc <+1152>: sub sp, sp, #0x20 ; =0x20
0x199715be0 <+1156>: stp x20, x0, [sp, #0x8]
0x199715be4 <+1160>: str x21, [sp]
0x199715be8 <+1164>: adrp x1, 249580
0x199715bec <+1168>: add x1, x1, #0xd90 ; =0xd90
0x199715bf0 <+1172>: orr w0, wzr, #0x4
//log *** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'
0x199715bf4 <+1176>: bl 0x1996fe9d0 ; CFLog
0x199715bf8 <+1180>: add sp, sp, #0x20 ; =0x20
//在看sel 是不是注册过,如果没有也要打印下
0x199715bfc <+1184>: b 0x199715c24 ; <+1224>
0x199715c00 <+1188>: add x0, x19, #0x8 ; =0x8
0x199715c04 <+1192>: mov x1, x21
0x199715c08 <+1196>: bl 0x19976d994 ; ___forwarding___.cold.4
/* CFlog 日志 */
0x199715c0c <+1200>: stp x21, x20, [sp, #-0x10]!
0x199715c10 <+1204>: adrp x1, 249580
0x199715c14 <+1208>: add x1, x1, #0xdb0 ; =0xdb0
0x199715c18 <+1212>: orr w0, wzr, #0x4
0x199715c1c <+1216>: bl 0x1996fe9d0 ; CFLog
0x199715c20 <+1220>: add sp, sp, #0x10 ; =0x10
//获取0x199715c24 开始为 获取注册的 SEL 过程
0x199715c24 <+1224>: mov x0, x23
0x199715c28 <+1228>: bl 0x19944dc44 ; sel_getName
0x199715c2c <+1232>: mov x20, x0
0x199715c30 <+1236>: bl 0x19944dda4 ; sel_getUid
// 是否注册过
0x199715c34 <+1240>: cmp x0, x23
// 如果注册过,则进行doesNotRecognizeSelector流程。
0x199715c38 <+1244>: b.eq 0x199715c5c ; <+1280>
//进行 异常log selector "xxx" for message "xxx" does not match selector known to Objective C runtime
0x199715c3c <+1248>: sub sp, sp, #0x20 ; =0x20
0x199715c40 <+1252>: stp x20, x0, [sp, #0x8]
0x199715c44 <+1256>: str x23, [sp]
0x199715c48 <+1260>: adrp x1, 249580
0x199715c4c <+1264>: add x1, x1, #0xdd0 ; =0xdd0
0x199715c50 <+1268>: orr w0, wzr, #0x4
0x199715c54 <+1272>: bl 0x1996fe9d0 ; CFLog
0x199715c58 <+1276>: add sp, sp, #0x20 ; =0x20
//下面为调用doesNotRecognizeSelector 流程
0x199715c5c <+1280>: mov x0, x21
0x199715c60 <+1284>: bl 0x1994387e8 ; object_getClass
0x199715c64 <+1288>: adrp x8, 227524
0x199715c68 <+1292>: add x20, x8, #0xb4b ; =0xb4b
0x199715c6c <+1296>: mov x1, x20
//class_respondsToSelector(receiverClass, doesNotRecognizeSelector:)
0x199715c70 <+1300>: bl 0x1994392e0 ; class_respondsToSelector
//object "xxx" of class "xxx" does not implement forwardInvocation:

0x199715c74 <+1304>: cbz w0, 0x199715c8c ; <+1328>
0x199715c78 <+1308>: mov x0, x21
0x199715c7c <+1312>: mov x1, x20
0x199715c80 <+1316>: mov x2, x23
//msgSend(receiverClass, doesNotRecognizeSelector:)
0x199715c84 <+1320>: bl 0x199436080 ; objc_msgSend
0x199715c88 <+1324>: brk #0x1
0x199715c8c <+1328>: mov x0, x21

//object "xxx" of class "xxx" does not implement forwardInvocation:
0x199715c90 <+1332>: bl 0x19976d964 ; ___forwarding___.cold.3
0x199715c94 <+1336>: bl 0x19976d8f8 ; ___forwarding___.cold.1

总结

现在我们来回答第二个问题:消息的机制是什么?
消息机制:向target发送消息动态寻找函数实现地址并调用的过程。
target:receiver( class / instance )。
消息:selector。
动态寻找:动态解析+消息传递(父类)+消息转发。
实现地址:IMP。

其实就是msgSend(receiver, sel,…)的一套完整流程。这一套流程也是来自与SmallTalk。

why?

isa(如何理解一切皆“是”对象?)

“对象”都是相对而言的,在Objc中使用isa来实现。实现万物皆对象,有个特殊的问题,就是根对象如何处理?
问:如果根元类的isa 为什么要指向自己?指向nil行不行,指向其他行不行,根元类的supercls为什么要指向根类(NSObject)?
从开篇提出的第一个问题的回答我们知道,isa是用来实现“串联起对象,类,类对象”。同时也实现构建了类方法,实例方法。下面来看一个例子。
[objc doSomething]的本质是?

objc 是什么(isa) 在哪里找方法
实例
元类 元类
元类 根元类 根元类
根元类

objc 是根元类,此时会出现一个问题,即:去哪里找这doSomething。那么objc是如何解决的呢?
首先一定不能为nil,如果为nil,就意味着 objc 是无意义(不知是什么类型?)的,其次无法进行方法调用,那么如果指向其他可以么?
其实是可以的,但是没有指向自身更好。
根元类的方法只能存在isa 指向的那个类(比如NSObject),则势必会造成入侵(NSObject类要除了要存储实例对象的方法,还要存类对象)。对象的结构(isa,class_data_bits_t等)定义也会变得混乱。
附上一张isa指向图
isa

supercls

为什么所有的类把NSObject作为根父类(间接或直接继承NSObject)?(为什么根元类的父类为NSObject)。
答:NSObject提供了运行时,OOP对象模型,以及内存管理相关的基本能力, 对象通过继承 NSObject ,才可以获得上述功能。

那么如何创建一个不继承NSObject 的类呢,或者说跟NSObject同级的类?

1
2
3
4
5
6
7
__attribute__((objc_root_class))
@interface MyClass

@end
@implementation MyClass

@end

这个类没有任何功能,都需要开发者自己实现。

消息转发

为什么需要消息转发这样的机制?
这里引用smalltalk的一段话。

When a message is sent to an object, a method will be selected and executed. Since we cannot know, in general, the class of the object until run-time, the method cannot be selected until the message is actually sent. This is called “dynamic binding”。

因为objc的方法实现地址并不是直接由编译器来确定的(是要运行时经过msgsend在ro里面去查找)。因此是不能直接在编译阶段就确定一个方法是否是有实现的,所以需要实现一个类似“编译器方法检查”的运行时机制。

那么objc怎么实现这个机制呢?
这个也简单,编译器在发现方法不存在时,会直接报错,那运行时也可以这样做。也就是doesNotRecognizeSelector。在smalltalk中称为doesNotUnderstandMessage-Not-Understood

不过显然objc考虑的更周全,虽然要抛出doesNotRecognizeSelector,但是由于其dynamic binding的特性,使得在抛出异常之前,objc提供了另外的消息处理机会。

也是“消息传递”和“动态”的一种思想提现。

其他

Smalltalk之父或者说面向对象之父(之一)的Alan Kay曾写过:

I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” - that is what the kernal of Smalltalk/Squeak is all about.

The big idea is “messaging”!!

参考文献

Smalltalk overview
Objective-C
runtime