文章目录
  1. 1. 前言
  2. 2. Web & Native
  3. 3. 那些混合编程们
    1. 3.1. PhoneGap/Cordova
    2. 3.2. SuperWebView
      1. 3.2.1. SuperWebView中JS与Native通信的实现原理
      2. 3.2.2. 关于SuperWebView所指的混合渲染的说明
    3. 3.3. 5+Runtime & Native.js
      1. 3.3.1. Native.js通信方式实现分析
    4. 3.4. Titanium
    5. 3.5. React Native
      1. 3.5.1. React Native通信机制源码分析
      2. 3.5.2. 性能测试
    6. 3.6. samurai-native

本文主要介绍笔者前些日子对市面上一些移动端混合编程方案的实现方式上的调研,读者可以据此了解移动端目前常见的一些混合开发框架的实现原理,欢迎读者在本文基础上做更深入的探索和调研,如有纰漏,欢迎指正。

本文示例代码链接: https://github.com/jackwee/hybrid-demos

前言

  • 2008年,HTML5发布首个版本。

  • 2011年,Facebook开展Spartan项目,企图用HTML5的思想武装自己,占领iOS的浏览器,以此来与苹果抗衡。因为浏览器是相对开放的,Apple总不可能在浏览器端设置一个开关对网页内容进行限制。

  • 2012年,Mark Zuckerberg:”the biggest mistake that we made as a company is betting too much on HTML5 as opposed to native.”Facebook放弃HTML5,转投Native。

  • 2015年,Facebook发布react-native。

如今,混合开发的呼声越来越高,世面上也有很多混合开发框架,采用混合开发的应用也是越来越多。

以下将从什么是混合开发、为什么要做混合开发、移动端都在怎么做混合开发来做逐步介绍。


What?

两种或两种以上的程序设计语言的邂逅。

  • JNI:Java与C/C++的混合开发
  • iOS中Objective-C与C、Swift与C的混合开发
  • ReactNative中JavaScript与Native语言的混合开发

本文介绍和调研的主要是移动平台上WEB(WebView、JavaScript)与Native语言的混合开发。

Why?

Web & Native

对比 Web Native
优势 跨平台、开发效率高、方便调试、方便部署 性能体验好、访问Low API、强大的IDE、原生的动画、系统手势
存在问题 性能问题、浏览器兼容性问题、访问Low API受限、需要模拟原生动画和手势 平台依赖性强、调试不便、应用更新周期长、开发效率低

混合编程就是为了将两者的优点结合起来,做到“兼得🐟和🐻👐”。

How?

那些混合编程们

目前移动端主流的混合开发有两个流派,一个是基于WebView做混合开发的Hybrid流派,一个是基于虚拟机做混合开发的’JavaSciptBridge’流派。

  1. WebViewBridge流派(Hybrid流派):基于WebView做与Native语言的混合开发
  2. ‘JavaScipt Bridge’流派(JSBinding,LuaBinding,自定义消息传递):基于脚本语言本身做与Native语言的混合开发

另外,还有翻译流派,如J2ObjC将Java语言翻译成Objective-C,编译流派,如Xamarin直接C#编译为二进制文件来开发iOS应用,这两种做法相对比较小众,本文并不做探讨。

PhoneGap/Cordova

PhoneGap: "Write Once, Run Anywhere"。

目标是让使用者编写一套代码来实现跨平台操作,它是Hybrid技术的一种实现,赋予WebView访问Native API的能力,屏蔽掉平台相关API实现跨平台开发。

上图是Cordova的架构图,其中Web App部分是前端开发部分,在WebView上执行。Cordova Plugins是Cordova所提供的用于访问本地代码的插件,这些插件具有操作本地资源的能力,如定位、陀螺仪、相机、联系人等等,此外,用户也可以自定义插件,js代码部分可以通过Cordova Native APIs来访问这些插件。那么这里对插件的调用实际是怎么实现的?

我们使用一个简单的demo进行测试。

Native端定义了自定义插件MyHybridPlugin,其中定义了addBookmark方法,在html页面上点击按钮,我们通过cordova调用addBookmark。

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
(1)html
//页面上添加点击事件处理
<button id="bookmarkBtn" onclick="app.addBookmark()">Add a bookmark</button>
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/index.js"></script>
(2)JavaScript(index.js)
var app = {
addBookmark: function() {
...
cordova.exec(win, fail, "MyHybridPlugin", "addBookmark", [bookmark]);
}
};
(3)JavaScript(cordova.js)
//添加消息到消息队列
commandQueue.push(JSON.stringify(command));
(4)JavaScript(cordova.js)
//通过iframe通知native
pokeNativeViaIframe();
===>
execIframe = document.createElement('iframe');
execIframe.style.display = 'none';
execIframe.src = 'gap://ready';
document.body.appendChild(execIframe);
(5)Objective-C
//从js消息队列中取消息内容
webView:shouldStartLoadWithRequest:navigationType:
[_commandQueue fetchCommandsFromJs];
(6)JavaScript(cordova.js)
//以json方式返回消息内容,包括callbackId
nativeFetchMessages();
(7)Objective-C
//根据json内容查找对应的class和selector,并执行相应方法
- (BOOL)execute:(CDVInvokedUrlCommand*)command
(8)Objective-C
//回调执行结果,带上callbackId
sendPluginResult:callbackId:
(9)JavaScript(cordova.js)
//根据callbackId回调执行结果
iOSExec.nativeCallback();

通过调试发现,cordova是通过将js对native的方法调用信息,封装成一个UIL,并通过iframe的形式加载该url。native端通过UIWebViewDelegate的代理方法webView:shouldStartLoadWithRequest:navigationType:拦截url,并解析出其中的函数签名信息,通过oc的runtime查找对应的类和方法,实现了本地方法调用,并返回本次调用对应的callbackId,cordova会根据该id查找到对应的js回调方法,实现回调给js。

优点:

  • 跨平台

缺点:

  • 性能问题:从上面的分析可以看出js到native的调用流程比较繁琐
  • 对native工程师的依赖:前端工程师需要native开发来提供所需插件

SuperWebView

SuperWebView:能够帮助原生APP团队解决“如何在短时间内开发出体验好、功能强的HTML5页面”的问题

SuperWebView为web程序员开发App提供一套整体的解决方案,以SDK的方式提供使用(不开源),总结一下主要包括特点:

  • 管理平台提供上百种模块,平台可以根据用户选取组合的模块构建生成SDK,开发者下载使用放到项目中使用
  • 支持用户自定义模块,导出给js使用,需要构建静态库,并打包成zip上传到管理平台,由平台编译生成sdk
  • 利用管理平台,可以进行资源包的热更新
  • 系统API对象提供的JS接口,可以在使用JS进行以下操作:获取系统属性、系统事件、使用进行封装了的接口
  • 提供云API来进行:操作云端数据、统计、推送、短信验证等功能
  • 提供与腾讯X5浏览器的集成(Android)

下面是SuperWebView的架构图:

