任意方法的Swizzle的应用之一AOP(续)

本篇是是上篇的续作,请先看上篇。
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源码地址

性能高度优化版
Github源码地址

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

推荐阅读更多精彩内容