一、前言
一样的开篇问题,为什么要研究这个?iOS为什么要插件化?为什么要借助其他语言比如html5 js甚至脚本lua等来实现原本OC/Swift应该实现的东西?
原因可以归结为两点:
1. iOS平台 appstore 审核速度不可控,而很多活动页面需要频繁更新,如果每次更新都走appstore审核流程,那活动也就不要做了。
2. 可多平台复用代码,节省开发成本。比如同一个活动的页面,用html5+js完成,就可以通用的在iOS Android平台上,而只需要维护一份html5+js代码。
现如今国内各大互联网公司的iOS端产品,绝大多是都有使用这种技术,特别是html5+js。而使用脚本语言来做动态更新的app也不在少数。
本文先讨论使用html5 + js来插件化的技术。
请浏览一篇文章来脑补一下,我们要做啥 分析支付宝客户端的插件机制
当然,现在的支付宝版本已经告别了这种显性插件化的机制。后面会具体说。
另外还要补充一个前提,我们绝对不做纯html5+js的app,因为稍复杂的app,使用纯html5的方式,只会给自己挖坑,现阶段,native+部分简单逻辑的html5才是真正切合实际的方案。这个问题不展开讨论了。
二、应该准备点什么?
首先我们得准备点东西,当然你要熟悉OC语言(swift亦可),然后你要了解html语言,能写几句js。
然后我们绝不用历史上”著名”的PhoneGap来做,因为它真的很弱。也不能简单的使用UIWebViewDelegate的一个方法来做简单的js 和 OC的通信,因为那是远远不够的。
我们要使用的是很早就出现并广泛运用在mac平台,但直到iOS7才进入移动平台的JavaScriptCore。这真的是iOS7开始原生提供的,真的不是私有的,真的你随便用。
三、JavaScriptCore基础知识
3.1 JavaScriptCore是什么?
JavaScriptCore框架是基于webkit中以C/C++实现的JavaScriptCore的一个包装,之前广泛应用于mac平台,从iOS7开始,apple主动将其加入到iOS SDK中。JavaScriptCore让Objective-C和JavaScript代码的交互变得更加简单和直接。
JavaScriptCore中有几个重要的东西:
1
2
3
4
5
6
|
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
|
他们都是日常使用中经常用到的东西。后面会结合实例,介绍他们都是干嘛的。
3.2 iOS如何使用JavaScriptCore?
在需要的地方,引入:
1
2
|
#import JavaScriptCore/JavaScriptCore.h
|
3.3 JavaScriptCore能用来做什么?
3.3.1 通过OC执行JS方法或调取JS属性。
比如下面的一个例子:
1
2
3
4
5
6
7
|
JSContext
*context
=
[
[
JSContext
alloc
]
init
]
;
[
context
evaluateScript
:
@"var arr = [1, 2, 'This is js string'];var sum = function(a, b) { return a+b;}"
]
;
JSValue
*jsArray
=
context
[
@"arr"
]
;
JSValue
*jsSum
=
context
[
@"sum"
]
;
JSValue
*jsSumResult
=
[
jsSum
callWithArguments
:
[
NSArray
arrayWithObjects
:
@
12
,
@
33
,
nil
]
]
;
|
JSContext对象是JS的运行环境,通过 -evaluateScript 方法可以运行一段javaScript。javaScript的所有变量方法都会在JSContext对象中妥善的保存。通过对JSContext对象的一些操作,可以调用javaScript的方法,或者存取javaScript的对象。
之前见到的JSVirtualMachine顾名思义,是javaScript的虚拟机,是为JSContext提供运行资源。JSVirtualMachine的具体使用在后面也会讲到。
但是我们的JSContext *context 是通过 init方法生成的啊,看似并没有搀和到JSVirtualMachine啊?但是其实,通过init方法生成的JSContext对象,在init方法内部,仍然会自动线生成一个JSVirtualMachine,然后调用JSContext 对象的 -initWithVirtualMachine 方法。故,一个JSContext对象,必定要对应着一个JSVirtualMachine对象。
同一个JSVirtualMachine中的若干个JSContext可以互相交换方法对象等等,但是不同的JSVirtualMachine不能互相交换任何JSContext的资源。
下图来自苹果官方,很形象的描述了这一点。
2015-03-11 :(不知为何,苹果删除了官方文档中关于JavaScriptCore的部分。。擦咧。。。)
JSValue是JavaScriptCore中一个重要的类,前面说到,我们使用JSContext和 JSVirtualMachine 开拓了一个运行和保留javaScript的空间,而JSContext中javaScript的各个方法和属性对应着JSValue。
JSValue也是OC 和 javaScript 互相访问和修改的中间体,所有OC 和 javaScript的跨语言操作都要通过JSValue一些方法进行。
比如我们示例代码里的,JSValue *jsArray,jsArray对应着javaScript中的一个 array对象:arr。所以我们可以对jsArray进行一些操作,从而操作javaScript 中的 arr。
例如:
1
2
3
4
5
|
jsArray
[
0
]
;
//1
jsArray
[
2
]
;
//This is js string
jsArray
[
1
]
=
49
;
//修改arr 的第二个元素。
jsArray
[
@"length"
]
;
//结果是3,调用js arr对象的方法。
|
又比如我们示例代码里的,jsSum,对应着sum function,因此我们可以通过操作jsSum从而调取javaScript中的 sum function。
1
2
3
|
JSValue
*jsSumResult
=
[
jsSum
callWithArguments
:
[
NSArray
arrayWithObjects
:
@
12
,
@
33
,
nil
]
]
;
//jsSumResult = 45;
|
可见,我们可以方便的通过JSValue对象的 callWithArguments:方法来直接调取 js 的 function。js function的多参数,在OC中,由NSArray组装而成。
3.3.2 通过JS执行OC方法或调取OC属性。
苹果介绍,有两种方式可以方便的通过js 调用 OC:
- Block 用来调用方法。
- JSExport protocol 用来调用对象。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//我们有一个OC方法,提供给js调用
-
(
NSInteger
)
sumWithA
:
(
NSInteger
)
a
B
:
(
NSInteger
)
b
C
:
(
NSInteger
)
c
{
return
a
+
b
+
c
;
}
-
(
void
)
jsToOcFunction
{
JSContext
*context
=
[
[
JSContext
alloc
]
init
]
;
context
[
@"sumNums"
]
=
^
(
NSInteger
a
,
NSInteger
b
,
NSInteger
c
)
{
return
[
self
sumWithA
:a
B
:b
C
:c
]
;
}
;
JSValue
*sum
=
[
context
evaluateScript
:
@"sumNums(7, 56, 22)"
]
;
NSLog
(
@"sum %@"
,
sum
)
;
//sum 85
}
|
在例子中,我们定义了一个OC方法:sumWithA:(NSInteger)a B:(NSInteger)b C:(NSInteger)c,提供给js调取使用。
在调取时,我们首先声明一个JSContext,然后对context 的sumNums 赋予了一个OC的Block,Block内执行了我们之前提供的OC方法。
然后我们通过context的 -evaluateScript方法执行了一个js方法,尝试调用OC函数。最终看到了调取成功的结果。
我们再举一个例子,来说明JSContext 在js 调用 OC方法中的重要性。
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
|
//尝试用OC来实现一个JavaScriptCore所不具有的Log
JSContext
*context
=
[
[
JSContext
alloc
]
init
]
;
context
[
@"log"
]
=
^
(
)
{
NSLog
(
@"+++++++Begin Log+++++++"
)
;
NSArray
*args
=
[
JSContext
currentArguments
]
;
for
(
JSValue
*jsVal
in
args
)
{
NSLog
(
@"%@"
,
jsVal
)
;
}
JSValue
*this
=
[
JSContext
currentThis
]
;
NSLog
(
@"this: %@"
,
this
)
;
NSLog
(
@"-------End Log-------"
)
;
}
;
[
context
evaluateScript
:
@"log('ider', [7, 21], { hello:'world', js:100 });"
]
;
//Output:
// +++++++Begin Log+++++++
// ider
// 7,21
// [object Object]
// this: [object GlobalObject]
// -------End Log-------
|
这个经典的例子来自于 Ider的blog
例子中有两个关键点:
- [JSContext currentArguments] 类方法可以拿到js函数中的所有参数列表。每个参数在OC中也都用JSValue 描述。
- [JSContext currentThis] 类方法可以拿到调用该方法的对象。
需要特别注意的是:
1. 不论在任何情况下,不要在Block中直接使用外面的JSValue对象, 而应该把JSValue当做参数来传进Block中。
2. 不论在任何情况下,不要在Block中直接使用外面的JSContext对象, 而应该使用 [JSContext currentContext]获取。
上面我们看到了js调取OC方法的例子,下面我们看js通过JSExport protocol调取OC属性的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//使用rumtime 为一个系统控件UIButton增加JSExport protocol
@protocol
UIButtonExport
&
lt
;
JSExport
&
gt
;
-
(
void
)
setTitle
:
(
NSString
*
)
title
forState
:
(
UIControlState
)
state
;
@end
-
(
void
)
changeTitle
{
class_addProtocol
(
[
UIButton
class
]
,
@protocol
(
UIButtonExport
)
)
;
UIButton
*button
=
[
UIButton
buttonWithType
:UIButtonTypeSystem
]
;
[
button
setTitle
:
@"你好 OC"
forState
:UIControlStateNormal
]
;
button
.
frame
=
CGRectMake
(
100
,
100
,
100
,
100
)
;
[
self
.
view
addSubview
:button
]
;
JSContext
*context
=
[
[
JSContext
alloc
]
init
]
;
context
[
@"button"
]
=
button
;
[
context
evaluateScript
:
@"button.setTitleForState('你好 js', 0)"
]
;
}
|
而除了上述通过runtime的方式增加JSExport protocol之外,还可以通过category的方式,比如:
1
2
3
4
5
6
7
8
9
10
11
|
//UIButton+js.h
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol
UIButtonExport
&
lt
;
JSExport
&
gt
;
-
(
void
)
setTitle
:
(
NSString
*
)
title
forState
:
(
UIControlState
)
state
;
@end
@interface
UIButton
(
js
)
&
lt
;
UIButtonExport
&
gt
;
@end
|
可以看到,如果想要在js中调用OC 的类或者对象的方法,需要将方法在JSExport protocol中声明。
下面再举一个更复杂的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//book.js
//列出书的title
var
bookTitleList
=
function
(
book1
,
book2
)
{
var
book1Title
=
book1
.
title
;
var
book2Title
=
book2
.
title
;
return
'list:'
+
book1Title
+
book2Title
;
}
;
//创建两本书的合订本
var
makeBookFromTwoBooks
=
function
(
book1
,
book2
)
{
var
title
=
book1
.
title
+
book2
.
title
;
var
page
=
book1
.
page
+
book2
.
page
;
return
Book
.
makeBookWithTitlePage
(
title
,
page
)
;
}
;
|
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
|
//
// Book.h
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@class
Book
;
@protocol
BookExports
&
lt
;
JSExport
&
gt
;
@property
NSString
*title
;
@property
NSInteger
page
;
+
(
Book
*
)
makeBookWithTitle
:
(
NSString
*
)
title
page
:
(
NSInteger
)
page
;
@end
@interface
Book
: NSObject
&
lt
;
BookExports
&
gt
;
@property
NSString
*title
;
@property
NSInteger
page
;
+
(
Book
*
)
makeBookWithTitle
:
(
NSString
*
)
title
page
:
(
NSInteger
)
page
;
@end
//
// Book.m
#import "Book.h"
@implementation
Book
+
(
Book
*
)
makeBookWithTitle
:
(
NSString
*
)
title
page
:
(
NSInteger
)
page
{
Book
*book
=
[
[
Book
alloc
]
init
]
;
book
.
title
=
title
;
book
.
page
=
page
;
return
book
;
}
@end
|
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
|
-
(
void
)
moreExportsTest
{
JSContext
*context
=
[
[
JSContext
alloc
]
init
]
;
NSString
*
path
=
[
[
NSBundle
mainBundle
]
pathForResource
:
@"book"
ofType
:
@"js"
]
;
NSString
*jsString
=
[
NSString
stringWithContentsOfFile
:path
encoding
:NSStringEncodingConversionAllowLossy
error
:nil
]
;
[
context
evaluateScript
:jsString
]
;
Book
*book1
=
[
Book
makeBookWithTitle
:
@"资治通鉴第一部"
page
:
330
]
;
Book
*book2
=
[
Book
makeBookWithTitle
:
@"资治通鉴第二部"
page
:
520
]
;
JSValue
*fucntion
=
context
[
@"bookTitleList"
]
;
JSValue
*result
=
[
fucntion
callWithArguments
:
@
[
book1
,
book2
]
]
;
NSLog
(
@"result %@"
,
result
.
toString
)
;
//result list:资治通鉴第一部资治通鉴第二部
context
[
@"Book"
]
=
[
Book
class
]
;
JSValue
*function
=
context
[
@"makeBookFromTwoBooks"
]
;
JSValue
*jsResult
=
[
function
callWithArguments
:
@
[
book1
,
book2
]
]
;
Book
*newBook
=
[
jsResult
toObject
]
;
NSLog
(
@"newBook title %@ page: %ld"
,
newBook
.
title
,
(
long
)
newBook
.
page
)
;
//newBook title 资治通鉴第一部资治通鉴第二部 page: 850
}
|
第一段js,是我们的javascript文件。我们在其中定义了两个方法。
第二段,是我们定义了一个Book类,可以看到Book对象有两个属性:title 和 page,有一个类方法:+ (Book *)makeBookWithTitle:(NSString *)title page:(NSInteger)page,用来生成新的Book。这个方法将在js中被调用。
然后我们声明了,@protocol BookExports ,将需要在js中直接访问的属性和方法放置进去。
最后,让我们的Book类遵循BookExports protocol(@interface Book : NSObject )。
这样一个可在js中直接访问的类就生成完毕了。
最后一段是我们的运行代码。
首先将一段js代码加载斤JSContext中并执行。之后声明两个Book对象,book1 book2。将其作为参数传入js函数bookTitleList中。可以看到执行结果与预期相同。在js中,我们直接book1.title取出了OC对象的属性。
紧接着,我们执行了第二个js函数,在其中,用js调用了OC的Book的类方法,创建出了一个新的OC 的Book对象并返回。