先看右侧部分,上面是SuperWebView提供的页面组织结构,用户可以利用API对象提供的接口创建界面,用户可以创建window(整个界面),frame(界面中的模块界面),frameGroup(一组可以左右滑动浏览的界面),UIModule(native自定义视图);右侧下面部分是SuperWebView提供的基础服务,并提供了与Native通信的机制;左侧部分是SuperWebView平台已模块的形式提供的可以供JS使用的Native模块单元,官网上有模块管理平台提供了模块可以供开发者使用,开发者也可以根据平台标准自定义模块。

下面是使用SuperWebView实现的模拟网易云音乐的一个demo示例:

主页面的层级结构:

可以看到,应用界面可以由多个APIWebView与UIView(或其子类)组合而来,但是这些组合关系并不需要web端开发者来操心,可以借助平台api对象开放的接口调用,平台接口会构建出相应界面的的组合,例如,可以使用下面这样的方式打开一个可以左右滑动的多个页面组合而成的页面,其构建的界面结果就是UIScrollView中横向放置了四个页面,可以左右滑动翻页浏览。

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
api.openFrameGroup({
name: 'framegroup01',
background: '#FFF',
scrollEnabled: true,
rect: {
x: 0,
y: firstHeaderOffset.h + firstHeaderIndexHeight,
w: "auto",
h: api.frameHeight - firstHeaderOffset.h - firstHeaderIndexHeight - footerPos.h
},
index: 0,
frames: [{
name: 'frame01_recommand',
url: './html/first_frame/frame01_recommand.html',
bounces: false,
}, {
name: 'frame01_list',
url: './html/first_frame/frame01_list.html',
bounces: true,
}, {
name: 'frame01_radio',
url: './html/first_frame/frame01_radio.html',
bounces: false,
}, {
name: 'frame01_rank',
url: './html/first_frame/frame01_rank.html',
bounces: false,
}]
}, function(ret) {
setFrameGroupStatus(ret.index);
});

我们来看看APIWebView的真面目。

1
2
@interface APIWebView : UZWebView
@interface UZWebView : UIWebView

我们看到,APIWebView实际是UIWebView的子类,整个页面结构使用APIWebView嵌套的方式构建。

  • 通过Instruments在iPhone5s上进行内存测试,对前面云音乐demo反复操作,累计创建11个APIWebView,内存开销在35M左右(创建11个UIWebView,并加载本地类似静态页面内存开销在24M左右)。
  • 通过对APIWebView的构造方法initWithFrame:和dealloc方法进行拦截,发现在打开新页面是通过[UZWebViewController loadWindow]和[UZWebViewController openFrame:]创建多个APIWebView,再结合Native控件,构建出页面。

SuperWebView的用户通过对页面结构进行拆分,利用SuperWebView提供的API来构建页面,SuperWebView所提供的API会构建出相应的页面结构,例如上面云音乐demo中的首页面,就是由2个APIWebView和一个UIScrollView嵌套组合而成。至于界面的样式,需要用户用HTML+CSS进行描述。

SuperWebView中JS与Native通信的实现原理

下面分析下SuperWebView的实现JS与Native的通信机制。

测试环境:iPhone5S,iOS8.3和iPhone4S,iOS7.1.1

我们在JS端调用alert("alert from js");接口,利用远程调试进行断点跟踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
//alert的实现
function (msg) {
!function(msg){
if(msg===null){
msg='null'
};
if(msg===undefined){
msg='undefined'
};
msg=msg.toString();
api.alert({title:'index.html',msg:msg,buttons:['好']});
}(msg);
}
1
2
3
4
//api.alert的实现
function () {
return uz$e('UZAPI','alert',arguments,false,'api');
}
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
///uz$e的实现
function uz$e(c, m, p, isSync, module) {
var param = {};
//(1)
if (p.length === 1) {
var p0 = p[0];
//(1.1)
if (Object.prototype.toString.call(p0) === "[object Object]") {
param = p0;
} else if (typeof p0 === "function") {
param.cbId = uz$cb.id++;
uz$cb.fn[param.cbId] = p0;
}
} else if (p.length === 2) {
//(1.2)
var p0 = p[0];
var p1 = p[1];
if (Object.prototype.toString.call(p0) === "[object Object]") {
param = p0;
}
if (typeof p1 === "function") {
param.cbId = uz$cb.id++;
uz$cb.fn[param.cbId] = p1;
}
}
//(2)
if (typeof(_apiBridgeMethod)==='function'){
//(2.1)
return _apiBridgeMethod(c, m, param, isSync, module);
} else if (api.useWKWebView) {
//(2.2)
var message = {};
message.class = c;
message.method = m;
message.param = param;
message.isSync = false;
message.module = module;
window.webkit.messageHandlers.api.postMessage(message);
} else {
//(2.3)
uz$q.c.push(module+'.'+c+'.'+m+'/?'+encodeURIComponent(JSON.stringify(param)));
uz$r();
}
}

对这段代码进行分析:

(1)对参数进行处理

(1.1) 只有一个参数,对调用参数或者回调函数分别处理

(1.2) 有两个参数,对调用参数和回调函数分别处理

(2)执行函数调用

(2.1) 调用_apiBridgeMethod方法,在当前测试环境下,会走2.1的逻辑,其实现如下:

1
2
3
function () {
[native code]
}

可以看到_apiBridgeMethod的方法体是native code,那么是如何将native code注册给WebView的JS解释器的呢?

