This post was updated 2854 days ago and some of the ideas may be out of date.

一、前言

因为苹果的审核机制,我们修复 bug 的时候要经过如下过程

如图:

这个时间还是比较漫长的,因此热修复的出现帮助我们解决了这个问题。其中有 WaxPatch、React Native、JSPatch 等著名框架,而其中的 JSPatch 已经逐渐被各大公司所认可,我司最近也要开始使用 JSPatch 框架来助力热修复。于是我决定对它做一下整体分析与实战。为什么不叫源码解析呢?因为作者的文档写的太清楚了,而且还是中文的,我再重复一遍太班门弄斧了,所以我这里是入门小试。

二、JSPatch简介

JSPatch 是bang590大神提供的一个基于JavaScript语言的iOS平台hotfix解决方案。只需在项目引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。

github源码地址:https://github.com/bang590/JSPatch

三、实战

3.1 JSPatch 初体验:

我们做一个最简单的例子来初步认识一下 JSPatch 的强大!

1.首先我们创建一个Demo
然后我创建一个控制器,在上面加一个按钮 click,按钮的点击方法为"myBtnClick:"

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"JSPatchDemo";
    self.view.backgroundColor = [UIColor whiteColor];

    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.view addSubview:btn];
    btn.frame = CGRectMake(0, 0, 100, 100);
    btn.center = self.view.center;
    btn.backgroundColor = [UIColor blackColor];
    btn.layer.cornerRadius = 5;
    [btn setTitle:@"click" forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(myBtnClick:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)myBtnClick:(id)sender
{
    NSLog(@"?");
}

@end

此时我们点击这个按钮,myBtnClick 会被触发,在控制台打印出笑脸。

如图:

ian_jspatch3

这里我们就能通过 JSPatch 去改变这个方法的执行。

不会 JavaScript 怎么办?

作者已经为我们铺好了路,不用你会JavaScript,你只要会用工具就行了。

直接借助于作者写的JSPatchConvertor进行下代码转换 传送门->点我

当然你也可以借助于JSPatchX进行手写编码 传送门->点我

最后我们一定要把写好的js代码放在js语法检查器里面检查一下 传送门->点我

如图:

ian_jspatch4

将js代码保存为 main.js 引入到项目中去。

下面将 JSPatch 的三个主要文件引入。

如图:

ian_jspatch2

在 AppDelegate 里引入如下代码:


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [JPEngine startEngine]; NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"js"]; NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil]; [JPEngine evaluateScript:script]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; RootViewController *rootViewController = [[RootViewController alloc] init]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; self.window.rootViewController = navigationController; [self.window makeKeyAndVisible]; return YES; }

下面我们运行Demo,会发现这个click的方法被替换了。

如图:

ian_jspatch5

实战只是一个抛砖引玉,关于 JSPatch 的更多使用方法详见官方文档(中文的哟!)

传送门->点我

3.2 补丁的下发:

我们如何对 JSPatch 的补丁进行下发呢?

如图:

ianjspatch1

通过此流程图我们会发现,中间使用了RSA和DES加密,因为在补丁下发的时候,如果补丁被中间人拦截并修改,那么后果不堪设想,将会影响到此次涉及的所有版本的app,因此适当的加密和解密在此分发流程中必不可少。相信到了2017年随着苹果https的普及,安全性会更加完善。

当然对于这个我们也可以借助于第三方的平台,例如JSPatchbugly等。

那么我们应该自己搭建服务器还是使用第三方服务呢?我的观点还是那样,我们应该把时间浪费在有意义的事情上面。

四、原理分析

关于 JSPatch 的源码分析,作者写的已经很全面了,这里我就不都说了,贴一下作者的链接吧!

下面简单说一下我对 JSPatch 的一点理解,帮助你来去学习。

用一张图来总结下 JSPatch :

4.1 基本原理:

JSPatch 能通过 JS 去改写 OC 的方法主要还是通过 Runtime 实现的,我们可以利用 Runtime 通过类型或方法名得到相应的类和方法,我们也能替换某个类的方法的实现,当然还能注册一个新类,并为新类添加方法。

例如:

// 通过类名方法名反射的到响应的类和方法
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

// 替换某个类的方法为新的实现
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

// 注册一个新类,并为此类添加方法
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

具体可以学习一下《Objective-C Runtime 1小时入门教程》 对Runtime 做进一步了解。

4.2 JS如何调用OC

JSPatch 中是通过 require() 实现类的获取的,例如调用了 require('UIView') ,那么他就会在 JS 全局作用域上创建一个 JS 形式的以 UIView 命名的对象。

示例如下:

UIView = require("UIView");

var _require = function(clsName) {
  if (!global[clsName]) {
    global[clsName] = {
      __clsName: clsName
    }
  }
  return global[clsName]
}

然后进行方法调用,UIView.alloc()。JSPatch 将所有 JS 的方法调用都通过正则进行替换,统一调用 __c() 函数,然后再进行分发,做到了类似 OC/Lua/Ruby 等的消息转发机制。_methodFunc 方法就把要调用的类名和方法名传递给 OC 的。例如下面的转换:

如图:

代码如下:

Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
  if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
  var self = this
  return function(){
    var args = Array.prototype.slice.call(arguments)
    return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
  }
}})

_methodFunc() 就是把相关信息传给 OC ,OC 用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。

如图:

JS 和 OC 间互传消息是通过 JavaScriptCore 桥接的,OC 端在启动 JSPatch 引擎时会创建一个 JSContext 实例,JSContext 是 JS 代码的执行环境,可以给 JSContext 添加方法,JS 就可以直接调用这个方法:

如图:

_methodFunc() 中的 _OCcallI 就调用了这个方法

如图:

五、总结

这只是一个入门小试,如果想对 JSPatch 有更深层的理解,欢迎大家一起交流,一起学习。
最后推荐大家两个网址:

参考引用

JSPatch-实现原理详解

IOS热更新-JSPatch实现原理+Patch现场恢复

一场站着听完的iOS技术分享会

JSPatch实现原理详解

【Dev Club 分享第四期】JSPatch 成长之路