iOS - Runtime 中 Class、消息机制、super 关键字

image.png

Objective-C 是一门动态语言,这就意味着消息传递和类以及对象的创建都在运行时完成,这个核心的库是由 C\C++ 和汇编编写的,保证其系统运行的高效性。

isa

这个老朋友我们见了无数次了,在 arm64 架构之前,isa 仅仅是一个普通的指针,存储 Class、Meta-Class 对象的地址。
在 arm64 后,isa 变成了联合体(union)类型。这个类型可以像 struct 那样存储更多的信息。

我们可在 objc 源码中看到 isa 的结构并非是 Class 类型而是联合体:

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

ISA_BITFIELD定义是这样的:

# define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 19

这种表现形式是位域。

存储的某些信息是不需要一个完整的字节的,仅仅需要 1 个或几个二进制位,就可以通过位域来存储。位域的形式为:类型说明符(int、unsigned int 或 signed int)位域名: 位域长度,如:int a: 8;

位域中的字段

通过位域来存储更丰富的信息,正是苹果对内存优化的体现,上节中位域列表的各个字段的含义为:

nonpointer:0 表示普通指针,存储类对象及元类对象的地址,1 表示优化后的指针,通过位域列表存储更多信息。

has_assoc:是否设置过关联对象,若没有,则 release 时更快。

has_cxx_dtor:是否有 C++ 的析构函数,若没有,release 时更快。

shiftcls:存储类对象和元类对象的内存地址。

magic:用于在调试时分辨对象是否未完成初始化。

weakly_referenced:是否被若引用指向。

deallocating:对象是否正在释放。

extra_rc:存储的值为引用计数器减 1。

has_sidetable_rc:引用计数器是否过大无法存储在 isa 中,若为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中。

做个简单的验证,假如有 Test 类,无属性,在另一个类中使用它:

Test* t = [[Test alloc] init];
NSLog(@"%@", t);

在第二句加断点,进入 LLDB 调试环境借助命令:
print/x t->isa
得到打印:

(Class) $0 = 0x000001a10000cdc1 Test

将该地址复制到系统计算器中:


image

最后一位为 1 说明 nonpointer 位为 1,说明该 isa 指针是 arm64 优化过后的指针,存储了更多信息。

倒数第二位为 0,说明 has_assoc 位为 0,说明该类未设置关联对象,例子中我没有给 Test 类设置关联对象。

倒数第三位为 0,说明 has_cxx_dtor 位为 0,说明该类没有析构函数。(析构函数类似 dealloc 函数)

接下来的 33 位,如图:


image.png

表示字段 shiftcls,存放着类对象地址或者元类对象的值。

接下来的 6 位 01 1010 表示字段 magic,表示对象已经初始化成功,执行完 allocinit 后它的值为 1a,在源码中也有体现:

#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

接下来的一位为 0,为 weakly_referenced 位,表示该对象未被弱引用指向过。

接下来一位为 0,为 deallocating 位,表示该对象没有正在被释放。

接下来一位为 0,为 has_sidetable_rc 位,表示引用计数存储在后 19 位,若引用计数并没有存在后 19 位的时候该位为 1.

最后十九位为 0,为 extra_rc 位,用来存放引用计数 - 1。所以都是 0。

Objective-C 对象的分类以及 isa、superclass 指针 中提到,在 arm64 架构下,isa 需要和 ISA_MASK 位运算一次才能得到真正的类对象或者元类对象地址,正是因为 isa 优化后存储了更多的信息,只有中间的 33 位是类对象或者元类对象地址,所以需要对 ISA_MASK 进行一次位运算。

Class

Objective-C 中类对象和元类对象都能用 Class 表示,或者通俗点说,元类对象是特殊的类对象。在底层为 objc_class。

在 objc 源码中可看到 objc_class 的结构:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;    
    class_data_bits_t bits;    
}

objc_object 中有:

Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

所以可简化为:

struct objc_class : objc_object {
    Class isa;
    Class superclass;
    cache_t cache;  // 方法缓存
    class_data_bits_t bits;  // 用于获取具体类信息 
    ...
}

其中 bits 和 FAST_DATA_MASK 进行 & 运算可得到 class_rw_t,class_rw_t 的结构为:

struct 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;

    char *demangledName;
    ...
}

其中 class_ro_t 是一个只读的结构体:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    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;
    ...
};

为研究 Class 里面的结构,我们可自己实现 Class 的底层机制,包括 class_ro_tclass_rw_t、缓存列表、协议列表等等等,篇幅过长不贴出代码。接下来的例子中将使用这份代码进行转换。

和 objc 源码不同的是,方法列表、属性列表、协议列表这些二维数组的成员用了一维数组代替。

class_rw_t

class_rw_t 中里面的方法列表、属性列表、协议列表都是二维数组,并且是可读可写的,包含了本类和分类中的内容。

方法列表的二维数组,同理属性和协议列表的二维数组:


image.png

这样可以动态增加方法或者修改方法,并且二维数组的每个方法列表都有可能是一个分类的方法列表。

class_ro_t

class_ro_t 中的 baseMethodListbaseProtocolsivarsbaseProperties 是一维数组的,只读,包含了类的初始内容。
也就是说本类的协议、属性、方法等信息在这个一维数组里面。

image.png

这份不变的 baseMethodList 和 class_rw_t 中最后一个元素是一样的,在 runtime 初始化的过程中,会根据类的初始信息来创建 class_rw_t 的成员:

static Class realizeClass(Class cls)
{
    ...
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
    ...
}

method_t

method_t 是对方法/函数的封装,也是个结构体:

struct method_t {
    SEL name; // 函数名
    const char *types; // 编码(返回值类型、参数类型)
    MethodListIMP imp; // 指向函数的指针(函数地址)
};

IMP 代表函数的具体实现:

using MethodListIMP = IMP;

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

SEL 代表方法/函数名,一般叫做选择器,底层和 char* 类似,可以通过 @selector()sel_registerName() 获得。可以通过 sel_getName()NSStringFromSelector() 转成字符串。

那么可得知,不同类中相同名字的方法,所对应的方法选择器是相同的。

我们在 Test 类中添加实例方法 test():

- (void)test {
    NSLog(@"%s", __func__); //加断点
}

然后运行:

Test* t = [[Test alloc] init];
v_objc_class* tCls = (__bridge v_objc_class*)[Test class];
class_rw_t* data = tCls->data();
[t test]; // 加断点

进入调试环境看到 data 中的 test() 信息:


image.png

打印得:

Printing description of data->methods->first.imp:
(IMP) imp = 0x00000001002ce654 (Test_3`-[Test test] at Test.m:13)

来到第二个断点 Debug->Debug Workflow->Always Show Disassembly

image.png

发现画圈部分就是这个函数的起始地址:0x00000001002ce654

types

types 包含了函数的返回值、参数编码的字符串:

返回值 参数1 参数2 ... 参数n

在上节的调试环境 data 信息截图可看到 types 是:

v16@0:8

这样的形式,其中

解释
v 代表返回值是 void
16(第一个数字) 表示所有参数所占字节数
@ 第一个参数,id 类型
0 表示第一个参数(id)从 0 开始
: 代表 SEL
8 表示 SEL 从 8 开始

以上就是 objc 通过字符串来描述一个函数的返回值及参数信息。

Type Encoding

iOS 中提供了一个叫 @encode 的指令,可以将具体的类型表示成字符串编码,如打印:

NSLog(@"%s", @encode(int));
NSLog(@"%s", @encode(NSString));
NSLog(@"%s", @encode(id));
NSLog(@"%s", @encode(void));

结果:

i
{NSString=#}
@
v

完整的编码表:

编码 释义
c A char
i An int
s A short
l A longl is treated as a 32-bit quantity on 64-bit programs
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

方法缓存

在 objc_class 的结构体中,cache_t 类型的 cache 成员是用来缓存方法的,它通过哈希表来缓存曾经调用过的方法,可以提高查找速度。

Objective-C 对象的分类以及 isa、superclass 指针一文中,得知实例方法或者类方法都是通过 isa 指针找到类对象或者元类对象的方法列表,遍历,有则调用,没有则通过 superclass 指针在父类中找方法列表,遍历,有则调用,没有则继续向上找... 若一个函数调用很多次,造成的开销是很大的,所以在函数第一次调用的时候,会缓存到 cache 中,这样就不用每次都层层寻找而是从哈希表中取出直接调用。

cache_t 的结构为:

struct cache_t {
    struct bucket_t *_buckets; // 哈希表
    mask_t _mask; // 哈希表长度 - 1
    mask_t _occupied; // 已经缓存的方法数量
}

bucket_t 是一个结构体,结构为:

struct bucket_t {
    cache_key_t _key; // SEL 作为 key
    MethodCacheIMP _imp; // 函数内存地址
}

缓存方法查找原理

这里有个很高效的算法:目标函数和 _mask 进行 & 运算可以直接得到目标索引,凭借目标索引直接在哈希表中取函数地址进行调用。

image

该索引在 test() 方法放入哈希表的时候就已经确定。
当然存在这种情况,假如哈希表数组为 0,而 @selector(test) & _mask 结果为 3,则情况为:
image.png

也就是说,其他位都成了预留位置且都是 NULL,这样的做法虽然高效,但却是以牺牲内存空间为代价的。
而且可以发现,地址 & _mask 的结果是小于等于 _mask 的。

那么假如两个方法地址 & _mask 生成的索引是一样的该怎么办?
源码(objc-cache.mm)中有处理:

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m); // key 为 @selector(test),m 为 _mask
    mask_t i = begin;
    do {
        // 找到索引,返回调用(IMP)
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
        // 若不相等,则使用 cache_next() 方法
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

cache_hash 方法:

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask); // 得到索引的 & 运算
}

cache_next() 方法(arm64 架构):

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask; // 判断结果是否为 0
}

缓存方法的时候:


image.png

若 new() 函数的目标索引已经有值,则在目标索引 -1 的位置缓存,若还有值,则继续减 1,当结果为 0 的时候,则取 _mask 值即哈希表长度 - 1。

当缓存进来一个方法后缓存方法数大于 _mask 值后会调用 expand() 方法对 _buckets 进行扩容,然后调用 reallocate() 方法清空缓存。

并不是每次缓存方法 _mask 都会变,而是一开始就开辟容量为 n 的哈希表,不够用的时候则再开辟容量为 2 倍的哈希表,以此类推,如 10,20,40,80,160 ...

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    ...
    reallocate(oldCapacity, newCapacity);
}

我们用代码验证如上过程:
首先新建 Human 类,有 run 方法,新建 Singer 类继承 Human 类, 有 sing 方法,新建 BoA 类继承自 Singer 类,有 dance 方法。

BoA* boa = [[BoA alloc] init];
v_objc_class* boaCls = (__bridge v_objc_class*)[BoA class];
        
[boa run]; //加断点
[boa sing]; //加断点
[boa dance]; //加断点
NSLog(@"=====end===="); //加断点

运行来到第一个断点:


image.png

发现哈希表容量为 4(_mask + 1),此时 _occupied 为 1,缓存的可能是 init 方法。
来到第二个断点:


image.png

_occupied 为 2,已缓存 run 方法。
来到第三个断点:


image.png

_occupied 为 3,已缓存 sing 方法。
来到第四个断点:


image.png

_occupied 为 1,并且哈希表已经扩容,容量为 8。旧的缓存内容全部清空,这个 1 是缓存的 dance 方法。

objc_msgSend

首先我们将下面的代码转成 C++ 代码:

BoA* boa = [[BoA alloc] init];
[boa dance];

得到:

BoA* boa = ((BoA *(*)(id, SEL))(void *)objc_msgSend)((id)((BoA *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BoA"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)boa, sel_registerName("dance"));

[boa dance] 简化版后得:

objc_msgSend(boa, sel_registerName("dance"));

这就是我们最熟悉的消息机制:objc_msgSend 方法。

第二个参数为:传递一个 C 语言字符串,返回一个 SEL。实际等价于 @selector(dance)

Obejective-C 中的方法调用,最终都转换成 objc_msgSend 函数的调用。
objc_msgSend 的执行流程可分为 3 个阶段:

  • 消息发送
  • 动态方法解析
  • 消息转发

在执行 objc_msgSend 方法的时候,会对给接收者(Receiver)发送消息,例子中的接收者是对象 boa,在该阶段会尝试查找方法进行调用,若能找到,就不会进入动态解析阶段,否则则进入动态解析阶段,该阶段允许动态创建新方法,若动态解析阶段未做任何操作,则进入消息转发阶段,转发给另外一个对象来调用,若未找到合适的对象调用,则会报经典的方法找不到的错误:

unrecognized selector sent to instance xxx.

objc_msgSend 源码解读

我们可在 objc-msg-arm64.s 中看到 objc_msgSend 方法的汇编源码。
看到:

ENTRY _objc_msgSend

ENTRY 是一个宏,它的定义:

.macro ENTRY /* name */
    .text
    .align 5
    .globl    $0
$0:
.endmacro

_objc_msgSend 结束调用为:

END_ENTRY _objc_msgSend

中间的部分都是它的实现,这段代码内部做了什么?
首先看到:

cmp p0 #0
b.le    LNilOrTagged

该句表示若 p0 小于等于 0 的话 跳转到 LNilOrTagged 代码块。并且这里的 p0 是 objc_msgSend 的第一个参数,为上述例子中的 boa。

b 为汇编中的跳转指令。le 是小于等于的意思。p0 为寄存器,里面存放的是消息接收者。

在 LNilOrTagged 中看到:

b.eq    LReturnZero 

LReturnZero 中看到:

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

ret 为 return 关键字。

那么该段的意思很明确:若消息接收者为 nil,则退出 objc_msgSend 函数。
若消息接收者不为空,则会来到:

LGetIsaDone:
    CacheLookup NORMAL

这句就是方法缓存查找,CacheLookup 也是一个宏:

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0 // call or return imp
    ...
    ...
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
  ...   
.endmacro

注释很明显在表明:该处在计算索引,然后根据索引去方法缓存中查找方法。其中:

CacheHit $0 

为查找到方法,直接调用或者返回 IMP。
没有查找到则:

CheckMiss $0

CheckMiss 同样为一个宏:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

由于上面传递的参数为 NORMAL,那么我们也只关注 NORMAL 的部分,即调用 __objc_msgSend_uncached 方法。该方法内部会调用 MethodTableLookup,说明未在缓存中找到方法则去其他地方查找方法,该方法内部:

bl  __class_lookupMethodAndLoadCache3

bl 为跳转调用的指令。

该方法为 C 语言函数,内部调用 lookUpImpOrForward

lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);

obj 为消息接收者,核心的代码就是 lookUpImpOrForward 方法,核心逻辑为:

...
retry:    
    runtimeLock.assertLocked();
    imp = cache_getImp(cls, sel); // 在执行该句之前可能动态添加一些方法,所以需要再检查一次缓存
    if (imp) goto done; // 若找到了,返回 IMP
    {
        // 未找到,来到这里
        Method meth = getMethodNoSuper_nolock(cls, sel); 
        if (meth) {
            // 找到方法后缓存该方法
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 返回 IMP
            imp = meth->imp;
            goto done;
        }
    }
    // 若还没有找到,则去父类的方法缓存里去查找
    {
        unsigned attempts = unreasonableClassCount();
        // for 循环为一层一层向父类查找
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            ...
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 若查找到方法,则缓存到本类当中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    ...
                    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;
            }
        }
    }
...

getMethodNoSuper_nolock 为便利 class_rw_t 中的方法列表:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL 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;
}

search_method_list() 方法中是分条件查找,一个是查找排好序的方法列表,一个是查找未排序的方法列表,findMethodInSortedMethodList() 为在已经排序的方法列表中查找,其内部是二分查找。另一个则是普通遍历查找。
最终,消息发送的流程为:

image.png

在 lookUpImpOrForward 的内部逻辑中,若如何都没有找到方法,会尝试动态解析

if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);// 动态解析
        runtimeLock.lock();
        // 标记是否解析过,置为 YES
        triedResolver = YES;
        goto retry;
}

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

_class_resolveMethod() 方法中:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { // 判断是否为元类
        
        _class_resolveInstanceMethod(cls, sel, inst);// 内部是调用 objc_msgSend 方法
    } 
    else {
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

_class_resolveInstanceMethod 可以动态的添加方法,我们模拟一下动态解析的过程,我们首先在 BoA.h 中添加函数声明:

- (void)playGolf;

不实现,在外部 [boa playGolf] 的时候会报:

'NSInvalidArgumentException', reason: '-[BoA playGolf]: unrecognized selector sent to instance 0x2811b8170'

然后重写 resolveInstanceMethod 方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(playGolf)) {
        Method method = class_getInstanceMethod(self, @selector(play));
        IMP imp = method_getImplementation(method);
        class_addMethod(self, sel, imp,  "v@:");
    }
    return YES;
}

play 方法:

- (void)play {
    NSLog(@"Play Golf!!!");
}

再次运行 [boa playGolf] 则会打印:

Play Golf!!!

该函数就是在运行时动态添加的,而非编译时期添加的。并且调用成功后 triedResolver 置为 YES,并且放到 cache 中,下次再调用则直接走消息转发的流程。

image.png

若消息发送和动态方法解析阶段都没有找到方法的实现,则会进入到最后的阶段:消息转发。

进入消息转发阶段,底层会调用 ___forwarding___ 函数,这个函数会调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,我们可以在该方法内让别的对象来调用 playGolf 函数:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(playGolf)) {
        return [[Valenti alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

Valenti 类中声明并实现了 playGolf 的方法:

-(void)playGolf {
    NSLog(@"Valenti plays golf!!!");
}

运行结果:

Valenti plays golf!!!

若未实现 forwardingTargetForSelector 方法,则会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,该方法要求返回一个方法签名,然后执行 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(playGolf)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // anInvocation 原方法接收者为 boa 对象,在这里改成了 Valenti 的对象
    [anInvocation invokeWithTarget:[[Valenti alloc] init]];
}

NSInvocation 中封装了函数的调用,参数,以及方法调用者。这些信息是由方法签名决定的。

消息转发的流程为:


image

例子中的方法都是误无参且无返回值的,那么有参有返回值的又是什么形式:
假如有 release 方法,该方法是打印「发布了多少张专辑」,需要传入一个 count 的参数决定多少张,BoA 声明未实现该方法, Valenti 中声明且实现了该方法:

- (BOOL)release:(int)count {
    NSLog(@"Release %d albums!", count);
    return count == 0 ? NO : YES;
}

则在消息转发阶段:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(release:)) {
        // 只有函数类型的不同
        return [NSMethodSignature signatureWithObjCTypes:"B@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[[Valenti alloc] init]];
}

外部调用 [boa release: 5] 运行打印:

Release 5 albums!

我们可以在 forwardInvocation 方法中得到 anInvocation 的返回值和参数信息:

int param;
[anInvocation getArgument:&param atIndex:2];
    
BOOL ret;
[anInvocation getReturnValue:&ret];
NSLog(@"%d %d",param, ret);

打印结果为 5, 1。

[anInvocation getArgument:&param atIndex:2] 为什么 index 为 2?,因为参数顺序为:receiver、selector 其次才是其他参数。

以上便是消息机制的所有内容。

super 关键字

理解 super 关键字,还需要借助上面 BoA 的继承链:BoA 继承 Singer 继承 Human。
然后在 BoA 的 init() 方法中:

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"[super class] %@", [super class]);
        NSLog(@"[super superclass] %@", [super superclass]);
    }
    return self;
}

结果为:

[super class] BoA
[super superclass] Singer

是不是和猜想有点出入?明明是 super 指针,打印的却是本类以及本类的父类。

super 关键字底层执行的是 objc_msgSendSuper 方法。该方法传入两个参数,一个是 objc_super 的结构体,源码中的结构体形式为:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver; // 消息接收者,BoA 对象

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

第二个参数为 SEL。
转成 C++ 代码后,我们发现传入的 objc_super 类型的参数第一个成员初始化结果为 self,第二个为 class_getSuperclass(objc_getClass("BoA")) 也就是 Singer 类。

从 objc_super 的结构可以知道,虽然调用的是 super,但是实际的消息接收者仍然是 BoA 对象。那么传入的父类作用是什么?是告诉从哪里开始找方法,也就是说是从父类中找class/superclass 方法,但接收者仍然是本类的对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容