简单的,以 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 方法。
先查看下 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")
这里我们需要关心的是,lua 语言的执行和 java 代码的执行,那这 2 个语言中的对象如何能保证 2 个语言相互调用的时候,是保证对方的对象是存活着的?因为这 2 个语言都是有 gc
概念的,如何能保证当 lua 对象存在引用的时候,对应的 java 对象是一定也是不能被 gc
的;反之,如何能保证 java 对象存在引用的时候,对应的 lua 对象是一定不能被 gc
的?
全局变量
首先,在 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 = {}
根据如下代码,同时根据上面控件的创建过程的分析,当一个 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 层的对象的内存也是不被释放的。
若需要新导入一个 Android
控件到 lua 中,则需要做如下内容 (以 TextView
为例):
自定义 LVTextView
,继承自 TextView
,实现 ILVView
自定义 UDTextView
,继承自 UDView<T>
,里面实现需要导入方法的各种实现,如setText,getText等。
自定义 UITextViewMethodMapper
继承自 UIViewMethodMapper
,里面实现导入方法的各种实现,如setText,getText等,其中里面调用至 UDTextView
中的方法。
在 LuaViewManager.loadLuaViewLibs
方法中添加注册方法
globals.load(new UITextViewBinder());
lua
调用 java
方法,通过静态 binding
方式,因此性能相比动态 binding
方式会好些。然而 LuaJ
是一个 java 实现的 Lua 解释器,因此性能比起优化过的 LuaBridge
还是会差一些
在 Activity
的 onCreate
中需要完成全部的初始化,而每个类的初始化,需要将通过反射获取类全部的方法,并导入 globals 中。因此初始化非常耗时,一次初始化并执行 hello.lua
中的方法,总共花费 2.831s
如果第二个页面同样需要使用 LuaView,则同样需要执行一次初始化。不过第二次执行的时候,相关反射的方法在 JVM 中会做了相关缓存,则执行速度会快不少
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
方法的控件将一直被持有
本文来自网易实践者社区,经作者张云龙授权发布。