美文网首页技术类文章收集大杂烩
一站式搞定 JSPatch 热修复

一站式搞定 JSPatch 热修复

作者: Cean16 | 来源:发表于2017-01-05 16:49 被阅读455次

    最近接触到热修复, 确实能解燃眉之急, 非常好用, 故分享给大家. 这里只讲 JSPatch, 这个是现在最热门最好用的框架, 用起来超级简单, 非常感谢 bang590 的贡献.

    JSPatch 是一个开源项目, 只需要在项目里引入极小的引擎文件, 就可以使用 JavaScript 调用任何 Objective-C 的原生接口, 替换任意 Objective-C 原生方法. 目前主要用于下发 JS 脚本替换原生 Objective-C 代码, 实时修复线上 bug.

    项目集成

    [Github][1] 下载后, 按照[操作文档][2]操作就可以轻松集成, 摘录 bang590 Github 简要步骤如下:
    [1]:https://github.com/bang590/JSPatch
    [2]:https://github.com/bang590/JSPatch/blob/master/README-CN.md

    • 拷贝 JSPatch/ 目录下的三个文件 JSEngine.m / JSEngine.h / JSPatch.js 到项目里
    • #import "JPEngine.h"
    • 调用 [JPEngine startEngine]
    • 通过 [JPEngine evaluateScript:@""] 接口执行 JavaScript。
    • 直接把下面代码拷贝到 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中即可, app 运行后只调用一次, 即每次运行 app 只更新一次 JS 修复
    • 如若要更新即时性, 可以把方法放到 - (void)applicationWillEnterForeground:(UIApplication *)application 这样每次 app 从后台进入前台, 都会拉取 JS 修复文件
    // 方法一: 从网络拉回js脚本执行
    
    [JPEngine startEngine];
    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kDownloadPath]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        [JPEngine evaluateScript:script];
    }];
    
    // 上面代码 kDownloadPath 换成你自己的 JS 文件地址即可
    // 每次都从网络拉取, 虽然文件小, 但也受限也网络状态, 不太理想.
    
    // 方法二: 先下载到本地, 再从本地文件夹中读取
    
    NSURLSession *session = [NSURLSession sharedSession]; 
    NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:kDownloadPath] completionHandler:^(NSURL * _Nullable location,NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"location: %@", location);
    
    // 下载任务会把下载的资源存放到临时文件夹tmp下. block结束后, 就会自动删除.
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *path = [docPath stringByAppendingPathComponent:@"demo.js"];
    NSLog(@"path: %@", path);// 拷贝路径在 Finder ->前往 ->前往文件夹 可看到已下载文件
    
    // 测试了会有缓存, 且不能把原有的 JS 文件覆盖, 故要先移除
    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
    }
    
    // 故把下载数据移动到document下
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path]error:nil];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [JPEngine startEngine];
        [JPEngine evaluateScriptWithPath:path];
        }]; 
    }];    
    [task resume];
    
    // 上面代码 kDownloadPath 换成你自己的 JS 文件地址即可
    

    实际不需要每次都拉取, 该方法也只是暂缓措施, 下次迭代版本必须把上次 JS 修复的用原生解决, 这时需要有一个后台可以下发 JS 下载路径和管理脚本, 并且需要处理传输安全等部署工作.

    JS 文件

    JS 文件创建

    • 使用 xcode 创建 JS 文件


      xcode 创建 JS 文件.png
    • 使用 Sublime Text 工具创建 JS 文件, 同样后缀保存为 .js 即可

    JS 语法

    • 在 defineClass 里定义 OC 已存在的方法即可覆盖, 语法如下:
    defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
    
    @param classDeclaration: 字符串,类名/父类名和Protocol
    @param properties: 新增property,字符串数组,可省略
    @param instanceMethods: 要添加或覆盖的实例方法
    @param classMethods: 要添加或覆盖的类方法
    
    // 例如:
    require('UIDevice');
    defineClass("ViewController", {
        viewDidLoad: function() {
            var model = UIDevice.currentDevice().model();
            console.log(model);
            if (UIDevice.currentDevice().systemVersion().floatValue() >= 9) {
                console.log("9.0版本");
            } else {
                console.log("其他版本");
            }
            console.log("js 打印, 脚本号: 1.0, 替换实例成功");
        }
    }, {
        test: function() {
            console.log("js 打印, 脚本号: 1.0, 替换类方法成功");
        }
    });
    
    • 要替换多个方法, 都要重新写 defineClass("类名", [新增属性,], {实例方法}, {类方法}), 属性可以省略.

    • 只有类方法或者实例方法, 就留空大括号 {}, 如只需修改类方法: defineClass("类名", {}, {类方法}).

    • 在方法名前加 ORIG 即可调用未覆盖前的 OC 原方法:

    •   viewDidLoad: function() {
         self.ORIGviewDidLoad();
       },
      
    
    - 在 JS 里面判断是否为空要判断 false
    
    - ```java
        var url = "";
        var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
        if (rawData != null) {} //这样判断是错误的
        应该如下判断:
        if (!rawData){}
        在JSPatch.js源码里_formatOCToJS方法对undefined,null,isNil转换成了false。
    
    • Objective-C 里的常量/枚举/宏/全局变量不能直接在 JS 上使用
    • 更多语法见 JSPatch 基础语法, 也可以借助 JSPatch 代码转换器, 当然转换器不是万能了, 还需要自己细心检查.
    • JSPatch 替换的是整个方法, 哪怕只有一行代码需要修复, 整个方法都需要重写成 JS 代码. 倡导使用敏捷开发的思想, 类似于主逻辑或者是功能模块入口的方法可以抽的更细, 这样即使需要修改, 成本也不会太大.

    版本管理

    公司搭建后台

    自己公司搭建后台, 除了下发拉取 JS 的地址外, 还可以加入一些参数, 比如: 版本控制, 指定修复某 iOS 版本等等, 条件根据需求定, 跟一般请求无异, 就不叙述了.

    七牛云平台

    JS 文件也可以存放到七牛云上, 七牛云同样提供版本控制, 这样自己公司后台省很多事, 只需写一个接口, 而且有一定的免费额度, 足够用了.

    七牛云平台.png
    七牛云使用流程
    • 注册完七牛云账号后, 点击添加对象存储创建储存空间, 访问控制注意选公开空间, 这样外界才能访问到 JS 文件.
    添加对象存储.png 创建储存空间.png 内存管理.png
    • 上传文件后, 复制外链接就是 JS 文件路径
    上传文件.png
    • 需要注意的是七牛云平台文件是有缓存的, 所以在上传 JS 文件的命名不要和前面重复, 不然下发后看到结果会是上一次同名文件效果, 缓存时间可以在空间设置里设置
    文件缓存时间.png

    JSPatch 平台

    不想搭建后台, 可以使用 JSPatch 平台, 也不用把 JS 文件上传到七牛云, 直接上传到 JSPatch 平台即可, 功能很多, 还提供条件下发, 平台文档介绍已经非常详细了, 这里就不再赘述了.

    不过平台是需要收费的

    JSPatch平台收费.png
    !!!使用 JSPatch 平台注意点
    • 注意在 JSPatch 平台的规范里,JS 脚本的文件名必须是 main.js
    • 自定义 RSA 密钥, 按照提示在终端输入命令后, 生成的文件在主目录下:
    RSA 文件.png
    • 按照文档那样导入 public_key 太麻烦了, 而且容易出错, 可以把 rsa_public_key.pem 文件拖入工程中, 再执行下面代码就可以:

    NSString *keyPath = [[NSBundle mainBundle] pathForResource:@"rsa_public_key" ofType:@"pem"];
    NSString *publicKey = [NSString stringWithContentsOfFile:keyPath encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"publicKey: %@", publicKey);
    [JSPatch setupRSAPublicKey:publicKey];
    //下方是 JSPatch 启动代码
    [JSPatch startWithAppKey:@"19ed6339k440fa3ab"];

    ifdef DEBUG

    [JSPatch setupDevelopment];

    endif

    [JSPatch sync];

        
    
    ######集成错误录
    
    - 若使用 XCode8 接入,需要在项目 Capabilities 打开 Keychain Sharing 开关,否则在模拟器下载脚本后会出现 `decompress error, md5 didn't match` 错误(真机无论是否打开都没问题)
    
    - pod JSPatch 平台 SDK 完成并添加依赖库, 启动 `startWithAppKey` 和 `sync` 后报错
    
    - ```objc
    duplicate symbol _OBJC_METACLASS_$_JPEngine in:
        /Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Intermediates/ViewController.build/Debug-iphonesimulator/ViewController.build/Objects-normal/x86_64/JPEngine.o
        /Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Products/Debug-iphonesimulator/JSPatch/libJSPatch.a(JPEngine.o)
    ld: 11 duplicate symbols for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
     //原因是工程里有手动导入 JSPatch.h JSPatch.m 和 JSPatch.js 文件, 和 cocoapods 冲突了
    

    小结

    JSPatch 热修复集成简单吧, 难点在 JS 语法上, 没有语法提示, 写的时候更要细心.
    如果没有效果的话, 检查 JS 语法是否正确, 也可以通过 Safari 的调试工具对 JS 进行断点调试, 详见 JS 断点调试, 还有是否执行之前缓存的文件.

    相关文章

      网友评论

      本文标题:一站式搞定 JSPatch 热修复

      本文链接:https://www.haomeiwen.com/subject/jguzvttx.html