将APICloud.a中的UZWebView.o反编译出来伪代码看到如下实现:

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
// UZWebView - (void)initJavaScriptCoreBridgeMethod
void __cdecl -[UZWebView initJavaScriptCoreBridgeMethod](struct UZWebView *self, SEL a2)
{
struct UZWebView *v7; // r8@1
void *v8; // r0@1
void *v9; // r5@1
void *v10; // r0@1
void *v11; // r6@1
void *v13; // r0@2
void *v14; // r6@2
void *v15; // r0@2
int v16; // r4@2
void *v17; // r0@2
void *v18; // r5@2
int *v19; // r4@3
int v23; // [sp+0h] [bp-78h]@1
void *v24; // [sp+4h] [bp-74h]@3
void *v25; // [sp+8h] [bp-70h]@3
void *v26; // [sp+Ch] [bp-6Ch]@3
int v27; // [sp+10h] [bp-68h]@3
int v28; // [sp+14h] [bp-64h]@3
int (__fastcall *v29)(__block_literal_3 *, Foundation::NSString::NSString *, Foundation::NSString::NSString *, Foundation::NSDictionary::NSDictionary *, Foundation::NSString::NSString *, int); // [sp+18h] [bp-60h]@3
void *v30; // [sp+1Ch] [bp-5Ch]@3
int v31; // [sp+20h] [bp-58h]@1
char v32; // [sp+24h] [bp-54h]@3
int *v33; // [sp+28h] [bp-50h]@3
char v34; // [sp+2Ch] [bp-4Ch]@1
int v35; // [sp+30h] [bp-48h]@2
void *v36; // [sp+44h] [bp-34h]@1
void *v37; // [sp+48h] [bp-30h]@1
__int64 *v38; // [sp+4Ch] [bp-2Ch]@1
unsigned int v39; // [sp+50h] [bp-28h]@1
int *v40; // [sp+54h] [bp-24h]@1
char v41; // [sp+60h] [bp-18h]@5
__int64 savedregs; // [sp+78h] [bp+0h]@1
_R4 = (unsigned int)&v31 & 0xFFFFFFF0;
__asm
{
VST1.64 {D8-D11}, [R4@128]!
VST1.64 {D12-D15}, [R4@128]
}
v7 = self;
v8 = objc_msgSend(&OBJC_CLASS___UIDevice, "currentDevice");
v9 = (void *)objc_retainAutoreleasedReturnValue(v8);
v10 = objc_msgSend(v9, "systemVersion");
v11 = (void *)objc_retainAutoreleasedReturnValue(v10);
_R4 = objc_msgSend(v11, "floatValue");
objc_release(v11);
objc_release(v9);
v36 = &__objc_personality_v0;
v37 = &GCC_except_table117;
v38 = &savedregs;
v40 = &v23;
v39 = (((unsigned int)&loc_13E + 2) | 1) + 42164;
_Unwind_SjLj_Register(&v34);
__asm
{
VMOV.F32 D0, #7.0
VMOV D1, R4, R4
VCMPE.F32 S2, S0
VMRS APSR_nzcv, FPSCR
}
if ( !(_NF ^ _VF) )
{
v35 = -1;
v13 = objc_msgSend(v7, "webView");
v35 = -1;
v14 = (void *)objc_retainAutoreleasedReturnValue(v13);
v35 = -1;
v15 = objc_msgSend(v7, "jsContextKeyPath");
v35 = -1;
v16 = objc_retainAutoreleasedReturnValue(v15);
v35 = -1;
v17 = objc_msgSend(v14, "valueForKeyPath:", v16);
v35 = -1;
v18 = (void *)objc_retainAutoreleasedReturnValue(v17);
objc_release(v16);
if ( v18 )
{
v25 = v14;
objc_initWeak(&v32, v7);
v26 = &_NSConcreteStackBlock;
v27 = -1040187392;
v28 = 0;
v29 = _43__UZWebView_initJavaScriptCoreBridgeMethod__block_invoke;
v33 = &v31;
v30 = &__block_descriptor_tmp_1135;
objc_copyWeak(&v31, &v32);
v23 = objc_retainBlock(&v26);
v35 = 1;
v24 = v18;
objc_msgSend(v18, "setObject:forKeyedSubscript:");
v19 = v33;
objc_release(v23);
objc_destroyWeak(v19);
objc_destroyWeak(&v32);
v14 = v25;
v18 = v24;
}
objc_release(v18);
objc_release(v14);
}
_Unwind_SjLj_Unregister(&v34);
_R4 = &v41;
__asm
{
VLD1.64 {D8-D11}, [R4@128]!
VLD1.64 {D12-D15}, [R4@128]
}
}

这里我们可以看到,首先是利用KVC从UZWebView获得JSContext,然后setObject:forKeyedSubscript:设置_apiBridgeMethod的值为_43__UZWebView_initJavaScriptCoreBridgeMethod__block_invoke,它的实现如下:

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
int __fastcall _43__UZWebView_initJavaScriptCoreBridgeMethod__block_invoke(
__block_literal_3 *.block_descriptor,
Foundation::NSString::NSString *a2,
Foundation::NSString::NSString *method,
Foundation::NSDictionary::NSDictionary *param,
Foundation::NSString::NSString *module,
int a6)
{
Foundation::NSDictionary::NSDictionary *v7; // r5@1
Foundation::NSString::NSString *v8; // r4@1
int v9; // r10@1
int v10; // r4@1
void *v11; // r8@1
int v12; // r11@1
void *v13; // r2@1
int v14; // r0@2
void *v15; // r0@3
void *v16; // ST04_4@4
int v17; // r5@4
void *v18; // r0@4
void *v19; // r8@4
void *v20; // r4@4
void *v21; // r0@4
int v22; // r6@4
__block_literal_3 *v24; // [sp+8h] [bp-20h]@1
v24 = .block_descriptor;
v7 = param;
v8 = method;
v9 = objc_retain(a2);
v10 = objc_retain(v8);
v11 = (void *)objc_retain(v7);
v12 = objc_retain(a6);
v13 = objc_msgSend(&OBJC_CLASS___NSDictionary, "class");
if ( (unsigned int)objc_msgSend(v11, "isKindOfClass:", v13) & 0xFF )
{
v14 = objc_retain(v11);
}
else
{
v15 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:count:");
v14 = objc_retainAutoreleasedReturnValue(v15);
}
v16 = v11;
v17 = v14;
v18 = objc_msgSend(&OBJC_CLASS___UZCommand, "alloc");
v19 = objc_msgSend(v18, "initWithClassName:methodName:param:", v9, v10, v17);
objc_release(v10);
objc_release(v9);
objc_msgSend(v19, "setIsSyncMethod:", module);
objc_msgSend(v19, "setModule:", v12);
objc_release(v12);
v20 = (void *)objc_loadWeakRetained(&v24->weakSelf);
v21 = objc_msgSend(v20, "execute:", v19);
v22 = objc_retainAutoreleasedReturnValue(v21);
objc_release(v20);
objc_release(v19);
objc_release(v17);
objc_release(v16);
return objc_autoreleaseReturnValue(v22);
}

