本文须要对消息转发机制有了解,建议阅读 Objective-C 消息发送与转发机制原理html
恰巧在 8 月学习 Method Swizzling ,阅读了 Aspects 和 JSPatch 作方法替换的处理,注意到了咱们此次介绍的主角 --_objc_msgForward_stret
.linux
JSPatch 目前的 Star 数已经破万,知名度可见一斑。面试时,也会常常被说起。Aspects也是一个在 AOP 方面很是著名的库。git
在消息转发时,咱们根据方法返回值的类型,来决定 IMP 使用 _objc_msgForward
或者 _objc_msgForward_stret
.github
根据苹果的文档描述,使用 _objc_msgForward_stret
的确定是一个结构体:面试
Sends a message with a data-structure return value to an instance of a class.bash
然而,不一样 CPU 架构下,判断 _objc_msgForward_stret
的规则也有差别。下面就来看看两个著名开源库的作法。架构
首先咱们来看 JSPatch , 在 JPEngine.m 文件里的 overrideMethod 方法,是如何去判断是否使用 _objc_msgForward_stret
:app
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
if (typeDescription[0] == '{') {
//In some cases that returns struct, we should use the '_stret' API:
//http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
//NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
}
#endif
复制代码
上面的代码,第一判断在非 arm64
下,第二判断是否为 union
或者 struct
(详见Type Encodings )。ide
最后,经过判断方法签名的 debugDescription 是否是包含特定字符串-is special struct return? YES
,进而决定是否使用 _objc_msgForward_stret
.能够说是一个很是 trick 的作法了.函数
关于 Special Struct
,JSPatch 做者本身也在 JSPatch 实现原理详解 中提到了缘由.文章说明在非 arm64 下都会存在 Special Struct
这样的问题。而具体判断的规则,苹果并无提供给咱们,因此使用到了这样的方法进行判断也是无奈之举。
好在通过大量项目运行以来,证实这个方法仍是靠谱的。
Aspects 一样也是一个很是有名的开源项目,查看 Aspects 中与 _objc_msgForward_stret
相关的 commit,做者对 Special Struct
的判断颇下功夫,先后修改了不少次。
最后的版本是这样的:
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
// As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id.
// https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783
// http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4)
Method method = class_getInstanceMethod(self.class, selector);
const char *encoding = method_getTypeEncoding(method);
BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
if (methodReturnsStructValue) {
@try {
NSUInteger valueSize = 0;
NSGetSizeAndAlignment(encoding, &valueSize, NULL);
if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
methodReturnsStructValue = NO;
}
} @catch (__unused NSException *e) {}
}
if (methodReturnsStructValue) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
#endif
return msgForwardIMP;
}
复制代码
与 JSPatch 相同,只对非 arm64 判断使用 _objc_msgForward_stret
.
最大的不一样,在于 Aspects
是判断方法返回值的内存大小,来决定是否使用_objc_msgForward_stret
。
根据代码上的注释,做者参考了 苹果的 OS X ABI Function Call Guide
,以及 ARM 遵循的标准 Procedure Call Standard for the ARM® Architecture
.
抱着搞明白的心态,我也去看了上述文档里面关于 Return Values
的说明:
通常来讲,函数的返回值,和函数存储在同一个寄存器当中。可是有些 Special Struct 太大了,超出了寄存器能存储的范围,就只能放置一个指针在存储器上,指向内存中返回值所在的地址。
专门查阅了 System V Application Binary Interface
中的 Intel386 Architecture Processor Supplement ,这里对于返回值为结构体类型的存储,有一个比较清楚的界定:
Structures. The called function returns structures according to their aligned size.
- Structures 1 or 2 bytes in size are placed in EAX.
- Structures 4 or 8 bytes in size are placed in: EAX and EDX.
- Structures of other sizes are placed at the address supplied by the caller. For example, the C++ language occasionally forces the compiler to return a value in memory when it would normally be returned in registers. See Passing Arguments for more information.
IA-32 说明 1,2,4,8 字节大小的结构体,被存储在寄存器中。其它大小的结构体,被放置的在寄存器中的,则是结构体的指针。
Aspects 中的判断,应该就是基于这个的。我心想,靠谱了。
仿佛学习到姿式的我,兴冲冲的去对 JSPatch 提了一个 PR .
事实证实,我仍是太年轻 ,测试结果 是这样的:
Xcode7.3 iPhone4s(8.1) 成功
Xcode7.3 iPhone6s(9.3) 失败
发现是在 64 位底下,一些结构体判断失败了。由于在 IA-32 下,寄存器是 32 位的。而新的机型,好比这里测试的 6s 模拟器,则属于 x86-64 ,寄存器是 64 位的。
因此须要增长对 16 字节的判断。
因而,本地对 6s 进行测试经过后,又增长了一次提交:
if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8 || valueSize == 16) {
methodReturnsStructValue = NO;
}
复制代码
然而....测试结果 仍是不行:
Xcode7.3 iPhone4s(8.1) 失败
Xcode7.3 iPhone6s(9.3) 成功
上面说了,16 字节的判断是在 64 位机型状况下作的,因此在的 32 位的机型上, 也对 16 字节进行处理,继续使用 _objc_msgForward
是会 Crash 的。
再增长一次提交:
#if defined(__LP64__) && __LP64__
if (valueSize == 16) {
methodReturnsStructValue = NO;
}
#endif
复制代码
终于经过了测试,完美 : )
这里说明一下,为何寄存器所能存储的结构体,是自己处理器位数的 2 倍的问题:
好比,在 x86-64
中,RAX
一般用于存储函数调用的返回结果,但同时也在乘法和除法指令中。在 imul 指令中,2 个 64 位的乘法最多会产生 128 位的结果,就须要 RAX
与 RDX
共同存储乘法结果. IA-32
也是一样的道理.
在苹果描述 32bit-PowerPC
函数规则的文档里,关于 Returning Results
的也有 2 个寄存器共同存储 1 个返回值状况的描述:
Values of type long long are returned in the high word of GPR3 and the low word of GPR4.
按照上述结果,Aspects 是有问题的,果真通过测试,在 64 位模拟器上返回结构体 Crash 了,这里是我提供的 复现过程
。
说完了模拟器中的处理,再来看看真机的规则。
查看苹果的 iOS ABI Function Call Guide , ARMv6 ,ARMv7 等遵循的规则一致。找到 Procedure Call Standard for the ARM Architecture (AAPCS)
。有一份在线的 PDF,里面对 Result Return
有比较完整的说明:
The manner in which a result is returned from a function is determined by the type of that result. For the base standard:
- A Half-precision Floating Point Type is returned in the least significant 16 bits of r0.
- A Fundamental Data Type that is smaller than 4 bytes is zero- or sign-extended to a word and returned in r0.
- A word-sized Fundamental Data Type (e.g., int, float) is returned in r0.
- A double-word sized Fundamental Data Type (e.g., long long, double and 64-bit containerized vectors) is returned in r0 and r1.
- A 128-bit containerized vector is returned in r0-r3.
- A Composite Type not larger than 4 bytes is returned in r0. The format is as if the result had been stored in memory at a word-aligned address and then loaded into r0 with an LDR instruction. Any bits in r0 that lie outside the bounds of the result have unspecified values.
- A Composite Type larger than 4 bytes, or whose size cannot be determined statically by both caller and callee, is stored in memory at an address passed as an extra argument when the function was called (§5.5, rule A.4). The memory to be used for the result may be modified at any point during the function call.
上面总结咱们要的关键信息,大于 4 字节的复合类型返回值,会被存储在内存中的地址上,做为一个额外的参数传递。
前面一直判断的,都属于非 ARM64 的,我也很好奇,为何 ARM64 就没有问题?
查看关于 ARM64
调用规则文档里的 Result Return
说明:
The manner in which a result is returned from a function is determined by the type of that result:
void func(T arg)
复制代码
would require that arg be passed as a value in a register (or set of registers) according to the rules in §5.4 Parameter Passing, then the result is returned in the same registers as would be used for such an argument.
上面说到 x8 寄存器。查看关于返回值的寄存器功能的说明,以下:
寄存器 | 功能 |
---|---|
r0…r7 | Parameter/result registers |
r8 | Indirect result location register |
第一条说的,就是返回值会和参数存在同样的寄存器,也就是 x0-x7 中。
第二条说的,除了第一条的状况以外,调用者就会为这个函数预留一各足够大小和对齐的内存块,存在 x8 寄存器中。
因为第二条规则,咱们能够知道,只要返回的不是 void
. arm64 上存储的返回值都是经过指向内存的指针来作的。我也拿了一个很是大的结构体进行验证:
typedef struct {
CGRect rect;
CGSize size;
CGPoint orign;
}TestStruct;
typedef struct {
TestStruct struct1;
TestStruct struct2;
TestStruct struct3;
TestStruct struct4;
TestStruct struct5;
TestStruct struct6;
TestStruct struct7;
TestStruct struct8;
TestStruct struct9;
TestStruct struct10;
}TestBigStruct;
复制代码
测试 TestBigStruct
,打印它的方法签名的 debugDescription
,包含的内容是 is special struct return? NO
. valueSize 倒是 640,寄存器确定是存放不下,使用的指针指向内存。
断定返回值
的Special Struct
的条件:
机器 | 条件 |
---|---|
i386 | 大小非 1,2,4,8 字节 |
x86-64 | 大小非 1,2,4,8,16 字节 |
arm-32 | 大于4字节 |
arm-64 | 不存在的 :) |
固然,还有判断方法签名的 debugDescription
是否含有 is special struct return? YES
的方法。
由于包含有 PowerPC-32/PowerPC-64/IA-32/X86-64/ARMv6/ARMv7/ARM64 这么多个体系的英文说明,不少仍是关于寄存器和汇编的,能够说看的我很是痛苦了。同时收获也是很是大的。
提及来对 JSPatch
的原理解读文章,应该没有谁写的比做者本人还好了,里面介绍了项目实际遇到的各类难点。读完下来,其中关于 super关键字
的解读,给了我另外一个问题的灵感。
建议你们学习新知识时,不妨结合知名的项目进行借鉴,能学到许多知识 : )
此次的探究过程,彻底属于本菜鸟本身瞎摸索的,若是有不对的地方,但愿你们多拍砖,让我进一步学习。
Introduction to OS X ABI Function Call Guide
Intel386 Architecture Processor Supplement
Procedure Call Standard for the ARM 64-bit Architecture
Procedure Call Standard for the ARM® Architecture