Fork me on GitHub

JSPatch简单分析1

说在前面

JSPatch 是一个基于 JavaScriptCore 的开源项目,只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 实时修复线上 bug。

JavaScriptCore初探

注:JavaScriptCore API也可以用Swift来调用,本文用Objective-C来介绍。

  • 在iOS7之前,原生应用和Web应用之间很难通信。如果你想在iOS设备上渲染HTML或者运行JavaScript,你不得不使用UIWebView。iOS7引入了JavaScriptCore,功能更强大,使用更简单。

  • JavaScriptCore是封装了JavaScript和Objective-C桥接的Objective-C API,只要用很少的代码,就可以做到JavaScript调用Objective-C,或者Objective-C调用JavaScript。

  • 在之前的iOS版本,你只能通过向UIWebView发送stringByEvaluatingJavaScriptFromString:消息来执行一段JavaScript脚本。并且如果想用JavaScript调用Objective-C,必须打开一个自定义的URL(例如:foo://),然后在UIWebView的delegate方法webView:shouldStartLoadWithRequest:navigationType中进行处理。

  • 然而现在可以利用JavaScriptCore的先进功能了,它可以:

    • 运行JavaScript脚本而不需要依赖UIWebView
    • 使用现代Objective-C的语法(例如Blocks和下标)
    • 在Objective-C和JavaScript之间无缝的传递值或者对象
    • 创建混合对象(原生对象可以将JavaScript值或函数作为一个属性)

使用 JavaScriptCore 做 JavaScript 和 OC 之间的相互调用的2个小例子

Objective-C调用JavaScript

1
1
2
3
4
JSContext *context = [[JSContext alloc] init];
NSString *str = @"(1*(2-2)+1)*5*8-2+9";
JSValue *value = [context evaluateScript:str];
NSLog(@"%@ = %@", str,value.toNumber);
2
1
2
3
4
5
6
JSContext *context = [[JSContext alloc] init];
NSString *jsfunc = @"function fac(n){ return (n * 2);}";
[context evaluateScript:jsfunc];
JSValue *func = context[@"fac"];
JSValue *value1 = [func callWithArguments:@[@100]];
NSLog(@"%@",value1.toNumber);

JavaScript调用Objective-C

1
2
3
4
5
6
7
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"function printHello() {print(\"Hello, World! 我在 JS 中 来的\");}"];
context[@"print"] = ^(NSString *text) {
NSLog(@"%@", text);
};
JSValue *function = context[@"printHello"];
[function callWithArguments:@[]];

知道了 JavaScriptCore 的强大之处,又由于 JavaScript 是脚本语言同时 OC 的动态语言,所以 JSPatch 就诞生了。

原理分析

  • 我们从 JSPatch 例子开始看,比如:我们使用了下面的 js 来动态创建一个view
1
2
3
4
require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)
  • 当我们在 oc 中执行上面的 js 时,
  • require(‘UIView’) 是调用 js 中的一个方法
1
2
3
4
5
6
7
8
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}
  • var view = UIView.alloc().init() 表示使用 UIView 调用 alloc init 方法,那么我们的 js 中其实是没有 这2 个方法的,但如果没有的话会报错,开发者开始想的办法动态给 js 添加,因为 js 可以随时随地增加方法,但比较痛苦。因为这些方法其实是来自于 oc 中 需要我们去遍历 oc 。由于 js 没有方法转发的功能,最后用了一个精巧的解决这个问题,在每一个 js 发放调用前先调一个 js 已经实现好的方法 __c(),其实现如下图。在这个js方法中做处理,同时想办法回调到 oc 中做应该做的事。使用上面的 js 就会最后回调到吗oc 中创建一个 view 设置颜色等。
  • 动添加 __c 的调用如下:
1
NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();",
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
;(function(){try{
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
}
})

defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {
dataSource: function() {
var data = self.__c("data")();
if (data) return data;
var data = [];
for (var i = 0; i < 20; i ++) {
data.__c("push")("cell from js " + i);
}
self.__c("setData")(data)
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.__c("dataSource")().length;
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.__c("dequeueReusableCellWithIdentifier")("cell")
if (!cell) {
cell = require('UITableViewCell').__c("alloc")().__c("initWithStyle_reuseIdentifier")(0, "cell")
}
cell.__c("textLabel")().__c("setText")(self.__c("dataSource")()[indexPath.__c("row")()])
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")("Alert",self.__c("dataSource")()[indexPath.__c("row")()], self, "OK", null);
alertView.__c("show")()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.__c("log")('click btn ' + alertView.__c("buttonTitleAtIndex")(idx).__c("toJS")())
}
})
}catch(e){_OC_catch(e.message, e.stack)}})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _customMethods = {
__c: function(methodName) {
var slf = this

if (slf instanceof Boolean) {
return function() {
return false
}
}
if (slf[methodName]) {
return slf[methodName].bind(slf);
}

if (!slf.__obj && !slf.__clsName) {
throw new Error(slf + '.' + methodName + ' is undefined')
}
  • JSPatch 的最基本的操作底层原理就是依靠 js 和 oc 的相互调用来实现的,同时 JavaScriptCore 可以做到 oc 对象 在 oc 和 js 组件无缝传递。
  • 下面简单说下 JSPatch 是怎么样做到动态添加方法 和 交换方法的。我们使用 runtime 来添加方法 一般是提前已经有了 C函数的实现,我们在添加时,让 IMP 指向我们的 c函数指针即可,但是 JSPatch 是完全动态的添加任何类型的方法,怎么做到呢?? 肯定不可能我们提前就实现好了 c函数,这里开发者是使用了一个通用的 c函数来实现这个问题,就是 所有添加的方法 其实都是执行到了写死的 c函数中,c函数如下,当然做了各种的坑处理,包括各种参数匹配,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{

#ifdef DEBUG
_JSLastCallStack = [NSThread callStackSymbols];
#endif
BOOL deallocFlag = NO;
id slf = assignSlf;
BOOL isBlock = [[assignSlf class] isSubclassOfClass : NSClassFromString(@"NSBlock")];

NSMethodSignature *methodSignature = [invocation methodSignature];
NSInteger numberOfArguments = [methodSignature numberOfArguments];
NSString *selectorName = isBlock ? @"" : NSStringFromSelector(invocation.selector);
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
JSValue *jsFunc = isBlock ? objc_getAssociatedObject(assignSlf, "_JSValue")[@"cb"] : getJSFunctionInObjectHierachy(slf, JPSelectorName);
if (!jsFunc) {
JPExecuteORIGForwardInvocation(slf, selector, invocation);
return;
}

NSMutableArray *argList = [[NSMutableArray alloc] init];
if (!isBlock) {
if ([slf class] == slf) {
[argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
} else if ([selectorName isEqualToString:@"dealloc"]) {
[argList addObject:[JPBoxing boxAssignObj:slf]];
deallocFlag = YES;
} else {
[argList addObject:[JPBoxing boxWeakObj:slf]];
}
}

for (NSUInteger i = isBlock ? 1 : 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch(argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {

#define JP_FWD_ARG_CASE(_typeChar, _type) \
case _typeChar: { \
_type arg; \
[invocation getArgument:&arg atIndex:i]; \
[argList addObject:@(arg)]; \
break; \
}
JP_FWD_ARG_CASE('c', char)
JP_FWD_ARG_CASE('C', unsigned char)
JP_FWD_ARG_CASE('s', short)
JP_FWD_ARG_CASE('S', unsigned short)
JP_FWD_ARG_CASE('i', int)
JP_FWD_ARG_CASE('I', unsigned int)
JP_FWD_ARG_CASE('l', long)
JP_FWD_ARG_CASE('L', unsigned long)
JP_FWD_ARG_CASE('q', long long)
JP_FWD_ARG_CASE('Q', unsigned long long)
JP_FWD_ARG_CASE('f', float)
JP_FWD_ARG_CASE('d', double)
JP_FWD_ARG_CASE('B', BOOL)
case '@': {
__unsafe_unretained id arg;
[invocation getArgument:&arg atIndex:i];
if ([arg isKindOfClass:NSClassFromString(@"NSBlock")]) {
[argList addObject:(arg ? [arg copy]: _nilObj)];
} else {
[argList addObject:(arg ? arg: _nilObj)];
}
break;
}
case '{': {
NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
#define JP_FWD_ARG_STRUCT(_type, _transFunc) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
_type arg; \
[invocation getArgument:&arg atIndex:i]; \
[argList addObject:[JSValue _transFunc:arg inContext:_context]]; \
break; \
}
JP_FWD_ARG_STRUCT(CGRect, valueWithRect)
JP_FWD_ARG_STRUCT(CGPoint, valueWithPoint)
JP_FWD_ARG_STRUCT(CGSize, valueWithSize)
JP_FWD_ARG_STRUCT(NSRange, valueWithRange)

@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
if (size) {
void *ret = malloc(size);
[invocation getArgument:ret atIndex:i];
NSDictionary *dict = getDictOfStruct(ret, structDefine);
[argList addObject:[JSValue valueWithObject:dict inContext:_context]];
free(ret);
break;
}
}
}

break;
}
case ':': {
SEL selector;
[invocation getArgument:&selector atIndex:i];
NSString *selectorName = NSStringFromSelector(selector);
[argList addObject:(selectorName ? selectorName: _nilObj)];
break;
}
case '^':
case '*': {
void *arg;
[invocation getArgument:&arg atIndex:i];
[argList addObject:[JPBoxing boxPointer:arg]];
break;
}
case '#': {
Class arg;
[invocation getArgument:&arg atIndex:i];
[argList addObject:[JPBoxing boxClass:arg]];
break;
}
default: {
NSLog(@"error type %s", argumentType);
break;
}
}
}

if (_currInvokeSuperClsName[selectorName]) {
Class cls = NSClassFromString(_currInvokeSuperClsName[selectorName]);
NSString *tmpSelectorName = [[selectorName stringByReplacingOccurrencesOfString:@"_JPSUPER_" withString:@"_JP"] stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"_JP"];
if (!_JSOverideMethods[cls][tmpSelectorName]) {
NSString *ORIGSelectorName = [selectorName stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"ORIG"];
[argList removeObjectAtIndex:0];
id retObj = callSelector(_currInvokeSuperClsName[selectorName], ORIGSelectorName, [JSValue valueWithObject:argList inContext:_context], [JSValue valueWithObject:@{@"__obj": slf, @"__realClsName": @""} inContext:_context], NO);
id __autoreleasing ret = formatJSToOC([JSValue valueWithObject:retObj inContext:_context]);
[invocation setReturnValue:&ret];
return;
}
}

NSArray *params = _formatOCToJSList(argList);
char returnType[255];
strcpy(returnType, [methodSignature methodReturnType]);

// Restore the return type
if (strcmp(returnType, @encode(JPDouble)) == 0) {
strcpy(returnType, @encode(double));
}
if (strcmp(returnType, @encode(JPFloat)) == 0) {
strcpy(returnType, @encode(float));
}

switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
#define JP_FWD_RET_CALL_JS \
JSValue *jsval; \
[_JSMethodForwardCallLock lock]; \
jsval = [jsFunc callWithArguments:params]; \
[_JSMethodForwardCallLock unlock]; \
while (![jsval isNull] && ![jsval isUndefined] && [jsval hasProperty:@"__isPerformInOC"]) { \
NSArray *args = nil; \
JSValue *cb = jsval[@"cb"]; \
if ([jsval hasProperty:@"sel"]) { \
id callRet = callSelector(![jsval[@"clsName"] isUndefined] ? [jsval[@"clsName"] toString] : nil, [jsval[@"sel"] toString], jsval[@"args"], ![jsval[@"obj"] isUndefined] ? jsval[@"obj"] : nil, NO); \
args = @[[_context[@"_formatOCToJS"] callWithArguments:callRet ? @[callRet] : _formatOCToJSList(@[_nilObj])]]; \
} \
[_JSMethodForwardCallLock lock]; \
jsval = [cb callWithArguments:args]; \
[_JSMethodForwardCallLock unlock]; \
}

#define JP_FWD_RET_CASE_RET(_typeChar, _type, _retCode) \
case _typeChar : { \
JP_FWD_RET_CALL_JS \
_retCode \
[invocation setReturnValue:&ret];\
break; \
}

#define JP_FWD_RET_CASE(_typeChar, _type, _typeSelector) \
JP_FWD_RET_CASE_RET(_typeChar, _type, _type ret = [[jsval toObject] _typeSelector];) \

#define JP_FWD_RET_CODE_ID \
id __autoreleasing ret = formatJSToOC(jsval); \
if (ret == _nilObj || \
([ret isKindOfClass:[NSNumber class]] && strcmp([ret objCType], "c") == 0 && ![ret boolValue])) ret = nil; \

#define JP_FWD_RET_CODE_POINTER \
void *ret; \
id obj = formatJSToOC(jsval); \
if ([obj isKindOfClass:[JPBoxing class]]) { \
ret = [((JPBoxing *)obj) unboxPointer]; \
}

#define JP_FWD_RET_CODE_CLASS \
Class ret; \
ret = formatJSToOC(jsval);


#define JP_FWD_RET_CODE_SEL \
SEL ret; \
id obj = formatJSToOC(jsval); \
if ([obj isKindOfClass:[NSString class]]) { \
ret = NSSelectorFromString(obj); \
}

JP_FWD_RET_CASE_RET('@', id, JP_FWD_RET_CODE_ID)
JP_FWD_RET_CASE_RET('^', void*, JP_FWD_RET_CODE_POINTER)
JP_FWD_RET_CASE_RET('*', void*, JP_FWD_RET_CODE_POINTER)
JP_FWD_RET_CASE_RET('#', Class, JP_FWD_RET_CODE_CLASS)
JP_FWD_RET_CASE_RET(':', SEL, JP_FWD_RET_CODE_SEL)

JP_FWD_RET_CASE('c', char, charValue)
JP_FWD_RET_CASE('C', unsigned char, unsignedCharValue)
JP_FWD_RET_CASE('s', short, shortValue)
JP_FWD_RET_CASE('S', unsigned short, unsignedShortValue)
JP_FWD_RET_CASE('i', int, intValue)
JP_FWD_RET_CASE('I', unsigned int, unsignedIntValue)
JP_FWD_RET_CASE('l', long, longValue)
JP_FWD_RET_CASE('L', unsigned long, unsignedLongValue)
JP_FWD_RET_CASE('q', long long, longLongValue)
JP_FWD_RET_CASE('Q', unsigned long long, unsignedLongLongValue)
JP_FWD_RET_CASE('f', float, floatValue)
JP_FWD_RET_CASE('d', double, doubleValue)
JP_FWD_RET_CASE('B', BOOL, boolValue)

case 'v': {
JP_FWD_RET_CALL_JS
break;
}

case '{': {
NSString *typeString = extractStructName([NSString stringWithUTF8String:returnType]);
#define JP_FWD_RET_STRUCT(_type, _funcSuffix) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
JP_FWD_RET_CALL_JS \
_type ret = [jsval _funcSuffix]; \
[invocation setReturnValue:&ret];\
break; \
}
JP_FWD_RET_STRUCT(CGRect, toRect)
JP_FWD_RET_STRUCT(CGPoint, toPoint)
JP_FWD_RET_STRUCT(CGSize, toSize)
JP_FWD_RET_STRUCT(NSRange, toRange)

@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
JP_FWD_RET_CALL_JS
void *ret = malloc(size);
NSDictionary *dict = formatJSToOC(jsval);
getStructDataWithDict(ret, dict, structDefine);
[invocation setReturnValue:ret];
free(ret);
}
}
break;
}
default: {
break;
}
}