大概意思是,读取调用信息,根据信息创建一个UZCommand对象,再去执行UZCommand,再看execute的实现:

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
// UZWebView - (id)execute:(id)
id __cdecl -[UZWebView execute:](struct UZWebView *self, SEL a2, id a3)
{
struct UZWebView *v3; // r11@1
void *v4; // r6@1
int v5; // r5@2
void *v6; // r0@3
int v7; // r10@3
void *v8; // r0@3
void *v9; // r8@3
void *v10; // r0@3
void *v11; // r0@3
void *v12; // r4@3
void *v13; // r0@3
int v14; // r5@3
void *v15; // r0@3
int v16; // r0@3
void *v17; // r6@3
void *v18; // r0@3
void *v19; // r5@3
int v20; // r10@4
void *v21; // r0@5
void *v22; // r4@5
void *v23; // r0@5
void *v24; // r0@5
void *v25; // r6@5
void *v26; // r0@5
void *v27; // r10@5
void *v28; // ST10_4@5
void *v29; // r4@5
void *v30; // r0@5
void *v31; // r0@5
int v32; // r4@5
void *v33; // r2@6
void *v34; // r0@7
void *v35; // r0@10
int v36; // r8@10
int v37; // r6@10
int v38; // r11@11
void *v39; // r0@12
void *v40; // r4@12
void *v41; // r0@13
int v42; // r4@13
void *v43; // r0@16
int v44; // r4@16
int v46; // [sp+18h] [bp-28h]@4
void *v47; // [sp+1Ch] [bp-24h]@10
int v48; // [sp+20h] [bp-20h]@3
void *v49; // [sp+24h] [bp-1Ch]@3
v3 = self;
v4 = (void *)objc_retain(a3);
if ( !((unsigned int)objc_msgSend(v3, "shouldClosed") & 0xFF) )
{
v6 = objc_msgSend(v4, "methodName");
v7 = objc_retainAutoreleasedReturnValue(v6);
v8 = objc_msgSend(v4, "className");
v9 = (void *)objc_retainAutoreleasedReturnValue(v8);
v10 = objc_msgSend(v4, "paramDict");
v48 = objc_retainAutoreleasedReturnValue(v10);
v11 = objc_msgSend(v3, "request");
v12 = (void *)objc_retainAutoreleasedReturnValue(v11);
v13 = objc_msgSend(v12, "URL");
v14 = objc_retainAutoreleasedReturnValue(v13);
v49 = v4;
v15 = objc_msgSend(v4, "module");
v16 = objc_retainAutoreleasedReturnValue(v15);
objc_release(v16);
v17 = v9;
objc_release(v14);
objc_release(v12);
v18 = objc_msgSend((void *)v3->_moduleDict, "objectForKey:", v9);
v19 = (void *)objc_retainAutoreleasedReturnValue(v18);
if ( !v19 )
{
v46 = v7;
v20 = NSClassFromString(v9);
if ( v20
|| (v21 = objc_msgSend(&OBJC_CLASS___NSBundle, "mainBundle"),
v22 = (void *)objc_retainAutoreleasedReturnValue(v21),
v23 = objc_msgSend(v22, "infoDictionary"),
v24 = (void *)objc_retainAutoreleasedReturnValue(v23),
v25 = v24,
v26 = objc_msgSend(v24, "stringValueForKey:defaultValue:", CFSTR("CFBundleExecutable"), &stru_12864),
v27 = (void *)objc_retainAutoreleasedReturnValue(v26),
v28 = v27,
objc_release(v25),
objc_release(v22),
v29 = objc_msgSend(v27, "length"),
v17 = v9,
v19 = 0,
v30 = objc_msgSend(v9, "length"),
v31 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("_TtC%lu%@%lu%@"), v29, v27, v30, v9),
v32 = objc_retainAutoreleasedReturnValue(v31),
v20 = NSClassFromString(v32),
objc_release(v32),
objc_release(v28),
v20) )
{
v33 = objc_msgSend(&OBJC_CLASS___UZModule, "class");
if ( (unsigned int)objc_msgSend((void *)v20, "isSubclassOfClass:", v33) & 0xFF )
{
v34 = objc_msgSend((void *)v20, "alloc");
v19 = objc_msgSend(v34, "initWithUZWebView:", v3);
}
}
if ( !v19 )
{
v43 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("ERROR: Module '%@' not found"), v17);
v44 = objc_retainAutoreleasedReturnValue(v43);
objc_msgSend(v3, "sendErrorEvent:", v44);
NSLog(CFSTR("%@"), v44);
objc_release(v44);
v5 = 0;
v7 = v46;
v38 = v48;
goto LABEL_17;
}
objc_msgSend((void *)v3->_moduleDict, "setObject:forKey:", v19, v17);
v7 = v46;
}
v47 = v17;
v35 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@:"), v7);
v36 = objc_retainAutoreleasedReturnValue(v35);
v37 = NSSelectorFromString();
if ( (unsigned int)objc_msgSend(v19, "respondsToSelector:", v37) & 0xFF )
{
v38 = v48;
if ( (unsigned int)objc_msgSend(v49, "isSyncMethod") & 0xFF )
{
v39 = objc_msgSend(v19, "performSelector:withObject:", v37, v48);
v40 = v19;
v5 = objc_retainAutoreleasedReturnValue(v39);
objc_release(v36);
objc_release(v40);
v17 = v47;
LABEL_17:
objc_release(v38);
objc_release(v17);
objc_release(v7);
v4 = v49;
goto LABEL_18;
}
objc_msgSend(v19, "performSelectorOnMainThread:withObject:waitUntilDone:", v37, v48, 0);
v17 = v47;
}
else
{
v17 = v47;
v41 = objc_msgSend(
&OBJC_CLASS___NSString,
"stringWithFormat:",
CFSTR("ERROR: Method '%@' not defined in Module '%@'"),
v7,
v47);
v42 = objc_retainAutoreleasedReturnValue(v41);
objc_msgSend(v3, "sendErrorEvent:", v42);
NSLog(CFSTR("%@"), v42);
objc_release(v42);
v38 = v48;
}
objc_release(v36);
objc_release(v19);
v5 = 0;
goto LABEL_17;
}
v5 = 0;
LABEL_18:
objc_release(v4);
return (id)objc_autoreleaseReturnValue(v5);
}

主要流程是从UZCommand中读出模块,方法,参数等信息,根据类名创建了实例,然后调用初始化构造函数initWithUZWebView(这里和官网介绍如何自定义模块中的说明是一致的),然后会判断是同步调用还是异步调用,同步调用则直接执行performSelector:withObject:,异步调用执行performSelectorOnMainThread:withObject:waitUntilDone:,至此,完成了JS到native方法的调用。

(2.2)如果使用了WKWebView,则通过发送信息的方式将调用信息发给Native端。

不过,目前SuperWebView暂未支持WKWebView,没有开放api和文档出来。

(2.3)uz$q和uz$r的实现如下

1
2
3
4
5
6
7
8
9
10
//uz$q
{c: Array[0], flag: true}
//uz$r
function uz$r() {
if(uz$q.flag && uz$q.c.length>0){
uz$q.flag = false;
window.location = 'uz://' + uz$q.c[0];
}
}

这里是将调用信息拼接成一个URL,scheme采用uz:,赋值给window.location赋值对当前页面重定向,这里应该是采用UIWebView的代理方式实现与Native端通信,会调用到webView:shouldStartLoadWithRequest:navigationType:代理方法中去,这种就是JSBridge的方式了。

补充说明一点,从Native回调到JS端的方式是,通过stringByEvaluatingJavaScriptFromString:的方式,通知js端,并把callbackID作为参数回传过去。

关于SuperWebView所指的混合渲染的说明

看到混合渲染,第一理解是,SuperWebView支持将Native的View直接fix在UIWebView内部做渲染,这样做就需要修改webview的渲染机制,并且还要支持将css的解析映射到native view中去,通过官方文档和使用教程,并没有看到相关介绍。目前我的理解是,将Native的View作为子View添加到UIWebView中去。为了验证这一点,我们使用官网提供个UISlider模块来写示例代码,在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
44
45
var uislider = api.require('UISlider');
uislider.open({
animation: true,
orientation: 'horizontal',
rect: {
x: 10,
y: 100,
size: 300
},
bubble: {
direction: 'top',
state: 'always',
w: 80,
h: 30,
size: 14,
color: '#888',
bg: 'widget://res/slider/bubble.png',
prefix: '温度:',
suffix: '摄氏度'
},
handler: {
w: 10,
h: 8,
bg: 'widget://res/slider/handler.png'
},
bar: {
h: 4,
bg: 'widget://res/slider/background.png',
active: 'widget://res/slider/bar-active.png'
},
value: {
min: 16,
max: 32,
step: 0.5,
init: 26
},
fixedOn: api.frameName,
fixed: false
}, function(ret, err) {
if (ret) {
alert(JSON.stringify(ret));
} else {
alert(JSON.stringify(err));
}
});

打开的视图效果如下:

我们看到UISlider是悬浮在WebView之上的,那么我们可以猜测,其是通过在native构建并添加到WebView上的,这一点可以通过反编译了UISlider的目标文件,可以看到open方法的实现里面多次调用了addSubView方法。

优点:

  • 性能好
  • 提供方便的api进行页面组合
  • 模块化平台,让更多的人为前端工程师服务
  • 管理平台:热更新,云数据等

缺点:

  • 样式需要前端开发者去模拟native的样式
  • 有些系统级的动画暂不支持,如导航栏渐变动画

5+Runtime & Native.js

  • 5+Runtime是对HTML5+规范的实现,除了支持标准HTML5外,还扩展了JavaScript对象plus,使得js可以调用各种浏览器无法实现或实现不佳的系统能力,设备能力如摄像头、陀螺仪、文件系统等,业务能力如上传下载、二维码、地图、支付、语音输入、消息推送等。编写一次,可跨平台运行。
  • 大量的手机OS的原生API无法被HTML5使用,Native.js把原生API封装成了js对象,通过js可以直接调ios和android的原生API。这部分就不再跨平台,写法分别是plus.ios和plus.android。
  • Native.js不是一个js库,不需要下载引入到页面的script中,也不像nodejs那样有单独的运行环境,Native.js的运行环境是集成在5+runtime里的。

使用方式:

  • 对于web端开发者,使用HBuilder IDE,它集成了5+Runtime和Native.js,可以创建移动项目来开发App
  • 对于Native端开发者,可以从平台下载SDK集成到项目中使用

看到5+runtime说是开源了,不过在开源项目中并未找到iOS native的代码实现,其中还是以静态库的形式提供,不过在pdr.js文件中,看到了plus.tools和plus.bridge的实现,这两个实现在后文中会使用到。

Native.js通信方式实现分析

下面以使用iOS中的UIAlertView为示例。

iOS端使用UIAlertView的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#import <UIKit/UIKit.h>
//...
// 创建UIAlertView类的实例对象
UIAlertView *view = [UIAlertView alloc];
// 设置提示对话上的内容
[view initWithTitle:@"自定义标题" // 提示框标题
message:@"使用NJS的原生弹出框,可自定义弹出框的标题、按钮" // 提示框上显示的内容
delegate:nil // 点击提示框后的通知代理对象,nil类似js的null,意为不设置
cancelButtonTitle:@"确定(或者其他字符)" // 提示框上取消按钮的文字
otherButtonTitles:nil]; // 提示框上其它按钮的文字,设置为nil表示不显示
// 调用show方法显示提示对话框,在OC中使用[]语法调用对象的方法
[view show];

JS端使用UIAlertView的方式如下:

1
2
3
4
5
6
7
8
9
10
// 创建UIAlertView类的实例对象
var view = new UIAlertView();
// 设置提示对话上的内容
view.initWithTitlemessagedelegatecancelButtonTitleotherButtonTitles("自定义标题" // 提示框标题
, "使用NJS的原生弹出框,可自定义弹出框的标题、按钮" // 提示框上显示的内容
, null // 操作提示框后的通知代理对象,暂不设置
, "确定(或者其他字符)" // 提示框上取消按钮的文字
, null ); // 提示框上其它按钮的文字,设置为null表示不显示
// 调用show方法显示提示对话框
view.show();

其中UIAlertView、initWithTitlemessagedelegatecancelButtonTitleotherButtonTitles和show的实现如下:

1
2
3
4
5
6
7
8
9
10
window.plus.ios.UIAlertView = function(create) {
this.__UUID__ = window.plus.tools.UUID('JSB');
this.__TYPE__ = 'JSBObject';
var args = window.plus.ios.__Tool.process(arguments);
if ( create && plus.tools.IOS == plus.tools.platform ) {
} else {
window.plus.bridge.execSync('Invocation', '__Instance', [this.__UUID__, 'UIAlertView', args]);
}
};
1
2
3
4
5
6
7
8
9
10
11
plus.ios.UIAlertView.prototype.initWithTitlemessagedelegatedefaultButtoncancelButtonotherButtons = function () {
var ret = null;
try {
var args = window.plus.ios.__Tool.process(arguments);
ret = window.plus.bridge.execSync('Invocation', '__exec', [this.__UUID__, 'initWithTitle:message:delegate:defaultButton:cancelButton:otherButtons:', args]);
ret = plus.ios.__Tool.New(ret, false);
} catch (e) {
throw e;
}
return ret;
};
1
2
3
4
5
6
7
8
9
10
11
plus.ios.UIAlertView.prototype.show = function () {
var ret = null;
try {
var args = window.plus.ios.__Tool.process(arguments);
ret = window.plus.bridge.execSync('Invocation', '__exec', [this.__UUID__, 'show', args]);
ret = plus.ios.__Tool.New(ret, false);
} catch (e) {
throw e;
}
return ret;
};

可以看到,我们创建的UIAlertView是一个JS对象,这个对象是当我们使用UIAlertView = plus.ios.importClass("UIAlertView");时动态创建的JS对象,与Native的UIAlertView相对应,我们称该JS对象为NJS对象。我们对NJS对象UIAlertView进行的方法调用,最终会执行window.plus.bridge.execSync,我们需要看下它的实现,在此之前,关于通过NJS对象访问Native对象,先做一些说明。

  • 首次导入Native类对象时,Native.js会动态创建一个JS对象与之相对应,JS对象包括相应的构造函数、方法、父类(prototype)等信息。
  • 由于是动态创建对应的JS对象,这里有一定的性能损耗,官方文档中性能优化一节建议页面打开后触发的“plusready”事件中进行类对象的导入操作,这样提前导入了我们需要导入的类对象,是我们在后面逻辑中使用时保证其已经导入,这种方式只是将导入时机提前,并不是消除了导入带来的损耗。所以官方也不建议我们在一个页面中导入过多的类对象,这样会影响性能。
  • 数据类型转换:在NJS中调用Native API或从Native API返回数据到NJS时会自动转换数据类型。
  • Native类对象的方法会在JS对象中有份映射,方法名是native方法名去掉‘冒号’之后的名称(字母大小写不变)。
  • 对于映射的JS对象,可以通过“.”调用方式来访问native对象的属性,但这种方式获得的值是Native层对象被映射为NJS对象那一刻的属性值,如果需要实时获取和设置native对象属性值,需要使用plusGetAttribute和plusSetAttribute方法,但这种方式效率比较低。
  • Objective-C和Java中类如果存在继承自基类,在NJS中对应的对象会根据继承关系递归将所有基类的公有方法一一换成NJS对象的方法,所有基类的公有属性也可以通过其plusGetAttribute、plusSetAttribute方法访问。
  • 由于Objective-C中类没有静态变量,而是通过定义全局变量来实现,目前NJS中无法访问全局变量的值。对于全局常量,在NJS中也无法访问。

继续之前的window.plus.bridge.execSync方法调用,其方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function (service,action,args,fn){
var json,sync,ret;
if(T.IOS==T.platform){
try{
if(json=T.stringify([[window.__HtMl_Id__,service,action,null,args]]),
sync=B.synExecXhr,
sync.open("post","http://localhost:13131/cmds",!1),
sync.setRequestHeader("Content-Type","multipart/form-data"),
sync.send(json),
fn)
return fn(sync.responseText)
}catch(e){
console.log("sf:"+action+"-"+service)
}
return window.eval(sync.responseText)
}
return T.ANDROID==T.platform?
(ret=window.prompt(T.stringify(args),"pdr:"+T.stringify([service,action,!1])),fn?fn(ret):eval(ret))
:void 0
}

