因为动态化的东西我第一次看实现方案的源码,而且目前还是大三的学生,缺少很多实践经验说错的地方还请原谅,也希望能指出,被告知。想了很久还是决定写出来,求大神勿喷。
并且我的一个朋友bestswifter写了一篇关于ReactNative源码分析的一品文章,React Native 从入门到原理,感兴趣也可以阅读下。
最近看到很多场对动态化提出了很多技术方案,原因就是客户端的业务需求越来越复杂,尤其是一些业务快速发展的互联网产品,肯定会造成版本的更新迭代跟不上业务的变化,尤其是App Store不确定性的审核,这个时候动态化的想法就自然的产生了。我不知道其他人是如何理解动态化的,但是我觉得,动态化指的就是我们不发布新的版本就可以实现大量的应用内容更新,这里的内容不应该仅仅是一些基本信息,应该涉及到应用的主题框架,甚至是布局,排版等。
因为我自己主要专注iOS,所以本次的源码分析和实现主要围绕iOS进行。
App的设计方案
现在移动端有三种主流的设计方案,分别是Web App、Hybrid App、 Native App。简单的叙述下,这三种
- Web App:指的就是利用H5打造的应用,不需要下载,存活于浏览器中,类似轻应用。图像渲染由HTML,CSS完成,性能比较慢,个人感觉体验不是很好,模仿原生界面,大部分依赖于网络。
- Native App:指的就是原生程序,存活在操作系统中(iOS,Android)一个完整的App,但是需要客户下载安装使用。图像的渲染由本地API完成,采用原生组件,支持离线网络。
- Hybrid App:部分H5和部分Native的混合架构,这种方案以H5的动态性为基础,通过定义Native的扩展(Bridge)来实现动态化,大部分依赖于网络;
- Native View方案:使用Native进行渲染的Native View方案,通过修改预定结构中的数据,实现动态化
- ReactNative:通过JavaScript脚本引擎支持页面DOM转换和逻辑控制来实现动态化
动态对比
Hybrid App具备一定的动态能力,但是Hybrid的H5部分体验较差。Web App的体验跟网络有很大的关系,网络环境不好,体验会很差,而且H5的渲染能力比较差。Native View方案不支持逻辑代码的替换。ReactNative的JS引擎不够轻量,不适合大数量的ListView处理。甚至还有更多的动态划方案,尽管ReactNative很火,就像我一个朋友提到过的,到目前位置并没有一种方案统一了动态化方案。
发现LuaView
同样为了更加深入的了解动态化的实现,我尝试去分析一种方案的源码更加深入的去了解。这里我选择了阿里聚划算开源的LuaView,这里我并不了解聚划算的动态化方案是如何构建的,但是原因肯定是因为聚划算的业务不断的扩展,由于聚划算的业务变化需求,因此LuaView的实践性肯定是经过考验的,从实践的角度出发,我选择尝试分析它。
学习Lua的体会
我玩过愤怒的小鸟,用过Photoshop,但是我现在才知道Lua在它们两个中就有应用,接触后,发现Lua是一种轻量级的语言,它的官方版本只包括一个精简的核心和最基本的库,这就让它非常非常的小,编译后也仅仅就是百于k而已,这根Lua的设计目标有关系,它的目标就是成为一个很容易嵌入其它语言中使用的语言,而且Lua可以用于嵌入式硬件,不仅可以嵌入其他编程语言,而且可以嵌入微处理器中。
很多人会发现Lua很轻量,并不具备网络请求,图形UI等能力,但是很多应用使用Lua作为自己的嵌入式语言,因为他本身的接口易于扩展使得它可以通过宿主语言完成能力扩展
以上的Lua的这些特性就让我们发现,使用Lua构建动态化方案的核心就在于将Android,iOS原生的UI、网络、存储、硬件控制等能力桥接到Lua层。如果做到,这种方案就可以支持UI动态搭建、脚本、资源、逻辑动态下发。借助Lua语言的可扩展性,我们可以很方便地在Native跟Lua之间搭建起桥梁,将Native的各种能力迁移到Lua层。
分析LuaView
通过上面繁琐无聊的介绍,我们就可以来分析一波LuaView是如何将Android,iOS原生的UI、网络、存储、硬件控制等能力桥接到Lua层的。
LuaView的意图就是利用Lua去构建Native UI。LuaView没有去自己构建一个UI库,而是借用Android,iOS原生UI,Android支持的Lua引擎为LuaJ,iOS支持的Lua引擎为LuaC。
根据聚划算团队的说明,
LuaView的一条重要设计原则就是同一份逻辑只写一份代码,这需要在设计SDK的时候尽可能得考虑到两个端的共性跟特性,将API构建在两个端的共性领域中,对于两端的特性领域则交由各自的Native部分来实现。
为了实现这种能力,肯定需要构建一个桥接平台,并且设计好统一的API。
源码分析
源码看了很久,然后总算能总结出一些东西,因为还是学生,可能有些地方的实践跟我想的有差异,还希望大家提出。
在分析源码前不得不具体说说Lua,上面也提到过,这个Lua很轻量,很小。因此lua是一个嵌入式语言,就是说它不是一个单独的程序,而是一套可以在其它语言中使用的库,lua可以作为c语言的扩展,反过来也可以用c语言编写模块来扩展lua,这两种情况都使用同样的api进行交互。lua与c主要是通过一个虚拟的“栈”来交换数据。
这个虚拟“栈”是很关键的一个点,Lua利用一个虚拟的堆栈来给C传递值或从C获取值。每当Lua调用C函数,都会获得一个新的堆栈,该堆栈初始包含所有的调用C函数所需要的参数值(Lua传给C函数的调用实参),并且C函数执行完毕后,会把返回值压入这个栈(Lua从中拿到C函数调用结果)。
我自己就是理解Lua引擎在App中其实起到一个内置系统的能力,我们把Lua脚本注入应用程序,Lua引擎自己解析,运行,然后去调用原生UI,这就需要为我们的操作系统进行扩展,利用的就是lua可以作为c语言的扩展,反过来也可以用c语言编写模块来扩展lua
这些理论可能说起来很繁琐,也可能是我自己总结的不够清晰,我们现在来引入实践代码进行分析,最后我们在尝试自己去手动实现一些简单的动态化能力,这样会有更清晰的认知。
看一下LuaView的结构

