Android LuaView 探索(上篇)

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

0. 简介


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


1. 基本使用


前期工程建立:


  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<? 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 的上下文环境;

参数 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<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) 则会执行 UDTextViewsetText 方法,最终会调用 LVTextViewsetText 方法,完成了设置控件文本的任务。


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;
    }
    ...

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: {
                ...
            }
    }
}

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

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