0. 简介
LuaView
是阿里聚划算部门为解决业务增长和频繁的业务需求变更而出的一套解决方案,即将部分业务逻辑导入到 lua
中去执行,通过 lua
的动态更新来实现这一需求
1. 基本使用
前期工程建立,可以参照 LuaViewSDK 完全新手教程(Android)
-
新建工程
暂不支持
android sdk 23 (6.0)
,需要将compileSdkVersion
和targetSdkVersion
都修改成小于 23 -
在 gradle 中引入 sdk
导入 LuaViewSDK,并在
build.gradle
中添加工程引用:dependencies { compile project(':LuaViewSDK') }
-
在
assets
中添加 lua 代码(这个代码也可以是服务器中下发)如新建一个
hello.lua
w, h = System.screenSize(); window.frame(0, 0, w, h); window.backgroundColor(0xDDDDDD); label = Label(); label.frame(0, 50, w, 60); label.text("Hello World LuaView to Android");
-
在
Activity
中添加代码public class LuaActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LuaView view = LuaView.create(this); view.load("hello.lua"); // 从 assets 中找到 lua 代码,并执行 setContentView(view); } }
2. 源码解析
Android 这边使用的是 LuaJ
第三方库进行 Lua 和 Java 之间的互调用
2.1 初始化
LuaView view = LuaView.create(this);
应用层代码中调用上面这句代码,分别执行了如下操作:
LuaView 模块的初始化工作,初始化分辨率常量、lua 文件存放路径等
在 Java 中创建一个 Globals 类型的变量(代表代码 lua 的上下文环境,也是一个 LuaTable 类型的变量),将全部的控件、Http、System 等设置到 Globals 中,即导入 lua 环境中
创建了一个
LuaView
对象 (也是 ViewGroup 类型),对应 lua 脚本中的window
,并返回
查看导入用户的 lib 的部分源码,如下:
public class LuaViewManager {
public static void loadLuaViewLibs(final Globals globals) {
//ui
globals.load(new UITextViewBinder());
globals.load(new UIEditTextBinder());
...
//animation
globals.load(new UIAnimatorBinder());
//net
globals.load(new HttpBinder());
//kit
globals.load(new TimerBinder());
globals.load(new SystemBinder());
...
//常量
globals.load(new AlignBinder());
...
}
...
}
2.2 导入相关类的方法到 lua 环境中
上面的每个 XXXXBinder
对象都派生自 BaseFunctionBinder
,都需要实现 2 个方法
@Override
public Class extends LibFunction> getMapperClass() {
...
}
@Override
public LuaValue createCreator(LuaValue env, LuaValue metaTable) {
...
}
以 UITextViewBinder
为例:
@Override
public Class extends LibFunction> getMapperClass() {
return UITextViewMethodMapper.class;
}
@Override
public LuaValue createCreator(LuaValue env, LuaValue metaTable) {
return new BaseVarArgUICreator(env.checkglobals(), metaTable) {
@Override
public ILVView createView(Globals globals, LuaValue metaTable, Varargs varargs) {
return new LVTextView(globals, metaTable, varargs);
}
};
}
UITextViewMethodMapper
类中的部分代码如下:
public class UITextViewMethodMapper extends UIViewMethodMapper {
public LuaValue text(U view, Varargs varargs) {
if (varargs.narg() > 1) {
return setText(view, varargs);
} else {
return getText(view, varargs);
}
}
public LuaValue setText(U view, Varargs varargs) {
final CharSequence text = LuaViewUtil.getText(varargs.optvalue(2, NIL));
return view.setText(text);
}
public LuaValue getText(U view, Varargs varargs) {
return valueOf(String.valueOf(view.getText()));
}
...
}
还是以 UITextViewBinder
为例,加载的时候
globals.load(new UITextViewBinder());
最终会调用 BaseFunctionBinder
中的 call
方法
private LuaValue call(LuaValue env, Class extends LibFunction> libClass) {
LuaTable methodMapper = LuaViewManager.bind(libClass, getMapperMethods(libClass));
if (luaNames != null) {
for (String name : luaNames) {
env.set(name, createCreator(env, addNewIndex(methodMapper)));
}
}
return methodMapper;
}
参数
env
即前面的globals
变量,代表 lua 的上下文环境;
参数
libClass
即UITextViewBinder
的getMapperClass
方法调用返回的值
上面的代码主要做了这几件事情,如下:
获取 libClass 中的全部公有方法
构建一个
LuaTable
对象,将上面获取的全部方法,方法名为key
,方法本身为value
,设置到LuaTable
中以
Label
为key
,UITextViewBinder
中的方法createCreator
执行返回的匿名类对象BaseVarArgUICreator
为value
,设置到globals
中
2.3 UI 控件的创建
根据上面的方法导入,当 lua 脚本中执行方法时
label = Label();
这里 Label()
方法,就会执行 BaseVarArgUICreator.invoke(Varargs args)
方法:
public abstract class BaseVarArgUICreator extends VarArgFunction {
...
public Varargs invoke(Varargs args) {
ILVView view = createView(globals, metatable, args);
if (globals.container instanceof ViewGroup && view instanceof View && ((View) view).getParent() == null) {
globals.container.addLVView((View) view, args);
}
return view.getUserdata();
}
...
}
@Override
public ILVView createView(Globals globals, LuaValue metaTable, Varargs varargs) {
return new LVTextView(globals, metaTable, varargs);
}
这里方法调用主要做了 3 件事情:
对应 lua 层的
Label()
方法,这里会执行会执行对应的createView
的函数,创建一个LVTextView
对象判断当前的创建的 view 是否有 parent,并且当前 globals.container 是否是
ViewGroup
。这里的这句示例代码满足条件,就会把当前创建的 view 添加到 globals.container,即添加到LuaView
中返回
LVTextView
的luaUserData
(LVTextView
的luaUserData
成员变量的具体类型是UDTextView
)其他控件的创建,主要是第 1 步调用对应的
createView
方法创建对应的控件,最后返回对应的luaUserData
,其他逻辑和LVTextView
一致
注:这里
UDTextView
并不是一个 View,而是持有了一个 View 对象,相关的设置接口都会作用到这个 View 对象
2.4 UI 控件的方法调用
有前面已经知道了,执行 lua 代码,返回的 label
实际上是一个 userdata
,对应 UDTextView
。
label = Label()
label.text("Hello World LuaView to Android");
2.4.1 方法是如何调用到对应控件中的方法
上面的 lua 脚本,但执行 label.text("XXX")
,那是如何调用到 java 的 LVTextView.setText("XXX")
方法的?
还记得前面的导入相关类的方法到 lua 环境的过程么?导入过程中,会根据 UITextViewMethodMapper
中的全部公有方法构建了一个 LuaTable
,然后在设置 luaUserData
变量(UDTextView
类型的变量,也是一个 LuaValue
类型,对应 lua 代码中的 label
变量)的时候,会把构建的 LuaTable
当做 metatable
设置给 luaUserData
变量。所以对 lua 中的 label 变量调用方法,都会调用到 UITextViewMethodMapper
中的方法。
比如,lua 代码执行如下
label.text("Hello World LuaView to Android");
则会调用 java 层的 VarArgFunction
的 call
方法,最终调用 method.invoke(this, getUD(args), args)
方法,最后调用下面的方法,
UITextViewMethodMapper.java
public class UITextViewMethodMapper extends UIViewMethodMapper {
public LuaValue text(U view, Varargs varargs) {
if (varargs.narg() > 1) {
return setText(view, varargs);
}
...
}
public LuaValue setText(U view, Varargs varargs) {
final CharSequence text = LuaViewUtil.getText(varargs.optvalue(2, NIL));
return view.setText(text);
}
...
view.setText(text) 则会执行 UDTextView
的 setText
方法,最终会调用 LVTextView
的 setText
方法,完成了设置控件文本的任务。
public class UDTextView extends UDView {
...
public UDTextView setText(CharSequence text) {
final T view = getView();
if (view != null) {
view.setText(text);
}
return this;
}
...
2.4.2 参数是如何传递并转换过去的?
我们知道 lua 脚本语言并不是强类型的,一个变量既可以被设置为数值,也可以被设置为字符串;而 java 语言是强类型的,一个变量的声明必须指明是什么类型的,如一个 int 类型的变量并不能被设置为字符串,如下的代码是编译不过的:
int a = "string";
那么如果 lua 脚本中执行了一个方法,方法中传递一个变量(假设变量的值是一个整型数字),那最后是如何转化成 java 中的 int 参数的?
button.backgroundColor(15654382) -- 15654382 等于 0xeeDDee
比如执行上面这一段 lua 脚本,根据上面的方法如何调用的介绍,我们可以找到最后会调用 java 层的方法 LVButton.setBackgroundColor(int color)
,那这里的 lua 中的 15654382
是如何转化成 java 中的 int
变量的?
查看 LuaJ
源码可以发现,里面定义了 LuaInteger
、LuaString
等类,这些类的基类都是 LuaValue
。我们可以猜测 lua 中的整数对应 LuaInteger
、浮点数对应 LuaDouble
、字符串对应 LuaString
等
继续跟踪下 LuaJ
解析加载 lua 脚本的代码
public class LuaC extends Lua
implements Globals.Compiler, Globals.Loader {
...
public Prototype compile(InputStream stream, String chunkname)
throws IOException {
return (new LuaC(new Hashtable())).luaY_parser(stream, chunkname);
}
...
}
中间调用过程省略,直接看到 LexState.java
,这里可以看到当发现 "(" 符号的时候,开始处理后面读取的参数,具体的处理函数是 this.next
。
void funcargs(expdesc f, int line) {
...
switch (this.t.token) {
case '(': { /* funcargs -> `(' [ explist1 ] `)' */
this.next();
if (this.t.token == ')') /* arg list is empty? */
args.k = VVOID;
else {
this.explist(args);
fs.setmultret(args);
}
this.check_match(')', '(', line);
break;
}
...
}
...
}
最后跟踪到 LexState
的 int llex(SemInfo)
方法,可以发现,lua
文件中传入的参数是在这里被转化成对应 LuaValue
类型的变量。下面省略了大量的代码,仅仅留下生成数值类型的代码,在函数 read_numeral(seminfo);
里面将对应读入的内容转化成 LuaInteger
或者 LuaDouble
类型的数据添加到 seminfo
对象里面
int llex(SemInfo seminfo) {
nbuff = 0;
while (true) {
switch (current) {
...
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': {
read_numeral(seminfo);
return TK_NUMBER;
}
...
default: {
...
}
}
}
2.5 控件监听实现
简单的,以 Button
的点击事件为例
count = 0;
button = Button();
button.frame(10, 50, w, 60);
button.title("按钮");
button.callback(function()
count = count + 1
button.title("点击 " .. count .. " 次");
end)
这里添加监听会调用 callback
方法,对应的 UIViewMethodMapper.callback
方法
public LuaValue callback(U view, Varargs varargs) {
if (varargs.narg() > 1) {
return setCallback(view, varargs);
} else {
return getCallback(view, varargs);
}
}
public LuaValue setCallback(U view, Varargs varargs) {
final LuaValue callbacks = varargs.optvalue(2, NIL);
return view.setCallback(callbacks);
}
继续查看 UDView.setCallback(final LuaValue callbacks)
public UDView setCallback(final LuaValue callbacks) {
this.mCallback = callbacks;
if (this.mCallback != null) {
mOnClick = mCallback.isfunction() ? mCallback : LuaUtil.getFunction(mCallback, "onClick", "Click", "OnClick", "click");
...
//setup listener
setOnClickListener();
...
}
return this;
}
public UDView setOnClickCallback(final LuaValue callback) {
this.mOnClick = callback;
setOnClickListener();
return this;
}
private void setOnClickListener() {
if (LuaUtil.isValid(this.mOnClick)) {
final T view = getView();
if (view != null) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callOnClick();
}
});
}
}
}
public LuaValue callOnClick() {
return LuaUtil.callFunction(this.mOnClick);
}
由上可以看到 lua 中设置 callback 时传入的 function
保存到了 mOnClick
上面,同时设置了 onClickListener
到 view 上,当点击的时候,会去执行 mOnClick 中对应的 lua 方法。
2.6 排版实现
先查看下 lua 层的应用:
local w,h = System.screenSize();
window.frame(0, 0, w, h);
window.backgroundColor(0xDDDDDD);
container = View()
container.frame(0, 0, w, h);
container.flexCss("flex-direction:row-reverse")
local label = Label();
label.frame(0, 50, 100, 60);
label.text("Hello World LuaView to Android");
button = Button();
button.frame(10, 50, 100, 60);
button.backgroundColor(0xeeDDee);
button.title("按钮");
container.flexChildren(label, button)
定义一个 View 对应,对应 Android 中的 ViewGroup
-
设置 container 的排版代码
// 子对象按照水平反方向对齐方式排版 container.flexCss("flex-direction:row-reverse")
// 当需要设置多个值时,使用 ”,“ 隔开 container.flexCss("flex-direction:row-reverse,top:10")
-
设置 container 的排版子对象
container.flexChildren(label, button)
注意这里仅仅是排版子对象,可以不是 container 的子 view
查看排版效果
- container.flexCss("flex-direction:row")
- container.flexCss("flex-direction:row-reverse")
- container.flexCss("flex-direction:column")
- 其他详细的排版细节请查看开源库 css-layout
2.7 对象生命周期管理
这里我们需要关心的是,lua 语言的执行和 java 代码的执行,那这 2 个语言中的对象如何能保证 2 个语言相互调用的时候,是保证对方的对象是存活着的?因为这 2 个语言都是有 gc
概念的,如何能保证当 lua 对象存在引用的时候,对应的 java 对象是一定也是不能被 gc
的;反之,如何能保证 java 对象存在引用的时候,对应的 lua 对象是一定不能被 gc
的?
2.7.1 lua 对象存在引用的时候,对应的 java 对象如何保证存活
-
全局变量
首先,在 java 层代表 lua 上下文环境的对象是
mGlobals
对象,该对象的类型是LuaView
的一个成员变量。而LuaView
被创建之后,被当做一个 View 设置给 Activity 的 contentView。由此,可以知道只要当前 Activity 存活的时候,mGlobals
是一定存活的。接着,当执行 lua 代码,假设定义了一个全局变量,如下代码所示:
a = "lua"
那对应 java 层就会新建一个
LuaValue
类型的变量(LuaValue
是LuaInteger
、LuaTable
、LuaFunction
等的基类),并调用mGlobals.set
方法,将 java 层对象保存到mGlobals
中。由此,只要 lua 层的全局变量存在引用,那对应的 java 层对象就一定释放不掉。这里可以将
mGlobals
理解成一个HashTable
。public class LuaTable extends LuaValue implements Metatable { ... public void set( LuaValue key, LuaValue value ) { if (!key.isvalidkey() && !metatag(NEWINDEX).isfunction()) typerror("table index"); if ( m_metatable==null || ! rawget(key).isnil() || ! settable(this,key,value) ) rawset(key, value); } ... }
当在 lua 层将全局变量设置为空,如下所示。就会执行 java 层
mGlobals
的set
方法,将该对象从mGlobals
中移除,从此,java 层的对象也就失去了引用,jvm 就可以回收它了。a = nil
-
非全局变量
同理,当我们执行如下 lua 代码时,那 lua 层的
key
变量是�保持存活的,那对应的 java 对象是如何保持存活的?同上,对应的 java 层对应的这个变量 (取名为ja
) 是被设置到t
对应的 java 层对象 (取名为jt
),而jt
是被保存到mGlobals
中的,所以这里全局变量里面的值也都是存活的t = {} local a = "XX" t.key = a
-
临时普通变量(非 UI 控件)
当如下执行 lua 代码时,那 java 层对应 lua 层
a
的变量 (取名为ja
) 是如何保持存活的?local a = {} System.gc() a.b = "XXX"
这里,生成的
ja
并没有被保存到mGlobals
中。然而可以发现,ja
在生成之后是被保存到LuaClosure
中的p.k
当中,见下面的代码。public class LuaClosure extends LuaFunction { ... public final Prototype p; ... }
public class Prototype { ... public LuaValue[] k; ... } }
我们可以将 lua 文件中的 全部代码理解为一个
main
方法调用,那该方法就可以理解成一个最外层的LuaClosure
;lua 代码中{}
会对应生成一个新的LuaClosure
;同样一个 lua 方法定义也是一个LuaClosure
。由此可以将 lua 文件的全部代码理解为一个由LuaClosure
相互嵌套形成的一个树状结构。当 java 层加载 lua 文件时,执行流程如下:
luaView.load("hello.lua"); // luaView 的类型是 LuaView
内部会调用
LuaView
的loadFileInternal
方法:private LuaView loadFileInternal(final String luaFileName) { ... final LuaValue activity = CoerceJavaToLua.coerce(getContext()); final LuaValue viewObj = CoerceJavaToLua.coerce(this); mGlobals.loadfile(luaFileName).call(activity, viewObj); ... }
这里
mGlobals.loadfile(luaFileName)
返回了一个LuaClosure
对象,即 lua 上下文环境最外层的luaClosure
。而前面对 lua 代码的解析调用过程,全部都是在LuaClosure.call(activity, viewObj)
方法内执行,因此 lua 层代码在解析执行的时候,这个最外层的LuaClosure
对象是不会被释放的,因为该对象的方法执行还没有退出。因此,直接或者间接挂载在最外层的LuaClosure
的对象是不会被释放的,因为它的引用一定是被持有的。改变 lua 代码,为下面所示,当 lua 代码执行到最后一行的时候,根据 lua 的语法,这里
a
是要被释放的,那对应的 java 对象呢?同上的过程,我们发现,当代码执行到最后一行代码的时候,里面{}
对应的LuaClosure
已经从最外层的LuaClosure
移除,因此内层的LuaClosure
就可以被回收了,那挂载在上面的ja
(对应 lua 层变量a
) 也会被回收了。{ local a = {} System.gc() a.b = "XXX" } local b = {}
2.7.2 java 层对象被持有,lua 变量能被回收么?
-
根据如下代码,同时根据上面控件的创建过程的分析,当一个 UI 控件被创建的时候,java 层会默认将该控件添加到
LuaView
(对应lua 层的window
全局变量) 中,那么当代码执行出了{}
之后,那控件会被释放么?{ local button = Button(); } ...省略代码
我们可以理解
button
变量只能在{}
里面访问,当出了{}
,是不是就应该被回收了?然而其对应的 java 对象还被LuaView
持有,因此此时,对应的 java 对象是不能被回收的。不过,因为{}
外面的代码无法访问button
变量,因此不管 lua 层是不是回收了button
值,也不会产生什么问题。 -
另外,当按钮被点击的时候,lua 层临时变量
myCallback
还能被执行么?{ button = Button(); local myCallback = function() System.gc() end button.callback(myCallback) } ...省略代码
当点击发生的时候,按照常理,lua 代码执行已经出了
{}
,那myCallBack
按理就应该被释放了。而myCallback
在 java 层对应的对象(类型是LuaFunction
,也同样可以理解为一个LuaClosure
)已经被button
对应的控件持有了,所以,java 层的对象是不能被回收的,当我们执行的点击事件的时候,会执行myCallback
方法。那假设myCallback
已经被 lua gc 掉了,那是不是会出现问题?我们发现,
Luaj
是一个 Java 的 Lua 解释器。所以,所有 lua 层的对象对应的内存,其实都是保存在 jvm 的内存中,lua 层调用System.gc
其实最终还是调用的是 java 层的System.gc
,即可以理解为,java 层的对象和 lua 层的对象,其实是对应同一份内存。所以,只要 java 层对象不被释放,那 lua 层的对象的内存也是不被释放的。
3. 扩展性
若需要新导入一个 Android
控件到 lua 中,则需要做如下内容 (以 TextView
为例):
自定义
LVTextView
,继承自TextView
,实现ILVView
自定义
UDTextView
,继承自UDView
,里面实现需要导入方法的各种实现,如setText,getText等。自定义
UITextViewMethodMapper
继承自UIViewMethodMapper
,里面实现导入方法的各种实现,如setText,getText等,其中里面调用至UDTextView
中的方法。-
在
LuaViewManager.loadLuaViewLibs
方法中添加注册方法globals.load(new UITextViewBinder());
4. 性能
lua
调用java
方法,通过静态binding
方式,因此性能相比动态binding
方式会更好在
Activity
的onCreate
中需要完成全部的初始化,而每个类的初始化,需要将通过反射获取类全部的方法,并导入 globals 中。因此初始化非常耗时,一次初始化并执行hello.lua
中的方法,总共花费 2.831s如果第二个页面同样需要使用 LuaView,则同样需要执行一次初始化。不过第二次执行的时候,相关反射的方法在 JVM 中会做了相关缓存,则执行速度会快不少
5. 小结
SDK 接入工程简单
使用的
LuaJ
是一个 java 实现的 lua 解释器,lua 层的对象和对应 java 层的对象,是公用一份内存,所以并不存在 2 个语言中,生命周期不一致产生的问题导入控件的方法,较为繁琐,需要同时实现
LVMyView
、UIMyViewMethodMapper
、UDMyView
,并且重新写各种需要导入的接口接口调用时性能较好,但初始化时性能较差
并没有将
Activity
的概念引入 lua 中,因此只能实现LuaView
内容的热更新,但并不能热更新和 Android 接口相关的热更新(需要专门将相关导入lua中),并不能热更新展示页面 (Activity)的数量不同页面中使用 LuaView 时,需要重新初始化,新构建 lua 环境。
第一次初始化性能极差,第二次性能较好
相关 lua 层,并没有做进一步封装,因此在 lua 层能做的一些设计,如 class、mixin、Disposable 等机制
引入了
facebook.csslayout
的排版机制,排版功能同 css 的排版导入的控件数量较少,不够全面
-
lua 层定义的 UI 控件会默认加载到
window
(java 层LuaView
),如果需要定义一个没有 parent 的控件,需要在定义该控件之后,执行removeFromParent
方法。这一点和常见的 iOS 和 Android 等 GUI 系统的概念有些不一致,用起来较为怪异label = Label() label.removeFromParent()
没有主动调用
removeFromParent
方法的控件将一直被持有