LuaView初识

作者: 小千 | 来源:发表于2017-06-26 17:39 被阅读1102次

    前言

    作为一名iOS开发工程师,App的动态化是一种趋势,毕竟需求的增多,频繁的提交版本、更新版本对用户体验上肯定会有影响。当然动态化的方案有很多种:RN,Weex,LuaView等。对于一个对H5、React 零基础的小白,我准备还是从LuaView入手。最后还想说一句,没想到在简书写的第一篇文章是关于LuaView的。好吧,我承认我比较懒!

    什么是LuaView?

    LuaView是一种运行在一个ViewController/Activity中,可以灵活加载Lua脚本,并能够按照Native的方式运行的一种面向业务的开发技术方案。

    LuaViewSDK使用lua虚拟机进行脚本解析,通过构建lua与native之间的一系列基础bridge功能,从另一个角度实现了动态化的native能力。

    而对于为何选用Lua,其最大的优势就是:lua语法精炼直观,lua虚拟机轻量高效,使用Native编程模式,Native开发人员容易上手。

    以上很不要脸的取自其官方文档的描述: https://alibaba.github.io/LuaViewSDK/guide.html

    LuaViewSDK 是阿里开源的一个实现动态化方案的框架。开源地址: https://github.com/alibaba/LuaViewSDK

    目前其SDK由阿里的一个团队来维护。个人感觉推广力没有Weex高。官方文档也很久没有更新了。不过提供了一个官方技术交流群:539262083 。

    LuaViewSDK的整体架构

    上图是LuaViewSDK的架构:(由下往上)

    Native  & Framework :表示了Android、iOS及其对应的框架层。

    Lua Engine:即Lua虚拟机,Android对应LuaJ,iOS对应LuaC。作为lua脚本和nati语言之间的桥梁,将lua脚本翻译成native能够识别的目标语言。

    Lua-Native UI Lib:LuaView的核心组件。其实LuaView对Native的各种UI组件进行了再次封装,并且注册到了Lua环境中,Lua脚本可以直接创建和操作这些组件,来达到创建和控制Native组件。(其实查看SDK源码,会发现,不仅封装了UI组件,还有一些方法类,如Timer,Gesture等)。

    Script Manager:Lua脚本管理器,用于脚本的解压、验证、加解密、解压缩等工作。

    Security:Lua脚本的校验工作(完整性和安全性的校验)。

    Lua Script & Lua UI Lib:Lua 业务脚本以及 Lua 层的 UI 库。

    LuaView的基本用法

    LuaView

    第一种方式,直接创建LuaView对象,添加到你想渲染的View上,运行脚本进行界面渲染。

    //1、创建LuaView,LView为LuaView子类(SDK封装的)

    self.lv= [[LView alloc]initWithFrame:lvRect];

    self.lv.viewController= self;

    [self.view addSubview:self.lv];

    //2. 加载并运行脚本

    [self.lv runFile:scriptFileName];

    ....

    //3、LuaView对象被回收之前必须清理内存

    [luaview releaseLuaView];

    第二种方式,创建LViewController的控制器对象,其属性 lv 就是一个LuaView 对象,故运行脚本一样实现了界面渲染。其已经做好了各种生命周期和内存管理的处理,所以不用主动去释放。

    //1. 创建LuaView VC

    LViewController*luaVC=[[LViewController alloc]init];

    //2. 加载并运行脚本

    [luaVC.lv runFile:scriptFileName];

    此处遇到一坑:

    由于我初次使用lua,对其语法不熟,自己创建demo运行脚本时,用了别人写的一个简单demo的脚本:绘制一个label。可是运行后,发现没报错,但是也没绘制,界面白板,也没用返回错误提示。百思不得其解!最后对比了下别人 demo 和我的 demo 的 LuaViewSDK,发现版本不一致,别人的是 2.5.xx.x,而我的版本是0.5.1(最新的)。而原先 LuaViewSDK 语法和 lua 标准语法有区别 :‘.’ 和 ':' 互换了。最新SDK支持的lua标准语法(冒号调用方法,点调用属性),所以我用最新SDK 运行原来语法写成的脚本,是有问题的,语法不一致。最新的SDK中,LuaView 的子类 LView 有一属性 changeGrammar(默认为NO),设置为YES会进行语法转换。若新SDK 运行老语法的lua脚本,则需要将此属性设置为 YES 。

    而由于我项目中既有自己使用lua标准语法写的脚本,也有从别人demo拷贝过来的老语法lua脚本。故我想当然的讲 changeGrammar 设置为 YES,结果发现老语法lua脚本正常渲染界面,标准语法脚本却渲染失败,没有错误提示,白板。后来发现 changeGrammar 设置为YES,并非将lua语法转换成标准语法,而是遍历脚本后,将 ‘.’ 和 ':' 进行互换,所以标准语法写的lua脚本又被转换了。

    所以标准语法的lua脚本,changeGrammar 千万别设置为 YES。

    LuaViewCore

    LuaViewCore其实就是Lua的虚拟机,负责实现了Lua脚本到Native语言的映射。查看LuaView.h/m源码,会发现LuaView初始化时,会创建一个 LuaViewCore 的对象,即一个 LuaView 对应一个 LuaViewCore。

    当业务需要要求一个页面有多个子View都需要lua控制渲染时,若通过创建多个LuaView方式来渲染,则会创建多个LuaViewCore,这样或多或少会影响性能。那么如何实现共享一个Lua虚拟机,即共享LuaViewCore,来渲染多个界面。

    //1、初始化LuaViewCore

    self.lvCore = [[LuaViewCore alloc]init];

    //2、运行脚本

    [self.lvCore runFile:@”luaName.lua”];

    //    [self.lvCore loadFile:@”luaName.lua”];

    //3、调用脚本里的方法 topViewUI/bottomViewUI ,在指定的 self.topView/self.bottomView 进行UI渲染

    //str:成功则返回nil,失败则返回失败原因

    NSString *str0 = [self.lvCore callLua:@"topViewUI" environment:self.topView args:nil];

    NSLog(@"%@",str0?str0:@"topViewUI-sucessed");

    NSString *str1 = [self.lvCore callLua:@"bottomViewUI" environment:self.bottomView args:nil];

    NSLog(@"%@",str1?str1:@"bottomViewUI-sucessed");

    对应的脚本 luaName.lua 如下:

    function topViewUI( )

    aLabel = Label();

    aLabel:text("aaaa");

    aLabel:frame(0, 0, 100, 30);

    end

    function bottomViewUI()

    aLabel = Label();

    aLabel:text("cccc");

    aLabel:frame(0, 0, 100, 30);

    end

    此处遇到一坑:

    正如我前面所述,其官方文档很久没有更新,可能维护也很少。其官方描述 LuaViewCore 用法是这样的:LuaViewCore初始化后,load 脚本,然后就可以调用脚本的方法。但是按照这个流程,调用脚本方法,会返回错误信息“function is nil error”,即方法找不到。原因是,在lua中,方法的定义是放在脚本运行时的,而非编译时。故仅仅编译脚本,是无法调用脚本方法的。正确的流程是:LuaViewCore初始化后,run 脚本,然后就可以调用脚本的方法。(此处已和其官方团队联系确认,是其文档有误)

    Native自定义功能桥接

    源码解析

    在实现自定义功能桥接到Lua层之前,首先要从源码入手,了解LuaView是如何封装Native控件,并且注册到Lua环境中,Lua脚本可以任意创建和操作的!

    正如上面所言,LuaView 初始化时,会初始化一个 LuaViewCore,然后就没有其他什么特别的代码。LuaViewCore 对象作为Lua虚拟机,所以密码就在他这里。LuaViewCore 的初始化方法如下:

    myInit 方法实现了属性的初值赋值等。关键在于 registeLibs 方法。

    此方法将所有LuaViewSDK封装的NativeUI进行了遍历注册到Lua环境中。

    而LVClassProtocal 协议的 +(int) lvClassDefine:(lua_State *)L globalName:(NSString*) globalName 方法,即每个封装的UI类需要实现的,完成类及其方法注册到Lua环境。比如LVImage:

    lua是一种嵌入式的语言,可以作为c的扩展,也可以用c来编写模块了扩展lua。而在进行数据交互的时候,存在这这么一个栈,这个栈的作用是存储lua和c交互的参数,返回值等。如lua调用c函数并传入参数,所有参数会先压入这个栈,c函数执行时从栈中获取参数,执行完后,也会把返回值压入此栈,lua从此栈中获取返回值。(我个人理解是这样的,如有误,烦请指出)

    所以,上面LVImage的注册,首先是将类名和其初始化方法压栈,通过 lua_setglobal 方法,将栈顶的类名和函数注册到lua环境中,并通过globalName(此处是 "Image")进行标注。如此lua脚本中就可以通过 Image() 来创建LVImage对象。

    而下面的 luaL_Reg 结构体,则包含了一组 keyStr -- 方法。则是将这组函数注册到Lua环境中,作为全局函数。Lua脚本中LVImage对象就可以调用这些方法。

    以上就实现了一个Native控件注入到lua环境中进行使用。

    现有LuaView控件的扩展

    上面已经解读了LVImage是如何注册到Lua环境中进行使用。当Lua脚本里setImage 设置图片,传入参数是url时,图片是没有显示的,查看LVImage的方法会发现,因为LVImage 的 setWebImageUrl 是没有实现的。故考虑通过继承的方式扩展LVImage的功能,让其支持网络图片的加载。

    #import "XQImage.h"

    #import "LVHeads.h"

    #import <SDWebImage/UIImageView+WebCache.h>

    @implementation XQImage

    -(void) setWebImageUrl:(NSURL*) url finished:(LVLoadFinished) finished{

    [self sd_setImageWithURL:url];

    }

    @end

    XQImage 继承自LVImage,并且重写了父类的方法 -(void) setWebImageUrl:(NSURL*) url finished:(LVLoadFinished) finished。(此处采用SDWebImage进行图片下载展示)

    自定义子类实现了,但是Lua环境注册的还是父类LVImage,故,lua脚本初始化Image(),还是会初始化父类的实例,故无法调用到子类的图片下载赋值方法。父类的注册,是在LuaView初始化时,那么LuaView初始化后,需要将子类覆盖父类注册到lua环境中,让 globalName(“Image”)对应的是子类:

    self.lv[@"Image"] = [XQImage class];

    如此后,lua脚本 Image() 创建的就是native的XQImage对象。由此实现网络图片的加载和显示。

    完全自定义类的桥接

    上节通过继承的方式扩展 LuaView 已封装的UI控件,并且覆盖注册到lua环境中。那么如何将自定义的一个类,桥接到Lua环境中使用呢?

    正如前面源码解析,了解了 LuaView封装的NativeUI 是如何实现的,所以按部就班,照着这个逻辑实现自定义类的桥接。其中最关键的是实现 LVClassProtocal 协议的方法 + (int)lvClassDefine:(lua_State *)L globalName:(NSString *)globalName; 来实现指定类及其初始化方法,全局函数注册到 Lua 环境中,供 Lua 脚本直接使用。

    +(int) lvClassDefine:(lua_State *)L globalName:(NSString*) globalName{

    [LVUtil reg:L clas:self cfunc:lvNewItem globalName:globalName defaultName:@"XQItemLuaView"];

    const struct luaL_Reg memberFunctions [] = {

    {"image",  setIconImage},

    {"title",    title},

    {NULL, NULL}

    };

    lv_createClassMetaTable(L,META_TABLE_CustomView);

    luaL_openlib(L, NULL, [LVBaseView baseMemberFunctions], 0);

    luaL_openlib(L, NULL, memberFunctions, 0);

    const char* keys[] = { "addView", NULL};// 移除多余API

    lv_luaTableRemoveKeys(L, keys );

    return 1;

    }

    XQItemLuaView 是我自定义的一个 UIView 的子类,遵循 LVProtocal, LVClassProtocal 协议。其上有一个 ImageView 和 Label。C函数 lvNewItem 用于初始化一个 XQItemLuaView 的对象,setIconImage 用于根据 Lua 脚本传入的参数,设置 iconImageView 的图片展示。 title 为根据Lua脚本传入的参数字符串,设置 titleLabel 的text。

    LVClassProtocal 是一个静态协议,源码分析中可以看到,LuaView 加载其扩展类的时候,都是通过初始化 LuaViewCore 时,遍历所有需要加载的类,调用其 + (int)lvClassDefine:(lua_State *)L globalName:(NSString *)globalName 方法,实现加载。

    而完全自定义的类的加载,最好不要去直接更改其 LuaViewCore 源码。所以我创建了一个管理自定义类注册的操作类 XQRegisterManager 。

    #import "XQRegisterManager.h"

    #import "XQItemLuaView.h"

    @implementation XQRegisterManager

    /**

    自定义类的注册管理

    @param luaState 状态机

    */

    +(void)registerClassWithLuaState:(lua_State*)luaState{

    [XQItemLuaView lvClassDefine:luaState globalName:@"XQItemLuaView"];

    }

    @end

    在 LuaView/LuaViewController 初始化后,去调用注册自定义的类。如:

    self.lv = [[LView alloc] initWithFrame:lvRect];

    [XQRegisterManager registerClassWithLuaState:self.lv.luaviewCore.l];

    self.lvCore = [[LuaViewCore alloc]init];

    [XQRegisterManager registerClassWithLuaState:self.lvCore.l];

    总结

    以上是我一周时间学习LuaView的记录。从简单运行一个 Lua脚本开始认识这个SDK,到最后分析源码,来实现自定义类的桥接。下一步的目标是,在此基础上,研究资源脚本下载实现,SDK自带的debuger工具类的使用,以及当脚本出错或者下载失败的降级处理(LuaViewSDK 没有自带降级处理,所有运行失败会有错误抛出,需要根据错误,自行处理降级还是显示失败页面)等。

    如有纰漏,欢迎指出,谢谢!

    Demo地址

    相关文章

      网友评论

        本文标题:LuaView初识

        本文链接:https://www.haomeiwen.com/subject/hbdjxttx.html