LuaView
是阿里聚划算部门为解决业务增长和频繁的业务需求变更而出的一套解决方案,即将部分业务逻辑导入到 lua
中去执行,通过 lua
的动态更新来实现这一需求
前期工程建立:
新建工程
暂不支持 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);
}
}
Android 这边使用的是 LuaJ
第三方库进行 Lua 和 Java 之间的互调用
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());
...
}
...
}
上面的每个 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<U extends UDTextView> extends UIViewMethodMapper<U> {
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
中
根据上面的方法导入,当 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 对象
有前面已经知道了,执行 lua 代码,返回的 label
实际上是一个 userdata
,对应 UDTextView
。
label = Label()
label.text("Hello World LuaView to Android");
上面的 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<U extends UDTextView> extends UIViewMethodMapper<U> {
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<T extends TextView> extends UDView<T> {
...
public UDTextView setText(CharSequence text) {
final T view = getView();
if (view != null) {
view.setText(text);
}
return this;
}
...
我们知道 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: {
...
}
}
}
本文来自网易实践者社区,经作者张云龙授权发布。