之前在<原生swift的hotpatch可行性初探>对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子.
来一个例子, 实现用js patch swift的方法, 功能包括:
在js中通过类名/方法名/替换的方法, 来替换swift的方法在js中通过方法名来调用原有的swift方法swift代码:
public class ViewController: UIViewController { override public func viewDidLoad() { super.viewDidLoad() patch_init() // patch 初始化及完成patch操作 aclass().hehe() // 调用aclass的hehe方法, hehe方法里面会调用hehe1 } } public class aclass { public func hehe() { hehe1() } public func hehe1() { print("hehe1") } }!!!请注意, 前方高能, 请务必阅读开头提到的上一篇文章!!!patch方法的代码:
static JSContext *jsContext = nil; static JSValue *jsFunction = nil; void patched() { [jsFunction callWithArguments:nil]; } void patch_init() { jsContext = [[JSContext alloc] init]; jsContext[@"log"] = ^(NSString *message) { NSLog(@"%@", message); }; jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) { void *class = (__bridge void *)objc_getClass(className.UTF8String); void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (!class || !raw_method_address) { NSLog(@"class or method note found!"); return; } long offset = 0; for (long i=0; i<1024; i++) { if (*(long *)(class+i) == (long)raw_method_address) { offset = i; break; } } if (!offset) return; jsFunction = func; *(void **)(class+offset) = &patched; }; jsContext[@"call"] = ^(NSString *methodName){ void (*raw_method_address)() = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (raw_method_address) { raw_method_address(); } }; [jsContext evaluateScript:@"\ function callback(){\ log('patched hehe1');\ log('calling raw method:');\ call('_TFC9testswift6aclass5hehe1fT_T_');\ }\ patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_T_', callback);\ "]; }代码的最后一部分的js代码里面, 将testswift.aclass的_TFC9testswift6aclass5hehe1fT_T_方法替换为了js写的callback方法, 而callback方法里面又调用了原始的_TFC9testswift6aclass5hehe1fT_T_方法.
运行结果:
2016-09-09 16:44:49.639 testswift[1725:677144] patched hehe1 2016-09-09 16:44:49.640 testswift[1725:677144] calling raw method: hehe1符合预期!
刚刚的两个方法里面没有参数, 也没有返回值, 实际上本身的复杂度就有了一定程度的降低, 在来点复杂的吧, 带上参数吧.上代码, swift改成这样, 把hehe1加上参数String:
public class aclass { public func hehe() { hehe1("hehe") } public func hehe1(str: String) { print(str) } }那么问题来了, 这下就涉及到patch本身使用的语言及swift之间类型转换了.看了一下swift的短String的数据结构, 发现直接就是char*. 不过在传String值的时候, 实际上传了三个参数, char/int/void , 其中int是string长度, void*是留给objc的内存管理用的.修改patch实现如下:
static JSContext *jsContext = nil; static JSValue *jsFunction = nil; void patched() { [jsFunction callWithArguments:@[@"patched"]]; } void patch_init() { jsContext = [[JSContext alloc] init]; jsContext[@"log"] = ^(NSString *message) { NSLog(@"%@", message); }; jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) { void *class = (__bridge void *)objc_getClass(className.UTF8String); void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (!class || !raw_method_address) { NSLog(@"class or method note found!"); return; } long offset = 0; for (long i=0; i<1024; i++) { if (*(long *)(class+i) == (long)raw_method_address) { offset = i; break; } } if (!offset) return; jsFunction = func; *(void **)(class+offset) = &patched; }; jsContext[@"call"] = ^(NSString *methodName, NSString *parameter){ void (*raw_method_address)(char *, long, long) = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (raw_method_address) { raw_method_address(parameter.UTF8String, strlen(parameter.UTF8String), 0); } }; [jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) { NSLog(@"error: %@, %@", ctx.exception); }]; [jsContext evaluateScript:@"\ function callback(str){\ log('calling: patched hehe1');\ log('parameter: ' + str);\ log('calling raw method:');\ call('_TFC9testswift6aclass5hehe1fSST_', str);\ }\ patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fSST_', callback);\ "]; }跑一下结果:
calling: patched hehe1 parameter: patched calling raw method: patchedpatch成功.改动主要在于:
c方法patched里面调用js的callback增加了一个参数.js方法call里面改变了调用原始方法的参数列表再搞复杂一点, 我们搞一个返回值, 并且返回值是一个原始的swift类, 我们在hook里面对swift类做一个修改!swift代码:
public class aclass { public var p: UInt64 = 0xaaaaaaaaaaaaaaaa public func hehe() { let a = hehe1() print(a.p) } public func hehe1() -> aclass { return self } }正常情况下我们将看到输出12297829382473034410, 也就是0xaaaaaaaaaaaaaaaa的十进制. 我要把它改成1!
修改patch的实现:
static JSContext *jsContext = nil; static JSValue *jsFunction = nil; static void* selfHolder = NULL; void *patched(void *self) { selfHolder = self; JSValue *ret = [jsFunction callWithArguments:nil]; return (void *)[[ret toNumber] longLongValue]; } void patch_init() { jsContext = [[JSContext alloc] init]; jsContext[@"log"] = ^(NSString *message) { NSLog(@"%@", message); }; jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) { void *class = (__bridge void *)objc_getClass(className.UTF8String); void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (!class || !raw_method_address) { NSLog(@"class or method note found!"); return; } long offset = 0; for (long i=0; i<1024; i++) { if (*(long *)(class+i) == (long)raw_method_address) { offset = i; break; } } if (!offset) return; jsFunction = func; *(void **)(class+offset) = &patched; }; jsContext[@"call"] = (NSNumber*)^(NSString *methodName, NSString *parameter){ __block void *ret = 0; void*(*raw_method_address)(void *) = dlsym(RTLD_DEFAULT, methodName.UTF8String); if (raw_method_address) { ret = raw_method_address(selfHolder); } return [[NSNumber alloc] initWithLong:(long)ret]; }; jsContext[@"memory_write"] = ^(NSNumber *ptr, NSNumber *off, NSNumber *val) { long long pointer = [ptr longLongValue]; long offset = [off longValue]; long long value = [val longLongValue]; *(long *)(pointer+offset) = value; }; [jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) { NSLog(@"error: %@, %@", ctx, ctx.exception); }]; [jsContext evaluateScript:@"\ function callback(){\ log('calling: patched hehe1');\ log('calling raw method:');\ var ret = call('_TFC9testswift6aclass5hehe1fT_S0_');\ memory_write(ret, 16, 1);\ return ret;\ }\ patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_S0_', callback);\ "]; }跑一下结果:
2016-09-09 19:10:31.138 testswift[2040:722528] calling: patched hehe1 2016-09-09 19:10:31.139 testswift[2040:722528] calling raw method: 1请注意左下角的小1, 修改成功!这里主要增加了memory_write方法来写内存数据, 用于修改对象的属性. 这里的offset 16可以通过工具来计算. 由于在hehe1中用到了self, swift方法也隐含了对self的传递, 所以在调用原有方法的之前保存了一下self指针, 在调原有方法的时候用.
上面的patch的方法, 都具有一定的特定性, 无法满足所有需求, 但已经为可行性做了一个更强力的证明.在改进通用性和易用性后, swift hotpatch的雏形就出来了. 另外在调用原始方法时, 因为不确定参数表, 需要使用libffi, 或者一个自定义的汇编实现来解决.试验性的代码比较挫, 各位看官见谅!