if (_pointersToRelease) {
for (NSValue *val in _pointersToRelease) {
void *pointer = NULL;
[val getValue:&pointer];
CFRelease(pointer);
}
_pointersToRelease = nil;
}

if (deallocFlag) {
slf = nil;
Class instClass = object_getClass(assignSlf);
Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));
}
}

平台使用

  • 平台使用说明
  • 如果使用其平台来做还是算简单的,基本不用做其他配置。

集成 SDK

一般情况的SDK的使用完全按照文档基本没有问题

  • 在平台创建 APP
  • 下载SDK集成到项目中
  • 已经集成完毕
  • 在需要修复 bug 时,先写好 js 脚本
  • 本地测试
  • 测试成功后,去平台下发即可。

平台 SDK 使用注意点:

  • 补丁累计不要超过 1 个
  • 每一次 App 更新必须使用原生来处理掉所有的补丁,同时需要清除相应的补丁下载。
  • 每一次下发补丁需提本地测试

自建平台

拷贝下官方的一个 DEMO

  • 有一个最简单的控制器上有一个按钮,现在想加点功能,就是在点击这个按钮时跳转到一个新界面,新的界面上有一个 tableView,同时做相应的简单展示、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "JPViewController.h"

@implementation JPViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 50)];
[btn setTitle:@"Push JPTableViewController" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(handleBtn:) forControlEvents:UIControlEventTouchUpInside];
[btn setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:btn];
}

- (void)handleBtn:(id)sender
{
}

@end
  • 集成项目
1
2
3
4
[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
  • JS 脚本如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
}
})

defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {
dataSource: function() {
var data = self.data();
if (data) return data;
var data = [];
for (var i = 0; i < 20; i ++) {
data.push("cell from js " + i);
}
self.setData(data)
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.dataSource().length;
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if (!cell) {
cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
}
cell.textLabel().setText(self.dataSource()[indexPath.row()])
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource()[indexPath.row()], self, "OK", null);
alertView.show()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.log('click btn ' + alertView.buttonTitleAtIndex(idx).toJS())
}
})

1111.gif

- END -
扫一扫上面的二维码图案,加我微信

文章作者:梁大红

特别声明:若无特殊声明均为原创,转载请注明,侵权请联系

版权声明:署名-非商业性使用-禁止演绎 4.0 国际