Android LuaView 探索(下篇)

达芬奇密码2018-06-26 12:54

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)
  1. 定义一个 View 对应,对应 Android 中的 ViewGroup

  2. 设置 container 的排版代码

     // 子对象按照水平反方向对齐方式排版
     container.flexCss("flex-direction:row-reverse")
    
     // 当需要设置多个值时,使用 ”,“ 隔开 
     container.flexCss("flex-direction:row-reverse,top:10")
    
  3. 设置 container 的排版子对象

     container.flexChildren(label, button)
    

    注意这里仅仅是排版子对象,可以不是 container 的子 view

  4. 查看排版效果


  • container.flexCss("flex-direction:row")

  • container.flexCss("flex-direction:row-reverse")

  • container.flexCss("flex-direction:column")


  1. 其他详细的排版细节请查看开源库 css-layout


2.7 对象生命周期管理


这里我们需要关心的是,lua 语言的执行和 java 代码的执行,那这 2 个语言中的对象如何能保证 2 个语言相互调用的时候,是保证对方的对象是存活着的?因为这 2 个语言都是有 gc 概念的,如何能保证当 lua 对象存在引用的时候,对应的 java 对象是一定也是不能被 gc 的;反之,如何能保证 java 对象存在引用的时候,对应的 lua 对象是一定不能被 gc 的?


2.7.1 lua 对象存在引用的时候,对应的 java 对象如何保证存活


  1. 全局变量

    首先,在 java 层代表 lua 上下文环境的对象是 mGlobals 对象,该对象的类型是 LuaView 的一个成员变量。而 LuaView 被创建之后,被当做一个 View 设置给 Activity 的 contentView。由此,可以知道只要当前 Activity 存活的时候,mGlobals 是一定存活的。

    接着,当执行 lua 代码,假设定义了一个全局变量,如下代码所示:

     a = "lua"
    

    那对应 java 层就会新建一个 LuaValue 类型的变量(LuaValueLuaIntegerLuaTableLuaFunction 等的基类),并调用 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 层 mGlobalsset 方法,将该对象从 mGlobals 中移除,从此,java 层的对象也就失去了引用,jvm 就可以回收它了。

     a = nil
    
  2. 非全局变量

    同理,当我们执行如下 lua 代码时,那 lua 层的 key 变量是保持存活的,那对应的 java 对象是如何保持存活的?同上,对应的 java 层对应的这个变量 (取名为 ja) 是被设置到 t 对应的 java 层对象 (取名为 jt),而 jt 是被保存到 mGlobals 中的,所以这里全局变量里面的值也都是存活的

     t = {}
     local a = "XX"
     t.key = a
    
  3. 临时普通变量(非 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
    

    内部会调用 LuaViewloadFileInternal 方法:

     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 变量能被回收么?


  1. 根据如下代码,同时根据上面控件的创建过程的分析,当一个 UI 控件被创建的时候,java 层会默认将该控件添加到 LuaView (对应lua 层的 window 全局变量) 中,那么当代码执行出了 {} 之后,那控件会被释放么?

     {
         local button = Button();
     }
     ...省略代码
    

    我们可以理解 button 变量只能在 {} 里面访问,当出了 {},是不是就应该被回收了?然而其对应的 java 对象还被 LuaView 持有,因此此时,对应的 java 对象是不能被回收的。不过,因为 {} 外面的代码无法访问 button 变量,因此不管 lua 层是不是回收了 button 值,也不会产生什么问题。

  2. 另外,当按钮被点击的时候,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 为例):


  1. 自定义 LVTextView,继承自 TextView,实现 ILVView

  2. 自定义 UDTextView,继承自 UDView<T>,里面实现需要导入方法的各种实现,如setText,getText等。

  3. 自定义 UITextViewMethodMapper 继承自 UIViewMethodMapper,里面实现导入方法的各种实现,如setText,getText等,其中里面调用至 UDTextView 中的方法。

  4. LuaViewManager.loadLuaViewLibs 方法中添加注册方法

     globals.load(new UITextViewBinder());
    


4. 性能


  1. lua 调用 java 方法,通过静态 binding 方式,因此性能相比动态 binding 方式会好些。然而 LuaJ 是一个 java 实现的 Lua 解释器,因此性能比起优化过的 LuaBridge 还是会差一些

  2. ActivityonCreate 中需要完成全部的初始化,而每个类的初始化,需要将通过反射获取类全部的方法,并导入 globals 中。因此初始化非常耗时,一次初始化并执行 hello.lua 中的方法,总共花费 2.831s

  3. 如果第二个页面同样需要使用 LuaView,则同样需要执行一次初始化。不过第二次执行的时候,相关反射的方法在 JVM 中会做了相关缓存,则执行速度会快不少


5. 小结


  1. SDK 接入工程简单

  2. 使用的 LuaJ 是一个 java 实现的 lua 解释器,lua 层的对象和对应 java 层的对象,是公用一份内存,所以并不存在 2 个语言中,生命周期不一致产生的问题

  3. 导入控件的方法,较为繁琐,需要同时实现 LVMyViewUIMyViewMethodMapperUDMyView,并且重新写各种需要导入的接口

  4. 接口调用时性能较好,但初始化时性能较差

  5. 并没有将 Activity 的概念引入 lua 中,因此只能实现 LuaView 内容的热更新,但并不能热更新和 Android 接口相关的热更新(需要专门将相关导入lua中),并不能热更新展示页面 (Activity)的数量

  6. 不同页面中使用 LuaView 时,需要重新初始化,新构建 lua 环境。

  7. 第一次初始化性能极差,第二次性能较好

  8. 相关 lua 层,并没有做进一步封装,因此在 lua 层能做的一些设计这里并没有,如 class、mixin、Disposable 等机制

  9. 引入了 facebook.csslayout 的排版机制,排版功能同 css 的排版

  10. 导入的控件数量较少,不够全面

  11. lua 层定义的 UI 控件会默认加载到 window (java 层 LuaView),如果需要定义一个没有 parent 的控件,需要在定义该控件之后,执行 removeFromParent 方法。这一点和常见的 iOS 和 Android 等 GUI 系统的概念有些不一致,用起来较为怪异

    label = Label()
    label.removeFromParent()
    
  12. 没有主动调用 removeFromParent 方法的控件将一直被持有

相关阅读:Android LuaView 探索(上篇)

本文来自网易实践者社区,经作者张云龙授权发布。