前言
作为一名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 没有自带降级处理,所有运行失败会有错误抛出,需要根据错误,自行处理降级还是显示失败页面)等。
如有纰漏,欢迎指出,谢谢!