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

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, 或者一个自定义的汇编实现来解决.
试验性的代码比较挫, 各位看官见谅!

时间: 2024-09-20 00:19:41

[iOS]原生swift的hotpatch可行性初探 续1的相关文章

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

0x0 引子 最近在iOS群里面看到某应用因为Hotpatch审核被拒绝, 如果Hotpatch全面被封禁, 那还不如全切swift, 又能提高性能, 又能减少编码中犯的错误. 仔细想想如果swift也有办法被Hotpatch, 不就更加完美了?Hotpatch是无法被全面封禁的, 可爱的程序猿们总能有应对的办法 0x1 swift的方法调用方式 swift有四种方法调用方式: inline method static dispatch dynamic dispatch message send

iOS原生地图开发指南续——大头针与自定义标注

iOS原生地图开发指南续--大头针与自定义标注 在上一篇博客中http://my.oschina.net/u/2340880/blog/415360系统总结了iOS原生地图框架MapKit中主体地图的设置与应用.这篇是上一篇的一个后续,总结了系统的大头针视图以及自定义标注视图的方法. 一.先来认识一个协议MKAnnotation 官方文档告诉我们,所有标注的类必须遵守这个协议.所以可以了解,标注这个概念在逻辑属性和视图上是分开的.先来看下这个协议声明了哪些方法: ? 1 2 3 4 5 6 7

iOS原生如何加载HTML中img标签的图片

原文出自:iOS原生如何加载HTML中img标签的图片 前言 最近iOS App项目中使用Webview加载H5页面比较多,也有不少朋友经常问到这个问题,在这里我也学习学习如何通过iOS原生的方式来加载H5页面中的图片然后让webview显示图片. 相信有很多朋友也遇到过这样的问题,可是很多朋友都没有思路,不知如何入手.今天,刚好学习了一下,并写了一个简单的demo.下面让我们一起来学习一下吧! 本篇文章适合哪些人群阅读? 项目中有类似需求的,而又没有思路的 曾经做过类似需求的,可以参考两者的思

使用iOS原生sqlite3框架对sqlite数据库进行操作

使用iOS原生sqlite3框架对sqlite数据库进行操作 一.引言       sqlite数据库是一种小型数据库,由于其小巧与简洁,在移动开发领域应用深广,sqlite数据库有一套完备的sqlite语句进行管理操作,一些常用的语句和可视化的开发工具在上篇博客中有介绍,地址如下: sqlite数据库常用语句及可视化工具介绍:http://my.oschina.net/u/2340880/blog/600820.       在iOS的原生开发框架中可以对sqlite数据库进行很好的支持,这个

iOS原生地图开发指南再续——地图覆盖物的应用

iOS原生地图开发指南再续--地图覆盖物的应用 一.引言 在前两篇博客中,将iOS系统的地图框架MapKit中地图的设置与应用以及关于添加大头针和自定义大头针的相关操作做了详细的介绍.链接如下:http://my.oschina.net/u/2340880/blog/415360.http://my.oschina.net/u/2340880/blog/415441.这篇博客中将进一步讨论关于地图添加覆盖物的使用方法. 二.添加地图覆盖物的逻辑原理 地图覆盖物其实就是在地图上画一些东西,例如路径

iOS原生地图开发进阶——使用导航和附近兴趣点检索

iOS原生地图开发进阶--使用导航和附近兴趣点检索 iOS中的mapKit框架对国际化的支持非常出色.在前些篇博客中,对这个地图框架的基础用法和标注与覆盖物的添加进行了详细的介绍,这篇博客将介绍两个更加实用的功能的开发:线路导航与兴趣点搜索.前几篇博客的链接如下: 地图基础用法详解:http://my.oschina.net/u/2340880/blog/415360. 添加大头针与自定义标注:http://my.oschina.net/u/2340880/blog/415441. 添加地图覆盖

ios 用swift语言的,然后链接数据库,sqlite。

问题描述 ios 用swift语言的,然后链接数据库,sqlite. ios开发,用是swift语言.如何链接SQLite数据库.求详细代码 解决方案 iOS数据库Sqliteios SQlite操作数据库iOS中操作SQLite数据库 解决方案二: 用fmdb. http://www.tuicool.com/articles/jA3yUfj

iOS原生地图开发指南

iOS原生地图开发详解 在上一篇博客中:http://my.oschina.net/u/2340880/blog/414760.对iOS中的定位服务进行了详细的介绍与参数说明,在开发中,地位服务往往与地图框架结合使用,这篇博客主要对iOS官方的地图框架MapKit.framework进行介绍. 一.初始化地图视图与相关属性方法介绍 1.初始化地图视图 地图视图的展示依赖于MKMapView这个类,这个类继承于UIView,因此和其他View的使用方法类似.在我们需要展现地图的地方: ? 1 2

iOS 8 Swift Programming Cookbook

iOS 8 Swift Programming Cookbook   资源地址 http://pan.baidu.com/s/1c0hn1Gc   书籍介绍   源码截图   书籍截图