本篇是是上篇的续作,请先看上篇。
https://www.jianshu.com/p/0eb7238326f5
上一篇博客介绍了如何使用内联汇编给任意方法添加AOP切面,然后给了一个实现,文末曾提到最多仅支持6个显式(self,_cmd会占用前两个寄存器)非浮点的参数和8个浮点参数。虽然绝大部分情况下都是少于6个参数的,但是超过6个情况还是时有发生。以上只是其一,其二是对包含匿名参数的函数的支持,匿名参数都是存储在栈上的,比如:stringWithFormat:,第一个参数以后的那些匿名参数,因此还是需要提供对更多参数的支持。
如果对ARM64参数传递规则了解的就知道,大于8个参数会通过栈来传递。我大致画一个图就容易理解了。
当前sp在最底下,x29到sp范围是当前调用函数所需要的所有暂存的空间,x29往上16个Byte保存的是上一次x29,30,当前函数调用完后恢复x29,30使用。再往上是上一次sp的地址,其存储的就是额外的参数。所以当前函数在调用的时候会去该位置(x29+0x10)获取额外的参数。
了解了这个再看看遇到的情况,原始调用函数A调用函数B在加入Swizzle的情况变成了A->Swizzle->...->B,所以此时函数B到同样的位置拿到的数据就不正确了。怎么办呢?一种是让B到A存参数的位置读取,这明显行不通,毕竟到了运行时,B代码已经编译完成,是固定的,正常方法无法改变,Swizzle之后B无法知道A存参数的位置。第二种将A的放置的参数再拷贝一份到B需要的位置,换句话说就是建立一个伪栈。大致的思路是有了,但可行性呢?
众所周知sp寄存器的重要性,我们在函数中定义的临时变量一般都需要sp+偏移量来读取写入,一旦随意改变,后果可想而知,所以我们必须在伪栈调用完后立即还原sp指针,而且存参数的位置也需要传递过来。解决了这个问题,还有第二个问题就是栈上到底存了多少参数,不需要知道其如何存储,只需要知道其大小。
NSMethodSignature的frameLength方法可以获取总共参数大小,然后我从二进制源码中发现frameLength-0xe0才是栈参大小。NSMethodSignature可以根据签名字符串构建,字符串可以从Method中获取,但频繁创建NSMethodSignature对象还是开销较大,这里我将其和Class,selector关联起来缓存。OK,原理大致如此。
ZWFrameLength
先实现frameLength获取
/* 0xe0是基础大小,其中包含9个寄存器共0x48,8浮点寄存器共0x80,还有0x18是额外信息,比如frameLength,
超过0xe0的部分为栈参数大小
*/
int ZWFrameLength(void **sp) {
id obj = (__bridge id)(*sp);
SEL sel = *(sp + 1);
Class class = object_getClass(obj);
if (!class || !sel) return 0xe0;
[_ZWLock lock];
NSMutableDictionary *methodSigns = _ZWAllSigns[NSStringFromClass(class)];
[_ZWLock unlock];
NSString *selName = class_isMetaClass(class) ? ZWGetMetaSelName(sel) : NSStringFromSelector(sel);
NSMethodSignature *sign = methodSigns[selName];
if (sign) {
return (int)[sign frameLength];
}
Method method = class_isMetaClass(class) ? class_getClassMethod(class, sel) : class_getInstanceMethod(class, sel);
const char *type = method_getTypeEncoding(method);
sign = [NSMethodSignature signatureWithObjCTypes:type];
[_ZWLock lock];
if (!methodSigns) {
_ZWAllSigns[NSStringFromClass(class)] = [NSMutableDictionary dictionaryWithObject:sign forKey:selName];
} else {
methodSigns[selName] = sign;
}
[_ZWLock unlock];
return (int)[sign frameLength];
}
函数实现还是比较简单的,class_getInstanceMethod调用,NSMethodSignature对象创建还是有一定开销的,特别是frameLength会被频繁调用,所以还是需要缓存一下。这里type作为Key来缓存性能应该也不差,特别是内存开销小很多,type一致,sign也就一致。但有一点需要注意,class_getInstanceMethod在类定义的方法少的时候开销不大,开销较大的情况是Swizzle了大量父类的方法,特别是苹果提供的父类方法较多,所以如果使用这种方式Swizzle需要指明方法实现所在的类,可以减少循环。我这里空间换时间,按class和selector来映射。lock前后使用了两次,主要是为了减少临界区的大小,可以提高效率。
构造伪栈
接下来讲调用时候如何构造伪栈和注意事项
void ZWAopInvocation(void **sp, NSDictionary *Invocation, ZWInvocationOption option) {
id obj = (__bridge id)(*sp);
SEL sel = *(sp + 1);
if (!obj || !sel) return;
NSInteger count = ZWGetInvocationCount(Invocation, obj, sel);
__autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(option)];
NSInteger frameLenth = ZWFrameLength(sp) - 0xe0;
for (int i = 0; i < count; ++i) {
ZWGetAopImp(Invocation, obj, sel, i);
asm volatile("cbz x0, LZW_20181107");
asm volatile("mov x17, x0");
asm volatile("ldr x14, %0": "=m"(arr));
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181110");
asm volatile("add x12, x11, 0xc0");
asm volatile("sub sp, sp, x13");//增长sp
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181110:");
asm volatile("bl _ZWLoadParams");
asm volatile("mov x1, x14");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x1e0");//恢复sp
asm volatile("LZW_20181107:");
}
}
相比较于上篇博客的版本,函数上半部分是一样的,这里需要调用frameLength获取栈帧大小,这里使用NSInteger存储,其和寄存器大小一致,方便操作。
新增代码读取frameLength-0xe0的值到x13,x12=x11(sp的值)+0xc0。其中0xc0=0xb0+0x10,而0xb0为ZWGlobalOCSwizzle中栈的大小。如果x13=0,则直接跳到LZW_20181110,否则顺序执行。
接下来就是比较危险的操作sub sp, sp, x13
,将sp增长x13的长度,新增的空间就是我之前说的伪栈,此时,在sp未恢复之前,之前定义的C变量frameLenth,sp,arr等全部失效了,所以不要再使用这些变量了,当然实在是需要可以强行通过x29+偏移量来读取,只不过比较麻烦,需要知道这些临时变量的具体位置,再计算相对于x29的偏移量。
跳转ZWCopyParams,其作用是将x12指向的栈中参数全部复制到伪栈上。
不管是否调用ZWCopyParams都需要调用ZWLoadParams,加载所有的寄存器参数。
blr x17
跳转x17,调用函数。
恢复sp,这个时候x29寄存器还是有效的, 所有可以根据它恢复sp。
注意
恢复sp时,偏移量0x1e0怎么来的?将代码全部汇编,去_ZWAopInvocation入口处找,看其开始将sp调整的大小,需要注意的是编译器开启优化后这个值可能会改变,比如我这里Xcode10.1开启Os级优化会变成0xa0,不同的版本的编译器,不同的优化等级可能该值都不一样,这个是我比较头疼的地方,该函数稍有改动就需要处理这个问题,所以这个库最好在指定的环境下编译成.a再使用(不开启优化生成.a库性能也不差,另外不知道为什么函数级别的优化关闭选项__attribute__((optnone))
无效,不然可以关闭Xcode对该函数的优化),实在不行可以通过Debug和Release环境来区分该值。我再想想看有没有别的办法解决这个问题,人不能被问题憋死,不要撞死胡同,总是有各种招可以解决的。
ZWCopyParams
OS_ALWAYS_INLINE void ZWCopyParams(void) {
//x12=原始栈参数地址,x13=frameLength-0x0e0
asm volatile("mov x15, sp");
asm volatile("LZW_20181108:");
asm volatile("cbz x13, LZW_20181109");
asm volatile("ldr x0, [x12]");
asm volatile("str x0, [x15]");
asm volatile("add x15, x15, #0x8");
asm volatile("add x12, x12, #0x8");
asm volatile("sub x13, x13, #0x8");
asm volatile("cbnz x13, LZW_20181108");
asm volatile("LZW_20181109:");
}
本函数接收x12,x13作为参数,将x12指向的栈中参数依次复制到伪栈,比较简单就不具体讲解了。
这里需要注意的是,寄存器的选择。这里我使用的是x9到x15这些寄存器,这些属于易失性寄存器,简单来说就是临时暂存,很容易被修改,不可靠。使用它们的好处是不用考虑将其之前存的数据转存到内容,使用完后一般不需要恢复原数据。
x16-x17是调用暂存的,比如我这里就将函数入口存入x17,然后通过blr命令跳转的。x18是平台保留寄存器,一般用不上。
函数A{
返回值C = 函数B//存储在x19
函数D
函数E(C)//mov x0, x19
}
函数B{}
函数D{
使用x19之前需要将x19存储在内存中,之后再恢复
}
函数E(C){}
x19-x28是调用上下文暂存数据的,其是非易失性,具体来讲就是在函数若干范围内的,例如:我在函数A开始处调用了函数B获得了一个返回值C,函数A实现代码体接下来需要多次使用C,就可以将C存在x19-x28中,存在栈上也是可以的,但效率较差。然而如果在函数A的某处调用了函数D,D中也使用x19-x28中相同的寄存器,这就需要先暂存之前数据,之后再恢复,比较麻烦。
对于OC这类的动态调用语言,调用关系不固定的情况下,最好不要使用x0-x8,q0-q7之外的寄存器传参。我这里之所以使用x11,x12,x13这些易失性寄存器传参是因为调用关系简单且固定,调用上下文也不会有其他操作来破坏其内容(什么系统中断啥的,就不用我们操心了,其会保存上下文并恢复的),同时省下暂存原数据的操作。
ZWInvocation
void ZWInvocation(void **sp) {
__autoreleasing id obj;
SEL sel;
void *obj_p = &obj;
void *sel_p = &sel;
NSInteger frameLenth = ZWFrameLength(sp)- 0xe0;
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x10, %0": "=m"(obj_p));
asm volatile("ldr x0, [x11]");
asm volatile("str x0, [x10]");
asm volatile("ldr x10, %0": "=m"(sel_p));
asm volatile("ldr x0, [x11, #0x8]");
asm volatile("str x0, [x10]");
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x0, [x11]");
asm volatile("ldr x1, [x11, #0x8]");
asm volatile("bl _ZWGetOriginImp");
asm volatile("cbnz x0, LZW_20181105");
__autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(ZWInvocationOptionReplace)];
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x0, [x11]");
asm volatile("ldr x1, [x11, #0x8]");
asm volatile("bl _ZWGetCurrentImp");
asm volatile("cbz x0, LZW_20181106");
asm volatile("mov x17, x0");
asm volatile("ldr x14, %0": "=m"(arr));
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181111");
asm volatile("add x12, x11, 0xc0");//0xb0 + 0x10
asm volatile("sub sp, sp, x13");
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181111:");
asm volatile("bl _ZWLoadParams");
asm volatile("mov x1, x14");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x70");
asm volatile("b LZW_20181106");
asm volatile("LZW_20181105:");
asm volatile("mov x17, x0");
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181112");
asm volatile("add x12, x11, 0xc0");
asm volatile("sub sp, sp, x13");//增长sp
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181112:");
asm volatile("bl _ZWLoadParams");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x70");//恢复sp
asm volatile("LZW_20181106:");
}
本函数与ZWAopInvocation改动类似,这里就不作具体讲解了。
给出测试
- (void)viewDidLoad {
ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str ,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
NSLog(@"after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
});
ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionReplace | ZWInvocationOptionOnly, ^(NSArray *info, NSString *str,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
NSLog(@"replace2 | after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
});
ZWAddAop(self, @selector(aMethod4::::::::), ZWInvocationOptionAfter, ^int (NSArray *info,NSInteger str, NSInteger a2, NSInteger a3, NSInteger a4, NSInteger a5, NSInteger a6, NSInteger a7, NSInteger a8){
NSLog(@"after43: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
return 11034;
});
[self aMethod2:@"test str" :@"this is a test" :@"this is a test":@"this is a test":@"this is a test":@"this is a test":@"this is a test a7":@"this is a test a8"];
[self aMethod4:1 :2 :3 :4 :5 :6 :7 :8];
}
- (void)aMethod2:(NSString *)str :(NSString *)a2 :(NSString *)a3 :(NSString *)a4 :(NSString *)a5 :(NSString *)a6 :(NSString *)a7 :(NSString *)a8 {
NSLog(@"method2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
}
- (void)aMethod4:(NSInteger)str :(NSInteger)a2 :(NSInteger)a3 :(NSInteger)a4 :(NSInteger)a5 :(NSInteger)a6 :(NSInteger)a7 :(NSInteger)a8 {
NSLog(@"method4: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
}
2018-11-23 14:54:56.943007+0800 DEMO[12814:3984030] replace2 | after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943074+0800 DEMO[12814:3984030] after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943584+0800 DEMO[12814:3984030] method4: 1 5 6 7 8
2018-11-23 14:54:56.943607+0800 DEMO[12814:3984030] after43: 1 5 6 7 8
总结
之所以费力的做AOP,是因为这货确实好用。例如:debug,日志输出,代码优化,非侵入式埋点,网络接口记录等等。之后如果有时间我会将代码再优化,增加些必要功能,提高其效率和可靠性。
最后聊几句:使用内联汇编,可以结合高级语言和低级语言两者的优点,低级语言完成高级语言无法完成的工作,提高效率,高级语言则减少开发难度,从这个角度看确实是很棒。
呵呵,一切听上去美好的故事总是有曲折可怖的过程。两者混合使用,各自的缺点也就糅在一起了。汇编书写易出错,C/OC会生成额外的复杂操作,稍微操作不当,程序就Crash了,一脸懵逼,而且错误难以排查,如果要增加功能,修改代码也比较麻烦,很多东西都是写死的(不写死就意味着更多的工作量,更复杂的代码逻辑,同时内联汇编不支持汇编宏,当然宏还是支持的,这倒是可以简化不少代码)。所以一般只有在必须使用的时候才使用,比如极致的效率优化,完成后不怎么修改的库。随随便便嵌入汇编,就是自己给自己挖坑,当然要是有特殊用途🙂🙂🙂,比如吹牛逼,整人啥的,就多多益善了。因此内联汇编谨慎使用,书写的时候要遵循两者的机制,如果不知道C/OC背地里偷偷干了什么就将其汇编排查,这可以解决很多问题。
性能高度优化版
Github源码地址