其中:

1
2
T=plus.tools,
B=plus.bridge

synExecXhr的全称是“同步调用XML HTTP Request”,前面我们提到了plus.bridge的实现中我们可以看到:

1
synExecXhr: new XMLHttpRequest()

可以看到,synExecXh实际是一个XMLHttpRequest对象,通过它最后将调用信息以http请求的方式发出去。我们在Native端利用oc-runtime hook住UIAlertView的构造函数,添加断点,可以看到调用栈如下图所示:

可以看到,JS调用到Native端通过DCAsycSocket以这种进程间通信的方式来实现,并且在非主线程完成。至此,我们可以得知,native.js中js与native端的通信是通过本地socket同步通信的方式完成的,完成调用之后,调用结果会以字符串的形式保存在sync.responseText中,js端再通过evaluate其中的字符串来得到返回结果。

小结:5+runtime还是基于WebView来做事,属于hybrid流派,通过本地socket通信方式来实现JS与Native的混合调用,支持动态导入native类对象,进行实例化、方法调用、属性访问等操作,与一般的hybrid技术不同的是,不需要native工程师来提供模块或者插件来支持扩展js的能力,web工程是可以参考native的方法调用类似的方式(只需要简单的修改),实现对native对象的访问。同时5+runtime还提供了一些跨平台的通用组件,如摄像头、陀螺仪、文件系统等。使用native.js技术所需要注意的问题就是性能问题,动态导入和访问native对象以及数据类型转化需要付出一定的性能损耗代价,官方给出了一些建议来进行性能优化。另外,值得一提的是DCloud公司还用HTML5做了一套模拟Native UI的开源项目MUI,有兴趣可以参考这里.

优点:

  • web端可以直接访问native的api,调用接口参考
  • 提供一套native样式库:MUI

    缺点:

  • 与native交互性能有点弱

Titanium

Titanium:”Write in JavaScript, run native everywhere”.

Titanium与PhoneGap不同,并不是基于WebView来做跨平台开发,属于JavaSciptBridge流派,关于它与PhoneGap的对比可以参考这篇文章。值得一提的是,Titanium的上层语言并没有采用HTML+CSS+ JavaScript,而是XML+JSON+JavaScript,这增加了一定的学习成本。

React Native

ReactNative:”learn once,write anywhere”.

ReactNative和Titanium的思路很像,也抛弃了WebView,属于JavaSciptBridge流派。ReactNative用JavaScript编写程序,渲染的界面全部都是Natvie的。React是前端的知名开发库,程序代码通过操作Virturl DOM来编写程序,React runtime负责操作和更新真正的DOM节点,而这个更新是通过diff做增量更新,这提高性能,ReactNative沿用了React的编程模型和更新模型。

ReactNative的JS运行在与应用主线程独立的线程,通过异步操作与Natvie接口通信,线程模型可以参考下图:

JS解释器可以运行于手机中的独立线程,也可以远程调试时运行在浏览器中,另外,I/O操作、图片解码、布局信息计算等其他一些消耗CPU的操作也可以放到独立线程中,iOS应用主线程用来操作UI控件和Native API访问,JS线程与UI主线程之间通过ReactNative桥接进行异步通信,实现JS与Objective-C之间的相互调用。

React Native通信机制源码分析

源码分析基于React Native v0.23.1,不过下载了目前的最新版0.38.0调试了,实现方式大同小异。

React Native 的思路就是将Native方法导出给JS用,使得用户可以用JS编写程序,而采用原生控件构建构建应用。
React Native 导出以模块(Module)为单位,在程序启动时,加载需要注册到js中的module,挂到js解释器的__fbBatchedBridgeConfig变量上,格式如下:

1
2
3
4
5
6
7
8
{"remoteModuleConfig":[
["RCTStatusBarManager",
["getHeight","setStyle","setHidden","setNetworkActivityIndicatorVisible"]],
["RCTSourceCode",
{"scriptURL":"http:\/\/localhost:8081\/index.ios.bundle?platform=ios&dev=true&hot=true"},
["getScriptText"],[0]]
]

React Native还在不断的迭代开发中,不同版本的实现方式可能不同,例如,在React Native通信机制详解一文中介绍,Natvie模块的注册方式是通过在利用编译指令将需要导出的模块存储到执行文件的二进制DATA端,程序启动时再从中读取导出的模块信息,我使用的源码是v0.23.1版本,可以看到,需要bridge的模块需要使用RCTRegisterModule宏,其展开如下:

1
2
3
4
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

可以看到,bridge的模块在load方法中进行注册,注册的模块保存在RCTBridge.m的static NSMutableArray<Class> *RCTModuleClasses;全局静态变量中。模块中需要bridge的方法使用RCT_EXPORT_METHOD宏,默认情况下,使用OC方法的第一个分号之前的部分作为JS中的调用名称,例如模块ModuleName中,需要导出的方法- (void)doSomething:(NSString *)aString withA:(NSInteger)a andB:(NSInteger)b,需要写成

1
2
3
4
RCT_EXPORT_METHOD(doSomething:(NSString *)aString
withA:(NSInteger)a
andB:(NSInteger)b)
{ ... }

最终JS的调用形式是NativeModules.ModuleName.doSomething

应用启动时,会创建一个CADisplayLink添加到线程(真机上是JS线程,模拟器上是主线程)的runloop中,周期性的调用JS的callFunctionReturnFlushedQueue方法,这个方法的作用就是从一个MessageQueue中取出消息内容。JS调用OC方法,会将调用的信息(moduleID、methodID、params)保存在这个MessageQueue中。

这里需要说明一下:测试发现,在模拟器中,JS线程与Native端使用RCTSRWebSocketExecutor来进行通信,在真机上,使用RCTJSCExecutor来执行js脚本,在RCTJSCExecutor中可以看到对它的说明Uses a JavaScriptCore context as the execution engine.

回到刚才的话题,调用callFunctionReturnFlushedQueue之后,会从MessageQueue取出调用信息,已json字符串的形式返回给native端,native端通过RCTJSONParse接口parse出调用信息,信息包括模块id,方法id,参数,以及可能的回调id,格式如下:

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
<__NSCFArray 0x7f8bd1610e60>(
//moduleIDs
<__NSCFArray 0x7f8bd16c4ae0>(
56,
33,
33,
34
)
,
//methodIDs
<__NSCFArray 0x7f8bd16f0400>(
1,
5,
4,
0
)
,
//params
<__NSCFArray 0x7f8bd16396c0>(
<__NSCFArray 0x7f8bd16f7d60>(
ws://localhost:8097/devtools,
<null>,
<null>,
280
)
,
<__NSCFArray 0x7f8bd163d350>(
29,
RCTView,
1,
<null>
)
,
<__NSCFArray 0x7f8bd162e670>(
7,
<null>,
<null>,
<__NSCFArray 0x7f8bd16e4c90>(
29
)
,
<__NSCFArray 0x7f8bd16f0d90>(
5
)
,
<__NSCFArray 0x7f8bd1621a10>(
5
)
)
,
<__NSCFArray 0x7f8bd16c7670>(
5,
2,
3
)
)
,
//callID
1171
)

我们先来看下JS部分是如何创建这个队列以及将这些消息调用信息保存在队列中的。

1
2
3
4
5
//BatchedBridge.js
const BatchedBridge = new MessageQueue(
__fbBatchedBridgeConfig.remoteModuleConfig,
__fbBatchedBridgeConfig.localModulesConfig,
);

前面提到过fbBatchedBridgeConfig.remoteModuleConfig是Native端注册的模块,fbBatchedBridgeConfig.localModulesConfig是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
//MessageQueue.js
constructor(remoteModules, localModules) {
this.RemoteModules = {};
this._callableModules = {};
this._queue = [[], [], [], 0];
this._moduleTable = {};
this._methodTable = {};
this._callbacks = [];
this._callbackID = 0;
this._callID = 0;
this._lastFlush = 0;
this._eventLoopStartTime = new Date().getTime();
[
'invokeCallbackAndReturnFlushedQueue',
'callFunctionReturnFlushedQueue',
'flushedQueue',
].forEach((fn) => this[fn] = this[fn].bind(this));
let modulesConfig = this._genModulesConfig(remoteModules);
//构建模块信息
this._genModules(modulesConfig);
localModules && this._genLookupTables(
this._genModulesConfig(localModules),this._moduleTable, this._methodTable
);
this._debugInfo = {};
this._remoteModuleTable = {};
this._remoteMethodTable = {};
this._genLookupTables(
modulesConfig, this._remoteModuleTable, this._remoteMethodTable
);
}

里面调用this._genModules(modulesConfig);,最后会调到_genModule。

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
//config是module信息,包括模块名,导出方法名列表等,moduleID模块对应的id(这个ID就是模块在remoteModules数组中的索引)
_genModule(config, moduleID) {
if (!config) {
return;
}
let moduleName, constants, methods, asyncMethods;
if (moduleHasConstants(config)) {
[moduleName, constants, methods, asyncMethods] = config;
} else {
[moduleName, methods, asyncMethods] = config;
}
let module = {};
methods && methods.forEach((methodName, methodID) => {
const methodType =
asyncMethods && arrayContains(asyncMethods, methodID) ?
MethodTypes.remoteAsync : MethodTypes.remote;
//构建方法信息
module[methodName] = this._genMethod(moduleID, methodID, methodType);
});
Object.assign(module, constants);
if (!constants && !methods && !asyncMethods) {
module.moduleID = moduleID;
}
//构建的module信息保存在RemoteModules中
this.RemoteModules[moduleName] = module;
return module;
}

调用this._genMethod构建方法信息。

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
//module:模块ID,method:方法ID(是方法在方法名列表中的索引),type:"remote,remoteAsync",区分是同步调用还是异步调用,异步调用用Promise实现
_genMethod(module, method, type) {
let fn = null;
let self = this;
if (type === MethodTypes.remoteAsync) {
fn = function(...args) {
return new Promise((resolve, reject) => {
self.__nativeCall(
module,
method,
args,
(data) => {
resolve(data);
},
(errorData) => {
var error = createErrorFromErrorData(errorData);
reject(error);
});
});
};
} else {
fn = function(...args) {
let lastArg = args.length > 0 ? args[args.length - 1] : null;
let secondLastArg = args.length > 1 ? args[args.length - 2] : null;
let hasSuccCB = typeof lastArg === 'function';
let hasErrorCB = typeof secondLastArg === 'function';
hasErrorCB && invariant(
hasSuccCB,
'Cannot have a non-function arg after a function arg.'
);
let numCBs = hasSuccCB + hasErrorCB;
let onSucc = hasSuccCB ? lastArg : null;
let onFail = hasErrorCB ? secondLastArg : null;
args = args.slice(0, args.length - numCBs);
return self.__nativeCall(module, method, args, onFail, onSucc);
};
}
fn.type = type;
return fn;
}

这里我们可以看到,__nativeCall的调用被包装在一个function中,这个function作为first-classed Value被返回,已的形式保存在module信息中。我们开看到,native调用的成功和失败回调函数也是在这里传入。

至此,Native方法的调用映射表构建完成。下面是JS端网络接口的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var RCTNetworkingNative = require('NativeModules').Networking;
/**
* This class is a wrapper around the native RCTNetworking module.
*/
class RCTNetworking {
static sendRequest(query, callback) {
RCTNetworkingNative.sendRequest(query, callback);
}
static abortRequest(requestId) {
RCTNetworkingNative.cancelRequest(requestId);
}
}
module.exports = RCTNetworking;

可以看到,RCTNetworking最终调用的是Native映射表中的本地方法。

我们继续来看__nativeCall的实现,这里很重要。

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
__nativeCall(module, method, params, onFail, onSucc) {
//(1)
if (onFail || onSucc) {
// eventually delete old debug info
(this._callbackID > (1 << 5)) &&
(this._debugInfo[this._callbackID >> 5] = null);
this._debugInfo[this._callbackID >> 1] = [module, method];
onFail && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onFail;
onSucc && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onSucc;
}
global.nativeTraceBeginAsyncFlow &&
global.nativeTraceBeginAsyncFlow(TRACE_TAG_REACT_APPS, 'native', this._callID);
this._callID++;
//(2)
this._queue[MODULE_IDS].push(module);
this._queue[METHOD_IDS].push(method);
this._queue[PARAMS].push(params);
//(3)
var now = new Date().getTime();
if (global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {
global.nativeFlushQueueImmediate(this._queue);
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
}
Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
if (__DEV__ && SPY_MODE && isFinite(module)) {
console.log('JS->N : ' + this._remoteModuleTable[module] + '.' +
this._remoteMethodTable[module][method] + '(' + JSON.stringify(params) + ')');
}
}

代码说明:
(1)往params中添加回调回调方法对应的id,可以看到,回调id保存在params数组最后,还会记录debug信息。
(2)将native调用信息添加到MessageQueue,可以看到,MessageQueue的格式是this._queue = [[], [], [], this._callID];,moduleID、methodID和params分别被加入到MessageQueue中不同数组中,所以就看到了前面我们打印出来的Native端收到的数据格式。
(3)如果是要求立即调用并且超时,则会调用global.nativeFlushQueueImmediate接口。这里我查找了下代码,js端和native端并没有为global.nativeFlushQueueImmediate,可见一般不会进入这个流程,一般还是使用前面介绍的Native定时驱动的方式来获取MessageQueue中的调用信息。但是,既然有这个值,说明是支持js主动调用到native端的,那又是如何实现的呢?

在RCTJSCExecutor.m文件中的setUp方法,我们看到这样一段代码:

1
2
3
4
5
6
7
8
9
10
[self addSynchronousHookWithName:@"nativeFlushQueueImmediate" usingBlock:^(NSArray<NSArray *> *calls){
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid || !calls) {
return;
}
RCT_PROFILE_BEGIN_EVENT(0, @"nativeFlushQueueImmediate", nil);
[strongSelf->_bridge handleBuffer:calls batchEnded:NO];
RCT_PROFILE_END_EVENT(0, @"js_call", nil);
}];

再看addSynchronousHookWithName的实现:

1
2
3
4
5
6
7
- (void)addSynchronousHookWithName:(NSString *)name usingBlock:(id)block
{
__weak RCTJSCExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:^{
weakSelf.context.context[name] = block;
}];
}

