JOBridge之一任意方法的Swizzle
之前的博客都偏理论,这次来玩个有趣的。
JSPatch作为热修复方案发布以来,得到很多同行的认可(github已经1W+ star了,已经步入超级项目的行列了),也是我个人比较推崇的开源项目。得益于JSPatch,很多bug在用户无感知的情况下快速被修复,对于快速迭代App还是有重大意义,而作为JSPatch的特性,有些中小型App甚至于用其来做一些功能更新,JSPatch被广泛应(lan)用(yong)。好景不长,苹果盯上JSPatch,拒绝包含热修复的App上架,然而上有政策下有对策,而热修复方案随之转入地下工作,很多人通过混淆JSPatch方法,修改其源码,甚至于再另出解决方案,来躲过苹果审查。自年中开始,我们App上架遇到了一定的阻碍,于是我开始思索是否也要加入跟苹果斗智斗勇的勇士大军,所以9月开始着手实现JOBridge,费时一月,基本完成了方案,但很多细节都还需要进一步完善。
JOBridge方案沿用了JSPatch的语法,但在实现上很不一样。整个方案基本没有JS实现代码(PS.主要是本人JS能力有限),很多关键的实现都是另辟蹊径,当然一些实现思路还是借鉴JSPatch实现。我将把整个方案的关键技术点分几次写出来,这次写其中一个关键技术点,该技术点不光可以在这里使用,也可以被应用在其他地方,怎么玩读者可以自己发掘。
如果看过JSPatch应该知道,当JS调用OC的方法时,比如alloc(),会被转成.c('alloc')(),.c方法是Object对象提供的全局方法,其负责将‘alloc’调用 处理后,调用OC提供的方法,该方法将根据对应的信息(obj,selector,参数等)创建一个NSInvocation,调用对应的OC方法alloc。如果JS替换了该alloc方法,则将alloc的实现入口换成_objc_msgForward,同时Swizzle forwardInvocation到JPForwardInvocation,该方法负责构建参数和调用js对应的实现。这样就间接实现了对任意方法的Swizzle,这么做可以避免直接处理参数存储,这确实是一种简单有效的处理手段,但确实挺曲折,我这里选择直接实现该过程。
在说明我的方案之前,我们需要先了解如下OC知识。OC中的Method=SEL+type+IMP(另外还有一个SortBySELAddress结构体,但似乎没有使用,或者是通过其他方式使用的),objc_msgSend就是通过Class的Method列表通过查找对应的SEL来获取IMP,就是一个key-value的过程,这是OC动态化的基础,具体的过程我之前的文章有详细解释和反汇编的代码。IMP是怎么定义的? typedef id (*IMP)(id, SEL, ...)
,就是一个C的函数指针。当找到IMP之后,objc_msgSend直接通过br x17
跳转到了函数执行(以下汇编代码是arm64下的实现,在CacheHit之后 ,当宏参数0就是取第一个宏参数)时,直接调用,否则就用寄存器返回),期间没有任何参数处理的逻辑,也就是说OC方法和C方法在具体的调用和传参上并没有什么不同,当然这是个基本常识,这里强调一下。
.macro CacheHit
.if $0 == NORMAL
MESSENGER_END_FAST
br x17 // call imp
.elseif $0 == GETIMP
mov x0, x17 // return imp
ret
.elseif $0 == LOOKUP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
再看SEL,我们会发现SEL和IMP没有必然联系,SEL只是IMP的一个identify,就如同NSDictionary的key和value的关系,但我们也别忘记了method还有一个type字段。有了这些基本认知之后就可以衍生出一些玩法了,比如我们可以通过给一个C方法加上一个SEL,就可以立即变成一个OC方法(如果和一个Class关联,就是一个标准的OC方法,当然也可以不和Class关联,直接将SEL和IMP的映射存在一个全局的NSDictionary里面),这将是后面博客涉及的内容,这次讲全局Swizzle。
Method=SEL+type+IMP,而SEL->name和type也没有必然的关系。比如:name=viewDidLoad,其type可以是任意的,其签名是影响参数获取的,如果不需要动态调用的(比如performSelector),其实也就不需要签名了。但是objc_msgSend调用是根据SEL指针来确定调用的,而SEL是一般是编译器生成的常量,编译器会根据我们所写的函数名字和参数类型生成type,所以这种情况下name和type是对应的,也就说只需要比较SEL就能确定是否是对应的IMP。不过动态添加的Method确是想怎么签名就怎么签名,还有block也是如此。
JOGlobalSwizzle
之前看过我博客的同学会发现有很多汇编的内容,但可能会认为汇编这东西没有太大的实际用处,这次我们就来点有用的东西,看看汇编可以怎么玩转这些东西。
全局Swizzle的做法就是直接构建一个无返回值无参数的C函数JOGlobalSwizzle,替代JPForwardInvocation,这样所有的JS定义的方法的默认实现都是JOGlobalSwizzle,该函数作为统一入口调用其他的一系列函数来实现JS方法的调用。
废话不多说,先上代码
OS_ALWAYS_INLINE void JOGlobalSwizzle(void) {
asm volatile("stp x29, x30, [sp, #-0x10]!");
asm volatile("mov x29, sp");
asm volatile("sub sp, sp, #0xb0");
asm volatile("str d7, [sp, #0x88]\n\
str d6, [sp, #0x80]\n\
str d5, [sp, #0x78]\n\
str d4, [sp, #0x70]\n\
str d3, [sp, #0x68]\n\
str d2, [sp, #0x60]\n\
str d1, [sp, #0x58]\n\
str d0, [sp, #0x50]\n\
str x8, [sp, #0x40]\n\
str x7, [sp, #0x38]\n\
str x6, [sp, #0x30]\n\
str x5, [sp, #0x28]\n\
str x4, [sp, #0x20]\n\
str x3, [sp, #0x18]\n\
str x2, [sp, #0x10]\n\
str x1, [sp, #0x8]\n\
str x0, [sp]\n\
mov x2, sp\n\
add x3, sp, #0x50\n\
add x4, sp, #0xb0\n\
bl _JOGlobalParamsResolver\n\
str x0, [sp, #0x98]\n\
");
@autoreleasepool {
asm volatile("str x0, [sp, #0x90]");
asm volatile("ldr x0, [sp, #0x98]");
asm volatile("bl _JOCallJsFunction");
asm volatile("str x0, [sp, #0x98]");
asm volatile("str d0, [sp, #0xa0]");
asm volatile("ldr x0, [sp, #0x90]");
}
asm volatile("ldr x0, [sp, 0x98]");
asm volatile("ldr d0, [sp, 0xa0]");
asm volatile("mov sp, x29");
asm volatile("ldp x29, x30, [sp], #0x10");
}
可以看到该函数全部是由内联汇编实现,实现还是比较简单,了解汇编的人都比较容易看懂。看不懂的也不要紧,我会一一作说明。其中asm是内联汇编命令,volatile则告诉编译器不要优化括号内汇编代码(但不同内联汇编代码之间却不受此影响,所以编译器也可能会优化我们的汇编代码,也就是说volatile在多行内联汇编的时候才会比较有效,单行可能没啥效果,必要时需要注意一下)。
接下来,我将具体说明,第一句,将有特殊意义的x29,x30寄存器存在栈上,以便在函数末尾恢复。然后x29存储上一次sp的数据,再将sp下移"0xb0"个Byte。"0xb0"由接下来函数需要多少栈上临时存储空间决定,多一点没事,少了可能就不行了。
然后将x0-x8,d0-d7(浮点寄存器)全部存储在栈上,因为后面会做大量的操作,会破坏寄存器,所以需要提前保存参数。这里需要说明的是,这参考了OC转发的__forwarding__
实现,其存储不是d0-d7,而是q0-q7,其中q寄存器是128bit,d寄存器是64bit,我这里为了参数解析简单用的是d寄存器,需要用q寄存器传参的情况极少,我就忽略了。
参数存储到栈上之后,我作了如下操作
mov x2, sp
add x3, sp, #0x50
add x4, sp, #0xb0
将sp也就是x0起始地址,sp+0x50也就是d0起始地址,sp+0xb0也就是上一个栈参数的起始地址,分别作为第三到第五个参数传递给_JOGlobalParamsResolver函数,主要是为了让后面的参数解析过程中计算offset简单一点。
接下来调用_JOGlobalParamsResolver,这函数的作用就是将刚刚存储在栈上的所有参数根据SEL的type签名取出来,并封装成OC对象,存储在NSArray里面返回。
str x0, [sp, #0x98]
这句将_JOGlobalParamsResolver的返回值x0(参数list)存储在栈上的一个临时位置,之后再细讲。
我这里创建了一个@autoreleasepool(这个不是必须的),其会隐式调用objc_autoreleasePoolPush(),这函数返回值会破坏x0,所以上一步会先将x0暂存到栈上,再将新的x0就是autoreleasepool哨兵地址,也暂存在栈上,接下来加载之前的参数list到x0,调用_JOCallJsFunction。
note:这里说明一下,JOGlobalSwizzle被声明为无返回值,无参数的,如果没有一些特殊操作,比如不使用OC一些特性,编译器不会为其添加任何多余的汇编代码(除了"ret"),这就方便我们进行内联汇编,而不用担心寄存器被破坏,因为如果编译器插入一些代码就很可能破坏寄存器,这类函数被称为C桩(stub)函数。
_JOCallJsFunction的作用就是去调用对应的JS实现,并获取返回值,该函数是可以返回任意数据的,所以返回值可能在x0,也可能在d0,所以两个都需要暂存起来,之后细讲。不过这里处理的不是很全面,返回值如果占用两个寄存器的情况没有处理。
接下来调用objc_autoreleasePoolPop(),该函数有一个参数,所以需要将之前的autoreleasepool哨兵地址从栈上加载到x0再调用。
_JOGlobalParamsResolver已经将所有的参数解析并另外存储,所有这里不需要将栈上的数据恢复到寄存器,直接pop栈就可以。如果参数可以直接被下一个调用的函数利用,不需要解析参数,这里很可能需要恢复参数到寄存器。
最后加载之前暂存的_JOCallJsFunction返回值到x0,d0,然后还原sp,x29,x30寄存器,函数执行完。
JOGlobalParamsResolver
id JOGlobalParamsResolver(id obj, SEL sel, void **intPointer, void **floatPointer, void **stackPointer) {
Class class = object_getClass(obj);
Method method = class_getInstanceMethod(class, sel);
int num = method_getNumberOfArguments(method);
int initParam = 2;
BOOL isBlock = NO;
char *blockType = NULL;
if ([obj isKindOfClass:[NSClassFromString(@"NSBlock") class]]) {
isBlock = YES;
char **pType = &blockType;
asm volatile("ldr x0, %0" : "=m"(obj));
asm volatile("ldr x8, [x0, 0x18]");
asm volatile("add x1, x8, 0x10");
asm volatile("add x2, x8, 0x20");
asm volatile("ldr w3, [x0, 0x8]");
asm volatile("tst w3, #0x2000000");
asm volatile("csel x2, x1, x2, eq");
asm volatile("ldr x0, %0": "=m"(pType));
asm volatile("ldr x2, [x2]");
asm volatile("str x2, [x0]" );
num = (int)strlen(blockType) - 1;
initParam = 1;
}
//寄存器值,浮点寄存器值,原始栈的参数,都在栈上
int r_offset = isBlock ? 8 : 16;
int f_offset = 0;
int s_offset = 0;
BOOL onStack = NO;
NSMutableArray *array = [NSMutableArray array];
[array addObject:obj];
if (!isBlock) [array addObject:MakeSelObj(sel)];
for (int i = initParam; i < num; ++i) {
char name[512] = {};
isBlock ? (name[0] = blockType[i+1]) : method_getArgumentType(method, i, name, 512);
char *type = name;
while (*type == 'r' || // const
*type == 'n' || // in
*type == 'N' || // inout
*type == 'o' || // out
*type == 'O' || // bycopy
*type == 'R' || // byref
*type == 'V') { // oneway
type++; // cut off useless prefix
}
void **p = intPointer;//默认是寄存器
int *offset = &r_offset;
if (type[0] == 'd' || type[0] == 'f') {//取浮点寄存器
p = floatPointer;
offset = &f_offset;
}
if (onStack) {//原始栈上的参数
p = stackPointer;
offset = &s_offset;
}
if (type[0] == '{') {
NSMutableArray *typeArray = [NSMutableArray array];
JOAnalyzeBrace(&type, type, typeArray);
NSArray *memArray = JOSpaceUsage(typeArray);
int offset_l = 0;
int totalMem = [memArray.lastObject[@"offset"] intValue] + [memArray.lastObject[@"len"] intValue];
if (JOIsOnFloatRegister(typeArray, f_offset)) {//如果在浮点寄存器上则从浮点寄存器值所在的栈开始偏移读取
p = floatPointer;
offset = &f_offset;
} else if (totalMem > 16 && r_offset < 64) {//如故结构体很大,那就是说结构体被放在了原始栈,则先取出寄存器存的指针,再取具体的结构体值
JOPointerObj *o = JOGetParam(p, '*', offset);
*offset += 8;
p = (void **)o.ptr;
offset = &offset_l;
}
//这里的结构体直接被存储为数组,需要js在使用的时候到对应的位置去取相应的数据
NSMutableArray *structArray = [NSMutableArray array];
for (int i = 0; i < typeArray.count; ++i) {
NSDictionary *type = typeArray[i];
int len = [memArray[i][@"len"] intValue];
*offset = JOCalcAlign(*offset,len);
const char *structType = [type[@"type"] UTF8String];
id o = JOGetParam(p, structType[0], offset);
if (o) [structArray addObject:o];
(*offset) += len;
}
[array addObject:structArray];
} else {
int len = JOLenWithToken(type[0]);
*offset = JOCalcAlign(*offset, len);
id o = JOGetParam(p , type[0], offset);
if (o) [array addObject:o];
if (onStack) {//在原始栈上,需要考虑内存对齐,否则直接跳过一个寄存器的大小8Byte
*offset += len;
} else {
*offset += 8;
}
}
if (r_offset >= 64) {//寄存器用完,则剩下的参数在栈上
onStack = YES;
}
}
return array;
}
}
该函数负责将存储在栈上的参数,根据OC Method的selector签名,来解析各种参数,并转成OC对象,存到NSArray中,这其中最头疼的是各种参数存放在什么位置,是怎么存储的,虽然有ARM64 PCSAA文档,但具体如何我还是花了大量的时间来做实验,在寄存器和栈上不断观察,基本了解了各种参数的传递规则(文档的描述和iOS实际情况还是有差异的,是否还有其他的规则,可能还需要各路英雄豪杰来补充)。
在JOGlobalSwizzle中调用JOGlobalParamsResolver前我没有破坏x0,x1寄存器,所以前两个参数还是self, _cmd,通过这两个参数就可以很容易获取Method的签名信息。(这里先忽略对Block签名的的处理,这是下一篇文章的内容,其会复用本函数做参数解析)
intPointer表示寄存器参数首地址,floatPointer表示浮点寄存器参数首地址,stackPointer表示栈参数首地址。r_offset,f_offset,s_offset表示当前正要处理的相对于这三个首地址的地址偏移量。
p表示是从哪个地址处理,可能是intPointer,floatPointer,stackPointer之一,如果处理的是栈上的结构体时其将被赋值为一个临时的地址。
offset一般情况下是r_offset,f_offset,s_offset之一,如果处理的是栈上的结构体时其将被赋值为一个临时的默认值为0的offset_l。
接下来根据参数个数循环解析。从第三个参数开始,获取单个参数签名的type,默认情况下其应该是在寄存器中的,而且其默认r_offset=16(8是block的情况)。根据type或onStack标记,看是否需要切换栈指针。type='d'或者'f'表明参数在浮点寄存器上。
如果type='{'则表明是个结构体,这个处理起来比较麻烦。其会先调用JOAnalyzeBrace方法。
OS_ALWAYS_INLINE void JOTryAddTokenToArray(char *ch, const char *begin, NSMutableArray *array)
{
char subChar[2] = "\0\0";
strncpy(subChar, ch, 1);
NSString *string = [NSString stringWithUTF8String:subChar];
[array addObject:@{@"offset": @(ch - begin), @"type" : string}];
}
OS_ALWAYS_INLINE void JOAnalyzeBrace(char **point, /*当前指针的position*/
const char *begin,
NSMutableArray *array) {
*point = strchr(*point, '=');
if ( (*point)++ != NULL) {
while (*point != '\0') {
switch (**point) {
case '{': JOAnalyzeBrace(point, begin, array); break;
case '}': ++(*point); return;
case '*':
case '^': JOTryAddTokenToArray(*point, begin, array); ++(*point); ++(*point); break;
default: JOTryAddTokenToArray(*point, begin, array); ++(*point); break;
}
}
}
}
该方法会将type字符串中有效参数类型提取出来存储在数组中,offset其实用不上。接下来调用JOSpaceUsage。
//返回每种数据类型的大小
OS_ALWAYS_INLINE int JOLenWithToken(char token) {
int len = 0;
switch (token) {
case 'B':
case 'c':
case 'C': len = 1; break;
case 's':
case 'S': len = 2; break;
case 'i':
case 'I':
case 'f': len = 4; break;
case 'l':
case 'L':
case 'q':
case 'Q':
case 'd':
case '^':
case '@':
case '#':
case '*':
case ':': len = 8; break;
}
return len;
}
//内存对其计算offset=0x...45b1,align=4,return 0x...45b4
OS_ALWAYS_INLINE unsigned int JOCalcAlign(unsigned int offset,unsigned align) {
return ((offset + align - 1) & (~(align - 1)));
}
//计算内存的布局,主要考虑内存对齐(嵌套struct和union暂时没有考虑)
OS_ALWAYS_INLINE NSArray *JOSpaceUsage(NSArray *array) {
NSMutableArray *mutable = [NSMutableArray array];
int offset = 0;
for (int i = 0; i < array.count; ++i) {
id obj = array[i];
int len = JOLenWithToken([obj[@"type"] UTF8String][0]);
offset = JOCalcAlign(offset,len);
[mutable addObject:@{@"offset":@(offset), @"len":@(len)}];
offset += len;
}
return [mutable copy];
}
该函数根据type类型和内存对其的情况计算出结构体的内存布局,每个数据在偏移地址和长度。
接下来调用JOIsOnFloatRegister,判断结构体是否会存储在浮点寄存器上。我发现如果结构体内部只存储了一种类型的数据就会通过浮点寄存器传参(要么全部double,要么全部float),比如CGRect。
接下来判断结构体size是否大于16,并且寄存器至少还有一个空位。那结构体会被存到栈上,并将其地址放入寄存器上。这时候就需要先调用JOGetParam,获取对应的结构体,并将offset设置为临时的offset_l。
如果结构体不大于16个Byte,并且寄存器有足够的空间,那就是结构体所有数据被存在了寄存器上。
根据p,type,offset调用JOGetParam获取参数数据,并封装成OC对象或者打包成JS对象,其中数字类型直接封装成NSNumber即可,指针,SEL需要封装成OC对象,OC对象却需要进一步打包成JS对象。这是因为如果不封装成JS对象,当JS将该对象再传过来时,可能会改变。比如NSMutableArray的addObject方法就会受到影响。
OS_ALWAYS_INLINE id JOGetParam(void **pointer, char type, int *offset) {
char *pos = (char *)pointer;
pos += *offset;
pointer = (void **)pos;
switch (type) {
case '@':
case '#': {
__autoreleasing id obj = (__bridge id)(*pointer);
return MakeJSObj(MakeObj(obj));
}
case 'B': return @((BOOL)*pointer);
case 'c': return @((char)*(pointer));
case 'C': return @((unsigned char)*(pointer));
case 's': return @((short)*(pointer));
case 'S': return @((unsigned char)*(pointer));
case 'i': return @((int)*(pointer));
case 'I': return @((unsigned int)*(pointer));
case 'l': return @((long)*(pointer));
case 'L': return @((unsigned long)*(pointer));
case 'q': return @((long long)*(pointer));
case 'Q': return @((unsigned long long)*(pointer));
case 'f': return @((float)*((float *)(pointer)));
case 'd': return @((double)*((double *)(pointer)));
case '^':
case '*': return MakePointerObj(*(pointer));
case ':': return MakeSelObj(*(pointer));
default : return @((unsigned long long)*(pointer));
}
return nil;
}
最后如果不是结构体就简单了,说明整个寄存器就一个参数,直接根据type获取参数就行。
JOCallJsFunction
JOCallJsFunction负责调用JS对应的实现,同时将JS返回值构造成OC或者C数据。
/* 解析参数完成后,使用内联汇编调用本函数,本函数将会调用js的对应实现,同时将js的返回值转换成对应的OC数据类型
*/
void JOCallJsFunction(__autoreleasing NSArray *params) {
if (params.count < 2) return;
__autoreleasing JSValue *preSelf = [JOBridge jsContext][@"self"];
__autoreleasing JSValue *precmd = [JOBridge jsContext][@"_cmd"];
[JOBridge jsContext][@"self"] = MakeJSObj(MakeObj(params.firstObject));
[JOBridge jsContext][@"_cmd"] = (JOSelObj *)params[1];
NSLog(@"CallJsFunction : %@",NSStringFromSelector(((JOSelObj *)params[1]).sel));
__autoreleasing JSValue *jsFunc = JOSearchJsMethod([params.firstObject class], NSStringFromSelector(((JOSelObj *)params[1]).sel));
__autoreleasing JSValue *ret = [jsFunc callWithArguments:[params subarrayWithRange:(NSRange){2, params.count - 2}]];
[JOBridge jsContext][@"self"] = preSelf;
[JOBridge jsContext][@"_cmd"] = precmd;
Class class = object_getClass(params.firstObject);
Method method = class_getInstanceMethod(class, ((JOSelObj *)params[1]).sel);
char *type = method_copyReturnType(method);
JOConstructReturnValue(ret, type[0]);//此句最好在方法的最末
}
本函数的关键点是声明的返回值void,但是实际上会通过汇编将返回值写入x0或者d0。为了不让x0,d0被编译器插入的release代码破坏,还需要将所有的局部OC对象声明为autoreleasing,其被会JOGlobalSwizzle中autoreleasepool管理。
JOSearchJsMethod就是去一个全局的字典搜索对应obj,对应selector的js实现。
最后是JOConstructReturnValue的实现代码如下,其利用内联汇编将返回值写入x0或者d0。这样调用方就可以像普通函数一样获取返回值了(原始调用者根据原始函数声明知道如何去获取该值)。因为返回值要占用x0或者d0寄存器,所以所有的OC变量都需要使用autoreleasing。
void JOConstructReturnValue(__autoreleasing JSValue *ret, char type) {
switch (type) {
case 'B': { BOOL r = [ret toBool];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'c': { char r = [[ret toObject] charValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'C': { Byte r = [[ret toObject] unsignedCharValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 's': { short r = [[ret toObject] shortValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'S':{ unsigned short r = [[ret toObject] unsignedShortValue];
asm volatile("ldr x0, %0": "=m"(r)); break; };
case 'i': { int r = [[ret toObject] intValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'I': { unsigned int r = [[ret toObject] unsignedIntValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'l': { long r = [[ret toObject] longValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'L': { unsigned long r = [[ret toObject] unsignedLongValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'q': { long r = [[ret toObject] longLongValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'Q': { unsigned long long r = [[ret toObject] unsignedLongLongValue];
asm volatile("ldr x0, %0": "=m"(r)); break; }
case 'f': { float r = [[ret toObject] floatValue];
asm volatile("ldr s0, %0": "=m"(r)); break; }
case 'd': { double r = [[ret toObject] doubleValue];
asm volatile("ldr d0, %0": "=m"(r)); break; }
case '#':
case '@': {
//使用__autoreleasing是为了防止其生命周期结束时调用release破坏内联汇编写入的x0寄存器的值
__autoreleasing id obj = [ret toObject];
obj = UnmakeJSObj(obj) ?: obj;
if ([obj isKindOfClass:JOObj.class]) {
__autoreleasing id r = [obj obj];
asm volatile("ldr x0, [%0]":"=r"(r), "=m"(*r));
} else if ([obj isKindOfClass:JOWeakObj.class]) {
__autoreleasing id r = [obj obj];
asm volatile("ldr x0, [%0]": "=r"(r), "=m"(*r));
} else {
__autoreleasing id r = obj;
asm volatile("ldr x0, %0": "=m"(r));
}
break;
}
case '^':
case '*': {
__autoreleasing id obj = [ret toObject];
obj = UnmakeJSObj(obj);
if ([obj isKindOfClass:JOPointerObj.class]) {
void *r = [obj ptr];
asm volatile("ldr x0, %0": "=m"(r));
}
break;
}
case ':': {
__autoreleasing id obj = [ret toObject];
obj = UnmakeJSObj(obj);
if ([obj isKindOfClass:JOSelObj.class]) {
SEL r = [obj sel];
asm volatile("ldr x0, %0": "=m"(r));
}
break;
}
default: break;
}
}
以上JOBridge调用一个Method的时候,转发调用到JS的过程。
使用扩展
1、有了对任意方法Swizzle的方案,很多以前做不了的事情现在都可以做了,比如:如果想了解整个某个模块所有的方法执行时间,给优化提供分析数据,可以改造JOGlobalSwizzle方法,将参数存在栈上,然后跳转至一个公共的方法,记录开始执行时间,再将栈上的参数恢复到寄存器上,然后调用原始方法,然后再跳转至一个公共的方法将结束时间和开始时间一减就可以得到执行时间,整个过程不需要解析参数。
为了造福大众,我这里给一个简单的实现demo:
static NSMutableDictionary *_OriginIMP;
OS_ALWAYS_INLINE void JOGlobalCSwizzle(void) {
asm volatile("stp x29, x30, [sp, #-0x10]!");
asm volatile("mov x29, sp\n\
sub sp, sp, #0xb0");
asm volatile("str d7, [sp, #0x88]\n\
str d6, [sp, #0x80]\n\
str d5, [sp, #0x78]\n\
str d4, [sp, #0x70]\n\
str d3, [sp, #0x68]\n\
str d2, [sp, #0x60]\n\
str d1, [sp, #0x58]\n\
str d0, [sp, #0x50]\n\
str x8, [sp, #0x40]\n\
str x7, [sp, #0x38]\n\
str x6, [sp, #0x30]\n\
str x5, [sp, #0x28]\n\
str x4, [sp, #0x20]\n\
str x3, [sp, #0x18]\n\
str x2, [sp, #0x10]\n\
str x1, [sp, #0x8]\n\
str x0, [sp]\n\
");
asm volatile("bl _beforeInvoke");
asm volatile("ldr x0, [sp]");
asm volatile("ldr x1, [sp, #0x8]");
asm volatile("bl _getImp");
asm volatile("mov x17, x0");
asm volatile("ldr d7, [sp, #0x88]\n\
ldr d6, [sp, #0x80]\n\
ldr d5, [sp, #0x78]\n\
ldr d4, [sp, #0x70]\n\
ldr d3, [sp, #0x68]\n\
ldr d2, [sp, #0x60]\n\
ldr d1, [sp, #0x58]\n\
ldr d0, [sp, #0x50]\n\
ldr x8, [sp, #0x40]\n\
ldr x7, [sp, #0x38]\n\
ldr x6, [sp, #0x30]\n\
ldr x5, [sp, #0x28]\n\
ldr x4, [sp, #0x20]\n\
ldr x3, [sp, #0x18]\n\
ldr x2, [sp, #0x10]\n\
ldr x1, [sp, #0x8]\n\
ldr x0, [sp]\n\
");
asm volatile("blr x17");
asm volatile("str x0, [sp, #0xa0]");
asm volatile("str d0, [sp, #0xa8]");
asm volatile("ldr x0, [sp]");
asm volatile("ldr x1, [sp, #0x8]");
asm volatile("bl _afterInvoke");
asm volatile("ldr x0, [sp, #0xa0]");
asm volatile("ldr d0, [sp, #0xa8]");
asm volatile("mov sp, x29");
asm volatile("ldp x29, x30, [sp], #0x10");
}
void beforeInvoke(id obj, SEL sel) {
NSLog(@"%@:%@",obj, NSStringFromSelector(sel));
}
void afterInvoke(id obj, SEL sel) {
NSLog(@"%@:%@",obj, NSStringFromSelector(sel));
}
IMP getImp(id obj, SEL sel) {
return [_OriginIMP[NSStringFromSelector(sel)] pointerValue];
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_OriginIMP = [NSMutableDictionary dictionary];
NSArray *array = @[@"aMethod1:", @"aMethod2:"];
for (NSString *obj in array) {
Method m = class_getInstanceMethod(self.class, NSSelectorFromString(obj));
_OriginIMP[obj] = [NSValue valueWithPointer:method_getImplementation(m)];
method_setImplementation(m, JOGlobalCSwizzle);
}
[self aMethod1:8844];
[self aMethod2:@"test str"];
}
- (void)aMethod1:(int)a {
NSLog(@"method1: %d",a);
}
- (void)aMethod2:(NSString *)str {
NSLog(@"method2: %@", str);
}
@end
在viewDidLoad中aMethod1,aMethod2将IMP都改为了JOGlobalSwizzle,同时将原始IMP存储在字典中。如果有大量的函数要Swizzle就需要用脚本实现了,同时会可能会消耗不少时间。
在JOGlobalSwizzle中,之所以先通过str命令存储了所有的参数,然后跳转切面函数beforeInvoke,由其做统一处理,接着ldr x0,x1,再调用getImp获取原始方法的实现,然后恢复参数到寄存器,blr跳转到原始实现的入口地址执行。接下来暂存一下返回值,加载x0,x1,调用切面afterInvoke,最后返回。是不是so easy,日志如下:
2018-11-05 19:25:49.192508+0800 DEMO[16603:4643546] beforeInvoke::aMethod1:
2018-11-05 19:25:49.192591+0800 DEMO[16603:4643546] method1: 8844
2018-11-05 19:25:49.192645+0800 DEMO[16603:4643546] afterInvoke::aMethod1:
2018-11-05 19:25:49.192695+0800 DEMO[16603:4643546] beforeInvoke::aMethod2:
2018-11-05 19:25:49.192726+0800 DEMO[16603:4643546] method2: test str
2018-11-05 19:25:49.192770+0800 DEMO[16603:4643546] afterInvoke::aMethod2:
这只是个简单的demo,如果想要每个方法都有独自的切面调用,就需要在beforeInvoke,afterInvoke中根据分发调用了。目前这个作用有限,我再另外做一个稍微成熟点吧。
2、之前说过objc_msgSend是通过SEL确定其IMP的,但是由于编译器的缘故SEL是常量,其name和type是对应的,导致调用过程相当于只考了name的因素并没有考虑type。而我们现在能对任意的Method Swizzle,就意味着我们可以实现一个自定义的objc_msgSendCustom,让部分Method都进入这个objc_msgSendCustom,并在查找IMP的时候考虑type因素,就可以实现类似于C++的重载功能。不过没有编译器参与,Type需要手动传入还是比较麻烦的。
3、实现变长参数函数。我们知道OC是可以是用变长参数,只不过需要使用c方法取参数。通过NSInvocation,目前我仅知道通过forwardInvocation能获取。能处理变长参数就意味着可以处理任意参数,之前我为了将任意的数据(比如:int,id,class,struct等)转换成NSString,利用宏预处理了调用的方式实现了将任意的数据转成String,但该方式有一定的局限性,对于任意+变长就处理不了。而有了这个Swizzle方案就可以实现了。
未完待续