浅析JSPatch的使用

浅析JSPatch的使用

一. 背景介绍

  • 背景:iOS作为苹果独家开发和运营的生态圈,具有非常封闭的运作环境,其App上线需要通过提交,排队,审核,上线这四个很长的过程,过往的排队时间长至于7~15天,对一个软件迭代就有了极大的限制,如果产品中出现了小bug, 需要修复问题的话就是及其困难的了,虽然目前苹果的审核时间已经短至1天,但是其App更新方式为全量更新,如若为了修复一个极小的问题而强迫用户更新一个版本的话,体验也是不好的, 所以在多年的技术进化中,产生了App热更新这样的一个需求。

  • 发展: 在这几年来的iOS开发界,各个厂商和各种极客各现神通,通过各种黑科技来实现各种热更新技术的实现,在iOS7之前,著名的有Wax框架,Wax框架主要使用Lua语言进行脚本实现,并且需要在原生的App中植入Wax的脚本引擎,使用上也不是太方便,所以逐渐被淘汰。2013年,苹果在iOS7中引入了原生框架JavascriptCore这样一个原生框架,彻底打开了JS脚本和native调用之间的桥梁,也为热更新技术的实现提供了原生的技术支持,各种极客和软件开发商都定制自己的脚本来调用本地的部分代码,但是都没有统一的方案,在这一个沉淀的过程中,产生了较为成熟易用的JSPatch框架。

二. JSPatch介绍

  • 诞生:JSPatch诞生于2015年5月,最初出自于腾讯广研院的高级iOS开发工程师@bang的个人项目,其超级深厚的开发基础和过人的天赋在这个项目中表现的淋漓尽致,其基本原理为使用javascriptcore中提供的调用Objective-C的原生入口,并充分利用Objective-C的动态特点来在运行时修改方法的入口和实现,用于替换原有的旧方法。 目前JSPatch在Github上已经开源,拥有大量的拥簇,并且充分在腾讯,阿里,百度各个大厂的产品中得到了验证,腾讯甚至提供了JSPatch托管SDK平台用于商用,可见其实现已经成熟到了商用的地步,可以放心使用了。以下为其他开发者总结的JSPatch和Wax的区别:
浅析JSPatch的使用_第1张图片
JSPatch_Wax.jpg

基本原理

  • Objective-C是动态语言,具备运行时特性,所以能够在运行时通过类名称或者方法的名称来获取执行入口,并且进行实际调用,而且还可以通过runtime特性来swizzle各种方法,进行动态修改。
  • 通过类和方法名称进行运行时调用的片段:
Class class = NSClassFromString(@"UIViewController");        
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString(@"viewDidLoad");
[viewController performSelector:selector];

动态替换类方法为新方法的片段:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

正是由于OC语言具有如上片段展示的runtime时决定调用方法的特性,成就了热更新的基础原理。

具体的实现原理比较复杂,作者已经开源了代码,并且在博客中写明了实现过程中的各种问题,如果有兴趣的可以参阅以下链接:
JSPatch在github的源地址,
JSPatch详细实现原理

三. 使用方法

  • 基本语法解析

下面展示一下OC代码和热更新的JS脚本的对应代码片段,例如线上 APP 有一段代码出现 bug 导致 crash:

@implementation JPTableViewController
...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSString *content = self.dataSource[[indexPath row]];  //可能会超出数组范围导致crash
  JPViewController *ctrl = [[JPViewController alloc] initWithContent:content];
  [self.navigationController pushViewController:ctrl];
}
...
@end

可以通过下发这样一段 JS 代码,覆盖掉原方法,修复这个 bug:

//JS
defineClass("JPTableViewController", {
  //instance method definitions
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    var row = indexPath.row()
    if (self.dataSource().length > row) {  //加上判断越界的逻辑
      var content = self.dataArr()[row];
      var ctrl = JPViewController.alloc().initWithContent(content);
      self.navigationController().pushViewController(ctrl);
    }
  }
}, {})

除了修复一些bug之外,也可以动态的修改一些App的行为, 详细越发可以查阅
JSPatch语法WIKI

  • SDK接入使用

前面已经提到,腾讯已经将JSPatch进行了商用包装,使用方式非常简单,进入JSPatch的官方站点,下载SDK,将SDK拖入工程,然后添加官方javascriptcore.framework就可以按照文档进行使用了,腾讯官方还进行了后端平台托管,使用者在后端注册App获取Key,然后在App端初始化即可使用,需要热更新的脚本也配置在后端进行下发,使用非常方便简洁,该平台提供每天1W次下发的免费量,超过1W次需要收费服务,详细使用方式可以查阅:
JSPatch官方网站以及介绍

  • 源代码引入使用