lv514可以理解为Lua的源码,为什么说可以理解为?因为作者对Lua的源码进行了部分的更改,例如类名,还有一个函数名,举个典型的例子:
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
|
struct
lv
_
State
{
CommonHeader
;
lu_byte
status
;
StkId
top
;
/* first free slot in the stack */
StkId
base
;
/* base of current function */
global_State
*l_G
;
CallInfo
*ci
;
/* call info for current function */
const
Instruction
*savedpc
;
/* `savedpc' of current function */
StkId
stack_last
;
/* last free slot in the stack */
StkId
stack
;
/* stack base */
CallInfo
*end_ci
;
/* points after end of ci array*/
CallInfo
*base_ci
;
/* array of CallInfo's */
int
stacksize
;
int
size_ci
;
/* size of array `base_ci' */
unsigned
short
nCcalls
;
/* number of nested C calls */
unsigned
short
baseCcalls
;
/* nested C calls when resuming coroutine */
lu_byte
hookmask
;
lu_byte
allowhook
;
int
basehookcount
;
int
hookcount
;
lv_Hook
hook
;
TValue
l_gt
;
/* table of globals */
TValue
env
;
/* temporary place for environments */
GCObject
*openupval
;
/* list of open upvalues in this stack */
GCObject
*gclist
;
struct
lv_longjmp
*errorJmp
;
/* current error recover point */
ptrdiff_t
errfunc
;
/* current error handling function (stack index) */
//
void
*
lView
;
}
;
|
这个状态机被进行了更改,并且加入的新元素
对比下原来的
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
|
struct
lua
_
State
{
CommonHeader
;
lu_byte
status
;
StkId
top
;
/* first free slot in the stack */
StkId
base
;
/* base of current function */
global_State
*l_G
;
CallInfo
*ci
;
/* call info for current function */
const
Instruction
*savedpc
;
/* `savedpc' of current function */
StkId
stack_last
;
/* last free slot in the stack */
StkId
stack
;
/* stack base */
CallInfo
*end_ci
;
/* points after end of ci array*/
CallInfo
*base_ci
;
/* array of CallInfo's */
int
stacksize
;
int
size_ci
;
/* size of array `base_ci' */
unsigned
short
nCcalls
;
/* number of nested C calls */
unsigned
short
baseCcalls
;
/* nested C calls when resuming coroutine */
lu_byte
hookmask
;
lu_byte
allowhook
;
int
basehookcount
;
int
hookcount
;
lua_Hook
hook
;
TValue
l_gt
;
/* table of globals */
TValue
env
;
/* temporary place for environments */
GCObject
*openupval
;
/* list of open upvalues in this stack */
GCObject
*gclist
;
struct
lua_longjmp
*errorJmp
;
/* current error recover point */
ptrdiff_t
errfunc
;
/* current error handling function (stack index) */
}
;
|
lvsdk中存在就是很多扩展后的控件,通过编写Lua脚本可以直接调用的原生UI
具体为什么要更改我也不知道,如果你知道了,希望能私信告诉我,如果你想查看源码:看这里Lua源码下载
我刚刚编写了一个简单Lua脚本,并且进行下测试
|
button3
=
Button
(
)
;
button3
.
frame
(
150
,
250
,
100
,
100
)
;
button3
.
image
(
"button0.png"
,
"button1.png"
)
;
button3
.
callback
(
function
(
)
Alert
(
"测试"
)
;
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
29
30
31
32
33
34
35
|
//
// ViewController.m
// luaTest
//
// Created by LastDays on 16/6/7.
// Copyright © 2016年 LastDays. All rights reserved.
//
#import "ViewController.h"
#import <LView.h>
@interface
ViewController
(
)
@property
(
nonatomic
,
strong
)
LView
*lview
;
@end
@implementation
ViewController
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
// Do any additional setup after loading the view, typically from a nib.
CGRect
cg
=
self
.
view
.
bounds
;
cg
.
origin
=
CGPointZero
;
self
.
lview
=
[
[
LView
alloc
]
initWithFrame
:cg
]
;
self
.
lview
.
viewController
=
self
;
[
self
.
view
addSubview
:self
.
lview
]
;
[
self
.
lview
runFile
:
@"lastdays.lua"
]
;
}
-
(
void
)
didReceiveMemoryWarning
{
[
super
didReceiveMemoryWarning
]
;
// Dispose of any resources that can be recreated.
}
@end
|
效果图:

可以看到调用的原生UI。
先来分析
|
self
.
lview
=
[
[
LView
alloc
]
initWithFrame
:cg
]
;
|
在初始化中主要是执行两个方法,我主要挑这其中的主要代码说,就不全贴上来了,如果感兴趣可以下载源码看,其中一个是初始化用于加密解密的rsa以及对脚本资源文件进行管理的bundle
|
-
(
void
)
myInit
{
self
.
rsa
=
[
[
LVRSA
alloc
]
init
]
;
self
.
bundle
=
[
[
LVBundle
alloc
]
init
]
;
}
|
另一个就是:
|
-
(
void
)
registeLibs
{
if
(
!
self
.
stateInited
)
{
self
.
stateInited
=
YES
;
self
.
l
=
lvL_newstate
(
)
;
//lv_open(); /* opens */
lvL_openlibs
(
self
.
l
)
;
[
LVRegisterManager
registryApi
:self
.
l
lView
:self
]
;
self
.
l
->
lView
=
(
__bridge
void
*
)
(
self
)
;
}
}
|
这里我们使用lvL_newstate()函数创建一个新的lua执行环境,但是这个函数中环境里什么都没有,因此需要使用lvL_openlibs(self.l);加载所有的标准库,之后可以使用。所有lua相关的东西都保存在lv_State这个结构中,通过lvL_newstate()创建一个新的 Lua 虚拟机时,第一块申请的内存将用来保存主线程和这个全局状态机。其实我个人感觉这就是一个内置在App中的运行环境,专门运行Lua脚本。
[LVRegisterManager registryApi:self.l lView:self];
这行代码,我就把它理解为扩展,对Lua API的一个扩展。上面我们提到过,使用Lua构建动态化方案的核心就在于将Android,iOS原生的UI、网络、存储、硬件控制等能力桥接到Lua层。他主要就是为了完成这里,在这里注册大量的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
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
|
// 注册函数
+
(
void
)
registryApi
:
(
lv_State
*
)
L
lView
:
(
LView
*
)
lView
{
//清理栈
lv_settop
(
L
,
0
)
;
lv_checkstack
(
L
,
128
)
;
// 注册静态全局方法和常量
[
LVRegisterManager
registryStaticMethod
:L
lView
:lView
]
;
// 注册System对象
[
LVSystem
classDefine
:L
]
;
// 基础数据结构data
[
LVData
classDefine
:L
]
;
[
LVStruct
classDefine
:L
]
;
// 注册UI类
lv_settop
(
L
,
0
)
;
[
LVBaseView
classDefine
:L
]
;
[
LVButton
classDefine
:L
]
;
[
LVImage
classDefine
:L
]
;
[
LVLabel
classDefine
:L
]
;
[
LVScrollView
classDefine
:L
]
;
[
LVTableView
classDefine
:L
]
;
[
LVCollectionView
classDefine
:L
]
;
[
LVPagerView
classDefine
:L
]
;
[
LVTimer
classDefine
:L
]
;
[
LVPagerIndicator
classDefine
:L
]
;
[
LVCustomPanel
classDefine
:L
]
;
[
LVTransform3D
classDefine
:L
]
;
[
LVAnimator
classDefine
:L
]
;
[
LVTextField
classDefine
:L
]
;
[
LVAnimate
classDefine
:L
]
;
[
LVDate
classDefine
:L
]
;
[
LVAlert
classDefine
:L
]
;
// 注册DB
[
LVDB
classDefine
:L
]
;
//清理栈
lv_settop
(
L
,
0
)
;
// 注册手势
[
LVGestureRecognizer
classDefine
:L
]
;
[
LVTapGestureRecognizer
classDefine
:L
]
;
[
LVPinchGestureRecognizer
classDefine
:L
]
;
[
LVRotationGestureRecognizer
classDefine
:L
]
;
[
LVSwipeGestureRecognizer
classDefine
:L
]
;
[
LVLongPressGestureRecognizer
classDefine
:L
]
;
[
LVPanGestureRecognizer
classDefine
:L
]
;
//清理栈
lv_settop
(
L
,
0
)
;
[
LVLoadingIndicator
classDefine
:L
]
;
// http
[
LVHttp
classDefine
:L
]
;
// 文件下载
[
LVDownloader
classDefine
:L
]
;
// 文件
[
LVFile
classDefine
:L
]
;
// 声音播放
[
LVAudioPlayer
classDefine
:L
]
;
// 调试
[
LVDebuger
classDefine
:L
]
;
// attributedString
[
LVStyledString
classDefine
:L
]
;
// 注册 系统对象window
[
LVRegisterManager
registryWindow
:L
lView
:lView
]
;
// 导航栏按钮
[
LVNavigation
classDefine
:L
]
;
//清理栈
lv_settop
(
L
,
0
)
;
//外链注册器
[
LVExternalLinker
classDefine
:L
]
;
//清理栈
lv_settop
(
L
,
0
)
;
return
;
}
|
简单介绍下这两个函数的作用lv_settop(L, 0),lv_checkstack(L, 128),lv_settop(L, 0)设置栈顶索引,即设置栈中元素的个数,如果index<0,则从栈顶往下数,lv_checkstack(L, 128)确保堆栈上至少有 extra 个空位.按照上面注释的解释就是为了做清理栈的工作。
因为这里注册了太多的API,主要是为了弄清原理,那么我们就选择我们脚本中使用的Button来分析。也就是这行代码
|
[
LVButton
classDefine
:L
]
;
|
LVButton继承自UIButton并且遵循LVProtocal协议。看一下classDefine:方法
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
|
+
(
int
)
classDefine
:
(
lv_State
*
)
L
{
{
lv_pushcfunction
(
L
,
lvNewButton
)
;
lv_setglobal
(
L
,
"Button"
)
;
}
const
struct
lvL_reg
memberFunctions
[
]
=
{
{
"image"
,
image
}
,
{
"font"
,
font
}
,
{
"fontSize"
,
fontSize
}
,
{
"textSize"
,
fontSize
}
,
{
"titleColor"
,
titleColor
}
,
{
"title"
,
title
}
,
{
"textColor"
,
titleColor
}
,
{
"text"
,
title
}
,
{
"selected"
,
selected
}
,
{
"enabled"
,
enabled
}
,
//{"showsTouchWhenHighlighted", showsTouchWhenHighlighted},
{
NULL
,
NULL
}
}
;
lv_createClassMetaTable
(
L
,
META_TABLE_UIButton
)
;
lvL_openlib
(
L
,
NULL
,
[
LVBaseView
baseMemberFunctions
]
,
0
)
;
lvL_openlib
(
L
,
NULL
,
memberFunctions
,
0
)
;
const
char
*
keys
[
]
=
{
"addView"
,
NULL
}
;
// 移除多余API
lv_luaTableRemoveKeys
(
L
,
keys
)
;
return
1
;
}
|
其中这段代码:
|
lv_pushcfunction
(
L
,
lvNewButton
)
;
lv_setglobal
(
L
,
"Button"
)
;
|
lvNewButton是一个函数,我们上面说过,我们跟Lua环境的交互主要是通过一个虚拟的栈,lv_pushcfunction(L, lvNewButton)的作用就是将lvNewButton函数压入栈顶,然后使用lv_setglobal(L, “Button”)将栈顶的lvNewButton函数传入Lua环境中作为全局函数。这样就是扩展我们的Lua环境,现在我们就可以编写Lua脚本,通过Button()关键字来调用lvNewButton函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const
struct
lvL_reg
memberFunctions
[
]
=
{
{
"image"
,
image
}
,
{
"font"
,
font
}
,
{
"fontSize"
,
fontSize
}
,
{
"textSize"
,
fontSize
}
,
{
"titleColor"
,
titleColor
}
,
{
"title"
,
title
}
,
{
"textColor"
,
titleColor
}
,
{
"text"
,
title
}
,
{
"selected"
,
selected
}
,
{
"enabled"
,
enabled
}
,
//{"showsTouchWhenHighlighted", showsTouchWhenHighlighted},
{
NULL
,
NULL
}
}
;
|
来看下lvL_Reg的结构体
|
typedef
struct
lvL
_
Reg
{
const
char
*name
;
lv_CFunction
func
;
}
lvL_Reg
;
|
看到该结构体也可以看出,包含name,和func。name就是为了,在注册时用于通知Lua该函数的名字。结构体数组中的最后一个元素的两个字段均为NULL,用于提示Lua注册函数已经到达数组的末尾。
这里我们其实可以理解为为Button添加库,像image,font这些在源码中可以看到,是一些静态的c函数
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static
int
image
(
lv_State
*L
)
{
LVUserDataInfo
*
user
=
(
LVUserDataInfo
*
)
lv_touserdata
(
L
,
1
)
;
if
(
user
)
{
NSString
*
normalImage
=
lv_paramString
(
L
,
2
)
;
// 2
NSString
*
hightLightImage
=
lv_paramString
(
L
,
3
)
;
// 2
//NSString* disableImage = lv_paramString(L, 4);// 2
//NSString* selectedImage = lv_paramString(L, 5);// 2
LVButton
*
button
=
(
__bridge
LVButton
*
)
(
user
->
object
)
;
if
(
[
button
isKindOfClass
:
[
LVButton
class
]
]
)
{
[
button
setImageUrl
:normalImage
placeholder
:nil
state
:UIControlStateNormal
]
;
[
button
setImageUrl
:hightLightImage
placeholder
:nil
state
:UIControlStateHighlighted
]
;
//[button setImageUrl:disableImage placeholder:nil state:UIControlStateDisabled];
//[button setImageUrl:selectedImage placeholder:nil state:UIControlStateSelected];
lv_pushvalue
(
L
,
1
)
;
return
1
;
}
}
return
0
;
}
|
现在可以重新看一下我们原来写的Lua脚本了
|
button
=
Button
(
)
;
button
.
frame
(
150
,
250
,
100
,
100
)
;
button
.
image
(
"button0.png"
,
"button1.png"
)
;
button
.
callback
(
function
(
)
Alert
(
"LatDays"
)
;
end
)
;
|
可以看到我么你的Button,还有image就是我们上面添加的标识。感兴趣可以下载demo做一些更改。就会发现两者是对应的。
我个人理解原因就是像Lua源码解析中说的,global_State 里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数, 有 GC 需要的把所有对象串联起来的相关信息,以及一切 Lua 在工作时需要的工作内存。
UI的扩展我们看完了,现在来分析下Lua脚本文件是如何运行的。
|
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
// Do any additional setup after loading the view, typically from a nib.
CGRect
cg
=
self
.
view
.
bounds
;
cg
.
origin
=
CGPointZero
;
self
.
lview
=
[
[
LView
alloc
]
initWithFrame
:cg
]
;
self
.
lview
.
viewController
=
self
;
[
self
.
view
addSubview
:self
.
lview
]
;
[
self
.
lview
runFile
:
@"lastdays.lua"
]
;
}
|
lview.viewController = self;