简单整理了这个是由于某些需要,其实作者文档有很详细的介绍,包括原理。大部分也都是直接摘录下来的。
JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态 添加模块 ,或 替换项目原生代码动态修复 bug。
1. 流程
主要的过程是,在JP平台(或者自己后台)发布补丁JS文件,选择下发方式(全量、灰度、开发等)。客户端接入JPSDK(或者使用自己服务的下载更新逻辑),自动更新下载最新的js执行。
2. 原理
1). 基础原理
通过 Objective-C Runtime 在运行时,可以通过类名/方法名反射得到相应的类和方法。
也可以替换某个类的方法为新的实现.
还可以新注册一个类,为类添加方法.
基本原理就是:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法
2).JS接口实现
首先看下如何用JS调用OC方法的
require('UIView');
var view = UIView.alloc().init();
在js中这样的语法是会报错的,作者的做法是在加载我们的补丁main.js
文件之前先在JSPatch.js
中加载了一个匿名自执行函数。主要作用就是用于替换这些接口。
通过正则把所有方法调用都改成调用
__c()
函数>
给 JS 对象基类 Object 的 prototype 加上 __c
成员,这样所有对象都可以调用到 __c
,根据当前对象类型判断进行不同操作.
具体做法:在OC加载main.js
的时候,修改了js代码,调用了这个匿名函数并利用正则增加了__c
的调用
NSString *formatedScript = [NSString stringWithFormat:
@";(function(){try{%@}catch(e){_OC_catch(e.message, e.stack)}})();",
[_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];
所以在main.js
执行的时候 已经转化为如下,alloc
、init
被转化为了字符串。
;(function(){
try{require('UIView');
var view = UIView.__c("alloc")().__c("init")();
}
catch(e){
_OC_catch(e.message, e.stack)
}
})();
3). 消息传递
在JS接口完成之后,要做的就是传递给OC,使用的JavaScriptCore的接口。在JPEngine启动的时候,定义好对应的执行block。在执行js中的definClass
方法替换的时候就会调用runtime,实现方法替换。
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{
return defineClass(classDeclaration, instanceMethods, classMethods);
};
context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol)
{
return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
};
4). 方法替换
这里没有简单的使用class_replaceMethod
来进行方法的替换,因为考虑到参数的原因。
当调用一个 NSObject 对象不存在的方法时,并不会马上抛出异常,而是会经过多层转发,层层调用对象的 -resolveInstanceMethod:;
-forwardingTargetForSelector:;
-methodSignatureForSelector:, -forwardInvocation: 等方法.
在第三个阶段
-forwardInvocation:
是会有一个 NSInvocation 对象,这个 NSInvocation 对象保存了这个方法调用的所有信息,包括 Selector 名,参数和返回值类型,最重要的是有所有参数值,可以从这个 NSInvocation 对象里拿到调用的所有参数值。
具体实现,以替换 UIViewController 的 -viewWillAppear: 方法为例:
- 把UIViewController的
-viewWillAppear:
方法通过class_replaceMethod()
接口指向_objc_msgForward
,这是一个全局 IMP,OC 调用方法不存在时都会转发到这个 IMP 上,这里直接把方法替换成这个 IMP,这样调用这个方法时就会走到-forwardInvocation:
。
- 为UIViewController添加
-ORIGviewWillAppear:
和 -_JPviewWillAppear:
两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数。
- 改写UIViewController的
-forwardInvocation:
方法为自定义实现。一旦OC里调用 UIViewController 的-viewWillAppear:
方法,经过上面的处理会把这个调用转发到-forwardInvocation:
,这时已经组装好了一个 NSInvocation,包含了这个调用的参数。在这里把参数从 NSInvocation 反解出来,带着参数调用上述新增加的方法-JPviewWillAppear:
,在这个新方法里取到参数传给JS,调用JS的实现函数。整个调用过程就结束了。
3. 使用
详细的JSPatch使用方式在文档中都有具体的介绍,基本涵盖了OC使用过程中需要用到的大部分情况。
以下是比较常用的几种:
1. require
在使用Objective-C类之前需要调用 require('className’)
:
require('UIView, UIColor')
var view = UIView.alloc().init()
var red = UIColor.redColor()
或者直接在使用时才调用 require() :
require('UIView').alloc().init()
2. 调用方法
- 调用类方法
var redColor = UIColor.redColor();
- 调用实例方法
var view = UIView.alloc().init();
view.setNeedsLayout();
- 获取/修改 Property 等于调用这个 Property 的 getter / setter 方法,获取时记得加 ():
view.setBackgroundColor(redColor);
var bgColor = view.backgroundColor();
- 方法名转换 多参数方法名使用
_
分隔:
var indexPath = require('NSIndexPath').indexPathForRow_inSection(0, 1);
3. defineClass
defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
@param classDeclaration: 字符串,类名/父类名和Protocol
@param properties: 新增property,字符串数组,可省略
@param instanceMethods: 要添加或覆盖的实例方法
@param classMethods: 要添加或覆盖的类方法
- 覆盖实例方法
// 覆盖viewDidAppear
defineClass("HYWebViewController", {
viewDidAppear: function(animated) {
self.super().viewDidAppear(animated);
console.log("JS---viewDidAppear---jspatch test");
},
});
- 在方法名前加 ORIG 即可调用未覆盖前的 OC 原方法:
// JS
defineClass("HYWebViewController", {
viewDidLoad: function() {
self.ORIGviewDidLoad();
},
})
- 类方法
// JS
defineClass("JPTableViewController", {
//实例方法
}, {
//类方法
shareInstance: function() {
...
},
})
- 覆盖 Category 方法与覆盖普通方法一样。
- 使用
self.super()
接口代表 super 关键字,调用 super 方法
- 动态新增 Property
可以在 defineClass() 第二个参数为类新增 property,格式为字符串数组,使用时与 OC property 接口一致:
defineClass("JPTableViewController", ['data', 'totalCount'], {
init: function() {
self = self.super().init()
self.setData(["a", "b"]) //添加新的 Property (id data)
self.setTotalCount(2)
return self
},
viewDidLoad: function() {
var data = self.data() //获取 Property 值
var totalCount = self.totalCount()
},
})
- Protocol
可以在定义时让一个类实现某些 Protocol 接口,写法跟 OC 一样:
defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {
})
这样做的作用是,当添加 Protocol 里定义的方法,而类里没有实现的方法时,参数类型不再全是 id,而是自动转为 Protocol 里定义的类型:
// objc
@protocol UIAlertViewDelegate <NSObject>
...
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex;
...
@end
// js
defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
viewDidAppear: function(animated) {
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
"Alert",self.dataSource().objectAtIndex(indexPath.row()),self, "OK", null)
alertView.show()
}
alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
console.log('clicked index ' + buttonIndex)
}
})
4. 特殊类型
- Struct
JSPatch原生支持 CGRect / CGPoint / CGSize / NSRange 这四个 struct 类型,用 JS 对象表示:
// Obj-C
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[view setCenter:CGPointMake(10,10)];
[view sizeThatFits:CGSizeMake(100, 100)];
CGFloat x = view.frame.origin.x;
NSRange range = NSMakeRange(0, 1);
// JS
var view = UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100})
view.setCenter({x: 10, y: 10})
view.sizeThatFits({width: 100, height:100})
var x = view.frame().x
var range = {location: 0, length: 1}
- Selector
在JS使用字符串代表 Selector:
//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];
//JS
self.performSelector_withObject("viewWillAppear:", 1)
5. nil
JS 上的 null 和 undefined 都代表 OC 的 nil,如果要表示 NSNull, 用 nsnull 代替,如果要表示 NULL, 也用 null 代替
6. NSArray / NSString / NSDictionary
NSArray / NSString / NSDictionary 不会自动转成对应的JS类型,像普通 NSObject 一样使用它们:
//Obj-C
@implementation JPObject
+ (NSArray *)data
{
return @[[NSMutableString stringWithString:@"JS"]]
}
+ (NSMutableDictionary *)dict
{
return [[NSMutableDictionary alloc] init];
}
@end
// JS
require('JPObject')
var ocStr = JPObject.data().objectAtIndex(0)
ocStr.appendString("Patch") // 当成oc对象使用
var dict = JPObject.dict()
dict.setObject_forKey(ocStr, 'name')
console.log(dict.objectForKey('name')) //output: JSPatch
如果要把 NSArray / NSString / NSDictionary 转为对应的 JS 类型,使用 .toJS() 接口:
// JS
var data = require('JPObject').data().toJS()
//data instanceof Array === true
data.push("Patch") // 可以作为js对象使用
var dict = JPObject.dict()
dict.setObject_forKey(data.join(''), 'name')
dict = dict.toJS()
console.log(dict['name']) //output: JSPatch
7. Block
block 传递
当要把 JS 函数作为 block 参数给 OC时,需要先使用 block(paramTypes, function) 接口包装:
// Obj-C
@implementation JPObject
+ (void)request:(void(^)(NSString *content, BOOL success))callback
{
callback(@"I'm content", YES);
}
@end
// JS
require('JPObject').request(block("NSString *, BOOL", function(ctn, succ) {
if (succ) log(ctn) //output: I'm content
}))
这里 block 里的参数类型用字符串表示,写上这个 block 各个参数的类型,用逗号分隔。NSObject 对象如 NSString *, NSArray 等可以用 id 表示,但 block 对象要用 NSBlock 表示。
从 OC 返回给 JS 的 block 会自动转为 JS function,直接调用即可:
// Obj-C
@implementation JPObject
typedef void (^JSBlock)(NSDictionary *dict);
+ (JSBlock)genBlock
{
NSString *ctn = @"JSPatch";
JSBlock block = ^(NSDictionary *dict) {
NSLog(@"I'm %@, version: %@", ctn, dict[@"v"])
};
return block;
}
+ (void)execBlock:(JSBlock)blk
{
}
@end
// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"}); //output: I'm JSPatch, version: 0.0.1
若要把这个从 OC 传过来的 block 再传回给 OC,同样需要再用 block() 包装,因为这里 blk 已经是一个普通的 JS function,跟我们上面定义的 JS function 没有区别:
// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"}); //output: I'm JSPatch, version: 0.0.1
require('JPObject').execBlock(block("id", blk));
总结:JS 没有 block 类型的变量,OC 的 block 对象传到 JS 会变成 JS function,所有要从 JS 传 block 给 OC 都需要用 block() 接口包装。
block 里使用 self 变量
在 block 里无法使用 self 变量,需要在进入 block 之前使用临时变量保存它:
defineClass("JPViewController", {
viewDidLoad: function() {
var slf = self;
require("JPTestObject").callBlock(block(function(){
//`self` is not available here, use `slf` instead.
slf.doSomething();
});
}
}
限制
从 JS 传 block 到 OC,有两个限制:
A. block 参数个数最多支持6个。(若需要支持更多,可以修改源码)
B. block 参数类型不能是 double。
另外不支持 JS 封装的 block 传到 OC 再传回 JS 去调用(原因见 issue #155):
- (void)callBlock:(void(^)(NSString *str))block {
}
defineClass('JPTestObject', {
run: function() {
self.callBlock(block('NSString*', function(str) {
console.log(str);
}));
},
callBlock: function(blk) {
//blk 这个 block 是上面的 run 函数里 JS 传到 OC 再传过来的,无法调用。
blk("test block");
}
});
8. 加载动态库
对于 iOS 内置的动态库,若原 APP 里没有加载,可以通过以下方式动态加载,以加载 SafariServices.framework 为例:
var bundle = NSBundle.bundleWithPath("/System/Library/Frameworks/SafariServices.framework");
bundle.load();
加载后就可以使用 SafariServices.framework 了。
9. 调试
可以使用 console.log() 打印一个对象,作用相当于 NSLog(),会直接在 XCode 控制台打出。
console.log() 支持任意参数,但不支持像 NSLog 这样 NSLog(@"num:%f", 1.0) 的拼接:
var view = UIView.alloc().init();
var str = "test";
var num = 1;
console.log(view, str, num)
console.log(str + num); //直接在JS拼接字符串
也可以通过 Safari 的调试工具对 JS 进行断点调试,详见 JS 断点调试
4.JPSDK集成相关API
// 若需要替换自己的打印格式
[JSPatch setLogger:^(NSString *msg) {
// msg 是 JSPatch log 字符串,用你自定义的logger打出
YOUR_APP_LOG(@"%@", msg);
}];
/*
发布前测试脚本 +testScriptInBundle 会寻找main.js执行
不能同时调用 +startWithAppKey: 方法
*/
[JSPatch testScriptInBundle];
/**
JSPatch 执行过程中的事件回调
//执行脚本 //脚本有更新 //已拉取新脚本 //条件下发 //灰度下发
*/
[JSPatch setupCallback:^(JPCallbackType type, NSDictionary *data, NSError *error) {
switch (type) {
case JPCallbackTypeUpdate: {
NSLog(@"updated %@ %@", data, error);
break;
}
case JPCallbackTypeRunScript: {
NSLog(@"run script %@ %@", data, error);
break;
}
default:
break;
}
}];
/**
定义用户属性 控制条件下发 必须在sync前调用
*/
[JSPatch setupUserData:@{
@"userId": @"100867",
@"location": @"guangdong"
}];
/**
自定义 RSA key,在 +sync: 之前调用,
*/
[JSPatch setupRSAPublicKey:];
/**
发布时 选择开发预览 只会对这部分设备有效
*/
#ifdef DEBUG
[JSPatch setupDevelopment];
#endif
// 检查更新 若频率高可在-applicationDidBecomeActive:
[JSPatch sync];
5.简单的替换 新增测试
// 覆盖viewDidLoad
defineClass('RootViewController', {
viewDidLoad: function(){
// ORIG 即可调用未覆盖前的 OC 原方法:
// self.ORIGviewDidLoad();
self.super().viewDidLoad();
console.log("JS---viewDidLoad---viewDidLoad");
// 要使用一个类前
require('NSUserDefaults');
// 或者是直接使用 var firstLoad = require('NSUserDefaults').standardUserDefaults();
var firstLoad = NSUserDefaults.standardUserDefaults();
require('NSString');
var saveVersion = NSString.alloc().init();
saveVersion = firstLoad.objectForKey("YENT_Version_Flag");
require('HYUtil');
var currentVersion = HYUtil.getCurrentVersion();
self.labelTitle().setText("首页");
self.setRequestUrl(HYUtil.getServerURL("/main.html#example/index/index"
));
self.tabbar().setSelectedIndex(0);
self.loadRequest();
require('HYString');
if(HYString.isValid(saveVersion))
{
//非第一次安装
var iSaveVersion = saveVersion.stringByReplacingOccurrencesOfString_withString(".","").integerValue();
var iCurrentVersion = currentVersion.stringByReplacingOccurrencesOfString_withString(".","").integerValue();
//判断版本是否更新
if (iCurrentVersion>iSaveVersion)
{
firstLoad.setObject_forKey(currentVersion,"YENT_Version_Flag");
firstLoad.synchronize();
self.initGuide();
}
else
{
self.initRoot();
}
}
else
{
//第一次安装
firstLoad.setObject_forKey(currentVersion,"YENT_Version_Flag");
firstLoad.synchronize();
self.initGuide();
}
// 新增测试
self.showMsg();
},
});
// 新增showMsg方法
defineClass('RootViewController', {
showMsg:function(){
require('HYUtil').showAlert_message("JSPatch提示", "JSPatch提示");
},
});