Android LuaView 探索

0. 简介

LuaView 是阿里聚划算部门为解决业务增长和频繁的业务需求变更而出的一套解决方案,即将部分业务逻辑导入到 lua 中去执行,通过 lua 的动态更新来实现这一需求

1. 基本使用

前期工程建立,可以参照 LuaViewSDK 完全新手教程(Android)

  1. 新建工程

    暂不支持 android sdk 23 (6.0),需要将 compileSdkVersiontargetSdkVersion 都修改成小于 23

  2. 在 gradle 中引入 sdk

    导入 LuaViewSDK,并在 build.gradle 中添加工程引用:

    dependencies {
        compile project(':LuaViewSDK')
    }
    
  3. 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");
    
  4. 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 getMapperClass() {
    ...
}

@Override
public LuaValue createCreator(LuaValue env, LuaValue metaTable) {
    ...
}

UITextViewBinder 为例:

@Override
public Class 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 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 的上下文环境;

参数 libClassUITextViewBindergetMapperClass 方法调用返回的值

上面的代码主要做了这几件事情,如下:

  • 获取 libClass 中的全部公有方法

  • 构建一个 LuaTable 对象,将上面获取的全部方法,方法名为 key,方法本身为 value,设置到 LuaTable

  • LabelkeyUITextViewBinder 中的方法 createCreator 执行返回的匿名类对象 BaseVarArgUICreatorvalue,设置到 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 件事情:

  1. 对应 lua 层的 Label() 方法,这里会执行会执行对应的 createView 的函数,创建一个 LVTextView 对象

  2. 判断当前的创建的 view 是否有 parent,并且当前 globals.container 是否是 ViewGroup。这里的这句示例代码满足条件,就会把当前创建的 view 添加到 globals.container,即添加到 LuaView

  3. 返回 LVTextViewluaUserData (LVTextViewluaUserData 成员变量的具体类型是 UDTextView)

  4. 其他控件的创建,主要是第 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 层的 VarArgFunctioncall 方法,最终调用 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) 则会执行 UDTextViewsetText 方法,最终会调用 LVTextViewsetText 方法,完成了设置控件文本的任务。

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 源码可以发现,里面定义了 LuaIntegerLuaString 等类,这些类的基类都是 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;
        }
        ...
    }
    ...
}

最后跟踪到 LexStateint 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)
  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")
Alt pic
  • container.flexCss("flex-direction:row-reverse")
Android LuaView 探索_第1张图片
Alt pic
  • container.flexCss("flex-direction:column")
Android LuaView 探索_第2张图片
Alt pic
  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,里面实现需要导入方法的各种实现,如setText,getText等。

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

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

    globals.load(new UITextViewBinder());
    

4. 性能

  1. lua 调用 java 方法,通过静态 binding 方式,因此性能相比动态 binding 方式会更好

  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 探索)