[iOS]原生swift的hotpatch可行性初探 续1

简介: ### 0x0 引子 之前在对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子. ### 0x1 先来一个简单的例子 来一个例子, 实现用js patch swift的方法, 功能包括: - 在js中通过类名/方法名/替换的方法, 来替换swift的方法 - 在js中通过方法名来调用原有

0x0 引子

之前在<原生swift的hotpatch可行性初探>对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子.

0x1 先来一个简单的例子

来一个例子, 实现用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

符合预期!

0x2 复杂一点

刚刚的两个方法里面没有参数, 也没有返回值, 实际上本身的复杂度就有了一定程度的降低, 在来点复杂的吧, 带上参数吧.
上代码, 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:
patched

patch成功.
改动主要在于:

  • c方法patched里面调用js的callback增加了一个参数.
  • js方法call里面改变了调用原始方法的参数列表

0x3 再复杂一点

再搞复杂一点, 我们搞一个返回值, 并且返回值是一个原始的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指针, 在调原有方法的时候用.

0x4 小结

上面的patch的方法, 都具有一定的特定性, 无法满足所有需求, 但已经为可行性做了一个更强力的证明.
在改进通用性和易用性后, swift hotpatch的雏形就出来了.
另外在调用原始方法时, 因为不确定参数表, 需要使用libffi, 或者一个自定义的汇编实现来解决.
试验性的代码比较挫, 各位看官见谅!

目录
相关文章
|
2月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
202 4
|
5月前
|
Swift iOS开发
iOS Swift使用Alamofire请求本地服务器报错-1002
iOS Swift使用Alamofire请求本地服务器报错-1002
126 1
|
6月前
|
Unix 调度 Swift
苹果iOS新手开发之Swift 中获取时间戳有哪些方式?
在Swift中获取时间戳有四种常见方式:1) 使用`Date`对象获取秒级或毫秒级时间戳;2) 通过`CFAbsoluteTimeGetCurrent`获取Core Foundation的秒数,需转换为Unix时间戳;3) 使用`DispatchTime.now()`获取纳秒级精度的调度时间点;4) `ProcessInfo`提供设备启动后的秒数,不表示绝对时间。不同方法适用于不同的精度和场景需求。
190 3
|
2月前
|
安全 数据处理 Swift
深入探索iOS开发中的Swift语言特性
本文旨在为开发者提供对Swift语言在iOS平台开发的深度理解,涵盖从基础语法到高级特性的全面分析。通过具体案例和代码示例,揭示Swift如何简化编程过程、提高代码效率,并促进iOS应用的创新。文章不仅适合初学者作为入门指南,也适合有经验的开发者深化对Swift语言的认识。
58 9
|
2月前
|
安全 API Swift
探索iOS开发中的Swift语言之美
【10月更文挑战第23天】在数字时代的浪潮中,iOS开发如同一艘航船,而Swift语言则是推动这艘船前进的风帆。本文将带你领略Swift的独特魅力,从语法到设计哲学,再到实际应用案例,我们将一步步深入这个现代编程语言的世界。你将发现,Swift不仅仅是一种编程语言,它是苹果生态系统中的一个创新工具,它让iOS开发变得更加高效、安全和有趣。让我们一起启航,探索Swift的奥秘,感受编程的乐趣。
|
3月前
|
安全 Swift iOS开发
探索iOS开发中的Swift语言之美
在数字时代的浪潮中,移动应用已成为日常生活的延伸。本文将深入探讨iOS平台上的Swift编程语言,揭示其背后的设计哲学、语法特性以及如何利用Swift进行高效开发。我们将通过实际代码示例,展示Swift语言的强大功能和优雅简洁的编程风格,引导读者理解并运用Swift解决实际问题。
|
4月前
|
安全 Swift iOS开发
探索iOS开发之旅:Swift语言的魅力与挑战
【9月更文挑战第21天】在这篇文章中,我们将一起潜入iOS开发的海洋,探索Swift这门现代编程语言的独特之处。从简洁的语法到强大的功能,Swift旨在让开发者能够以更高效、更安全的方式构建应用程序。通过实际代码示例,我们会深入了解Swift如何简化复杂任务,并讨论它面临的挑战和未来的发展方向。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和知识。
52 4
|
4月前
|
安全 编译器 Swift
探索iOS开发之旅:Swift编程语言的魅力与挑战
【9月更文挑战第5天】在iOS应用开发的广阔天地中,Swift作为苹果官方推荐的编程语言,以其简洁、高效和安全的特点,成为了开发者的新宠。本文将带领你领略Swift语言的独特魅力,同时探讨在实际开发过程中可能遇到的挑战,以及如何克服这些挑战,成为一名优秀的iOS开发者。
|
4月前
|
设计模式 前端开发 Swift
探索iOS开发:Swift与Objective-C的较量
在这篇文章中,我们将深入探讨iOS开发的两大编程语言——Swift与Objective-C。我们将分析这两种语言的特性、优势和局限性,并讨论它们在现代iOS开发中的应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和建议。
71 3
|
5月前
|
API Swift C语言
探索iOS开发:Swift中的异步编程与GCD应用
【8月更文挑战第4天】在iOS开发的海洋中,掌握Swift语言的航向是至关重要的。本文将引领你深入理解Swift中的异步编程概念,并借助Grand Central Dispatch(GCD)这一强大的工具,来简化并发编程的复杂性。我们将通过实际代码示例,展现如何在iOS应用中高效地管理后台任务和提升用户界面的响应性。
100 3