我们发现,这里是将native的方法挂到js解释器的全局上下文上,这样js端就可以直接调用这些native方法。除了nativeFlushQueueImmediate以外,还有其他一些全局方法。

至此,我们看到了JS端如何调用组织数据发送给Native端,那Native端拿到数据之后是如何处理的呢?

我跟踪了JS调用创建UIView的过程(通过hook UIView的initWithFrame方法,加断点进行测试),其导出的Native方法在RCTUIManager.m中,方法签名如下:

1
2
3
4
createView:(nonnull NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(__unused NSNumber *)rootTag
props:(NSDictionary *)props

创建UIView的流程如下:
Native端接收到数据,传给RCTBatchedBridge.m中的handleBuffer函数处理,从中解析出多组信息,每组信息包括模块id,方法id,参数,以及可能的回调id,用匿名block将每组信息包起来(这里我们取个名叫block1),放到moduleID所对应的RCTModuleData的methodQueue中异步执行,RCTModuleData是对module示例的包装,methodQueue是由module实例返回,由于iOS对UI的操作一般需要放在这线程,所以可以看到,UI相关的module的method的queue的实现返回的都是主线程:

1
2
3
4
5
//RCTActionSheet.m
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}

同时,还会往methodQueue中添加匿名block(这里我们取个名叫block2)的异步操作。block1执行时,首先根据moduleID在全局表中查找到对应的Module(RCTModuleData,ModuleData的创建会根据前面提到的RCTModuleClasses中的Class一一对应创建,保存在RCTBatchedBridge.m中的NSArray<RCTModuleData *> *_moduleDataByID;中),然后再根据methodID在Module中找到相应的方法RCTBridgeMethod(RCTBridgeMethod是对native方法的封装,内部会持有一个NSInvocation),为内部的_invocation设置参数并调用。这时实际调用的就是我们导出的Natvie方法了(也就是前面的createView: viewName:rootTag:props:),他会将创建UI的操作再次封装成一个个block(这里我们取个名叫block3),放到一个集合NSMutableArray<dispatch_block_t> *_pendingUIBlocks;中,当block2执行时,会从_pendingUIBlocks中逐个取出block3并执行,从而创建出相应的Natvie UI。

如果调用Natvie之后,JS端需要CallBack被调用,会将CallBackID通过参数传给Native,Native函数执行完成之后,会通过发送json字符串的形式发送给JS调用结果,其中会带上CallBackID以及此次发送消息的ID。

在查看ReactNative源码之前,本以为JS与OC之前的通信是通过利用JavaScriptCore进行JSBinding这种静态绑定的方式,查看源码之后才发现不是这样。通过上面对源码的分析,可以看到,RN自己实现以一套通信模式,JS与OC之间的调用采用动态查找的方式来实现,JS和Native工作于独立的线程,线程间根据一套基于ID映射协议的方式来进行通信,这样的动态查找的方式通信成本会高一些,为什么没有用JSBinding的方式,个人理解主要有一下两点原因。

  1. ReactNative 的目标是跨平台,并不是所有平台都采用JavaScriptCore引擎,另外,iOS7之前 JavaScriptCore也没有开发出来。
  2. ReactNative 的 JS 代码工作在独立线程(非主线程),如果采用静态绑定的方式,无法或者很难保证对UI的操作工作在主线程。

性能测试

(1)Native

(2)ReactNative

性能测试采用 RN v0.38.0,测试环境:iphone6,iOS9.3.2,测试用例,点击‘GO’按钮,push(带动画)一个新页面,新页面包含一个列表,首屏展示6项,向上滑动,总共展示20项,列表使用的数据是本地数据。测试数据如下,其中

  • 响应时间:从点击button开始构建页面,到页面完全构建完成(viewDidAppear)的时间
  • 页面构建内存开销:从点击button开始构建页面,到页面构建完成内存的增量
  • 页面滚动之后内存开销:向下滑动,加载所有的cell之后的内存增量
测试数据 响应时间(ms) 页面构建内存开销(MB) 页面滚动之后内存开销(MB)
React Native 831 2.56 0.50
Native 555 1.29 1.36

从体验上点击响应的顺滑程度明显native要顺滑很多,从上面的gif图中也可以看出。另外,内存开销上RN首屏列表开销明显高于Native。这里需要补充说明:React Native的ListView并不是使用UITableView实现,而是自己采用UIView实现的,这一点可以从视图层级中看出,如下图:

ListView的缓存策略也是自己做的,从这里得知,React Native的ListView的”Cell”每次都是重新创建,之所以这么做原因是RN认为UITableView
的复用存在“脏数据”的问题,而且,在现在的手机设备上,创建新的cell已经足够快。当”Cell”划出屏幕,相应的“Cell”会被从view tree上取下来,但并不会销毁,只有当内存警告或者列表项太多时,会有Cell的销毁工作,下次使用,再重新构建出来。

其实,ListView在RN中无法用UITableView实现,原因是,如前面所介绍,JS对Native的调用是异步操作,而且消息派发的驱动是Native端做的,试想一下,如果采用UITableView来做,在列表页快速滚动的时候,JS端是不能立刻同步获取下一个Cell来展示的。RN现在的实现方式是用ScrollView来实现,并且会预先创建若干个Cell来用户快速滑动时,下一个Cell的展示。测试发现,对于DataSource有20个数据,首屏只能显示6个cell的情况下,Native端首屏会创建6个cell,总共会创建7个(缓速的滑动);React Native首屏会创建17个cell(用于快速滑动展示而预创建),总共会创建20个。

优点:

  • JS与native的异步通信,脚本不会卡住主线程
  • 原生的控件+原生的体验
  • 热调试能力

缺点:

  • JS与native的异步通信
  • 门槛较高
  • 仅面向React前端开发

samurai-native

samurai-native的思路跟ReactNatvie的思路是一样的,也是将native的接口导给web端使用,而界面的渲染采用natvie控件,它与RN区别主要有两点:

(1)表达语法是HTML+CSS;

(2)标签名称与Native的View的名称对应,RN中由于对标签进行了抽象,有些标签与Native类名称上并不能对应,React Native书写一个tableview cell如下,可以看出,和原生的写法的命名有差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
underlayColor='#dddddd'>
<View>
<View style={styles.rowContainer}>
<Image style={styles.thumb} source={{ uri: rowData.img_url }} />
<View style={styles.textContainer}>
<Text style={styles.price}>£{price}</Text>
<Text style={styles.title}
numberOfLines={1}>{rowData.title}</Text>
</View>
</View>
<View style={styles.separator}/>
</View>
</TouchableHighlight>

关于samurai-native的介绍可以参考他在infoq上的演讲