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
将该地址复制到系统计算器中:
最后一位为 1 说明 nonpointer
位为 1,说明该 isa 指针是 arm64 优化过后的指针,存储了更多信息。
倒数第二位为 0,说明 has_assoc
位为 0,说明该类未设置关联对象,例子中我没有给 Test 类设置关联对象。
倒数第三位为 0,说明 has_cxx_dtor
位为 0,说明该类没有析构函数。(析构函数类似 dealloc 函数)
接下来的 33 位,如图:
表示字段 shiftcls
,存放着类对象地址或者元类对象的值。
接下来的 6 位 01 1010 表示字段 magic
,表示对象已经初始化成功,执行完 alloc
和 init
后它的值为 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_t
、class_rw_t
、缓存列表、协议列表等等等,篇幅过长不贴出代码。接下来的例子中将使用这份代码进行转换。
和 objc 源码不同的是,方法列表、属性列表、协议列表这些二维数组的成员用了一维数组代替。
class_rw_t
class_rw_t 中里面的方法列表、属性列表、协议列表都是二维数组,并且是可读可写的,包含了本类和分类中的内容。
方法列表的二维数组,同理属性和协议列表的二维数组:
这样可以动态增加方法或者修改方法,并且二维数组的每个方法列表都有可能是一个分类的方法列表。
class_ro_t
class_ro_t 中的 baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组的,只读,包含了类的初始内容。
也就是说本类的协议、属性、方法等信息在这个一维数组里面。
这份不变的 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() 信息:
打印得:
Printing description of data->methods->first.imp:
(IMP) imp = 0x00000001002ce654 (Test_3`-[Test test] at Test.m:13)
来到第二个断点 Debug->Debug Workflow->Always Show Disassembly:
发现画圈部分就是这个函数的起始地址: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
进行 & 运算可以直接得到目标索引,凭借目标索引直接在哈希表中取函数地址进行调用。
该索引在 test() 方法放入哈希表的时候就已经确定。
当然存在这种情况,假如哈希表数组为 0,而 @selector(test) & _mask 结果为 3,则情况为:
也就是说,其他位都成了预留位置且都是 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
}
缓存方法的时候:
若 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===="); //加断点
运行来到第一个断点:
发现哈希表容量为 4(_mask + 1),此时 _occupied 为 1,缓存的可能是 init 方法。
来到第二个断点:
_occupied 为 2,已缓存 run 方法。
来到第三个断点:
_occupied 为 3,已缓存 sing 方法。
来到第四个断点:
_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()
为在已经排序的方法列表中查找,其内部是二分查找。另一个则是普通遍历查找。
最终,消息发送的流程为:
在 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 中,下次再调用则直接走消息转发的流程。
若消息发送和动态方法解析阶段都没有找到方法的实现,则会进入到最后的阶段:消息转发。
进入消息转发阶段,底层会调用 ___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 中封装了函数的调用,参数,以及方法调用者。这些信息是由方法签名决定的。
消息转发的流程为:
例子中的方法都是误无参且无返回值的,那么有参有返回值的又是什么形式:
假如有 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:¶m atIndex:2];
BOOL ret;
[anInvocation getReturnValue:&ret];
NSLog(@"%d %d",param, ret);
打印结果为 5, 1。
[anInvocation getArgument:¶m 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 方法,但接收者仍然是本类的对象。