首先构建一个简单的demo, demo洁面只有一个按钮,按钮下面一个Label框,demo的简单逻辑是,点击按钮之后,Label框背景颜色变为蓝色,并且显示native code字样。

原始代码片段如下:

浅析JSPatch的使用_第2张图片
demoSource.png

初始运行界面为:

浅析JSPatch的使用_第3张图片
demoOne.png

点击changeColor按钮之后,事件响应并执行,运行界面变为:

浅析JSPatch的使用_第4张图片
demoBlue.png

以上为native代码的原始逻辑,下面我们开始进行动态替换:

  • 思路

我们要动态替换changeColor的点击事件,其实就是需要动态替换clickChangeColorEvent这个函数,大家可以去按照上面的JSPatch的语法wiki中去查阅详细语法,我们这里有更简单直接的方法,JSPatch的作者为了使用者方便,开发了
JSPatchConverter这个工具,大家可以去详细看一下使用,也可以直接下载成品App, 我们这里使用成品App进行JS代码生成,只需要在App的左侧写入要替换的方法,点击convert按钮,右边窗口即可生成热更新的脚本代码,如图:

浅析JSPatch的使用_第5张图片
jsconverter.png

将右边窗口的成品JS片段自己保留出来,然后放在自建的服务器准备下发使用。

  • 本地工程准备

从github中下载JSPatch的源码,并在源码工程中找到JSPatch文件夹,将该文件夹拖入demo工程,如图:

浅析JSPatch的使用_第6张图片
treeOne.png

同时添加苹果官方的javascriptcore.framework,如图:

浅析JSPatch的使用_第7张图片
treeTwo.png
  • 代码实现

首先要在AppDelegate.h中添加对应的头文件:

#import "JSPatch/JPEngine.h"

继而在iOS的App启动入口函数中添加JSEngine的初始化:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    [JPEngine startEngine];
    
    return YES;
}

以上两步已经完成了JSEngine的简单的初始化过程,然后进入下一步,获取远端脚本的过程。 将前面在JSConverter中生成的脚本代码自己放入自建服务器中(这一步自己寻找后端兄弟协助完成), 然后在实现一个获取脚本的函数,进行脚本获取,并将执行获取脚本的函数放在[JPEngine startEngine]之后执行,然后在获取到脚本之后,执行脚本行为,以下为完整范例:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    [JPEngine startEngine];
    [self getAndRunJSPatchScript];
    return YES;
}

- (void)getAndRunJSPatchScript
{
    NSURLSession *urlSession = [NSURLSession sharedSession];
    NSMutableURLRequest *httpRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxxxx/api"]];
    [httpRequest setHTTPMethod:@"POST"];
    NSURLSessionDataTask *dataTask = [urlSession dataTaskWithRequest:httpRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if(!error)
        {
            NSString *targetJSPatchScript = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            [JPEngine evaluateScript:targetJSPatchScript];
        }
    }];
    [dataTask resume];
}

然后运行新的程序,点击changeColor按钮,行为已经发生了改变:

浅析JSPatch的使用_第8张图片
demoYellow.png

可以看到,图中的色块已经变成了黄色,图中的提示语言也显示JSPatch代码进行了执行,而这一个过程当中,native的逻辑代码并没有改变。

  • 归纳

上面代码片段中的getAndRunJSPatchScript函数只是简单的调用了接口,获取了脚本,然后执行。更长远的规划,服务端完全可以实现一个脚本管理服务,提供一系列结构,将不同App版本,不同运营行为的脚本进行分类管理,App只需要上报自己的App基本信息,然后由服务端来返回要热更新的脚本,更加的灵活和体系化。

总结

上述内容可以看到JSPatch确实是一个很方便的热更新库,但是个人认为在App的开发过程中,大家还是应该把质量管控在开发阶段,不可依赖于这样的热更新修复,这样的修复可以用于应急来修补漏网的bug, 但是不建议作为App的主要逻辑开发层,因为这样的会带来更多的外部管理App的功能,从某种意义上来说也添加了运营的复杂性, 而且在苹果主推的未来开发语言swift中,这种OC语言所具有的动态性将会不再存在,以这套原理来运作的JSPatch库也就无从使用了,但是在当下的实际OC仍然是很多App的主开发语言的时代,这个热更新修复的库还是非常非常实用的,至于各位开发者将会对这个库依赖多深,这就是自己定夺的问题了。

你可能感兴趣的:(浅析JSPatch的使用)