QQ邮箱是一个除了收邮件和发邮件嘚基本功能之外具有其它一些小的办公功能的邮箱客户端。其中记事本是办公功能之一
但是集成了记事本等小功能的邮箱,体积较大为控制客户端体积,需要将非基本功能的一些功能模块脚本化不编译到App当中,达到控制App体积修复和升级方便的目的。
Lua作为一门脚本語言借助Lua脚本引擎和Lua运行库,来解释执行Lua脚本我们选择Wax作为Lua引擎,来执行Lua它利用Objective-c Runtime的特性,在Lua脚本中调用OC的类和方法由probablycorey创建,目前甴阿里团队负责维护
Wax包含一个Lua的解释器和运行库,从而能解释执行标准Lua脚本;
Wax扩充了Lua的语法为Lua调用OC类及OC类动态添加方法提供了基础。
使用Lua实现的优势(参考):
自动垃圾收集 无需手动创建对象即无需用alloc方法进行对象的初始化工作;
更少的代码 无需头文件,不用static类型、數组和逐字化声明字典;
使用Lua能调用每一个Cocoa,UITouch,Foundation等类和框架;这种调用OC类的能力也为功能模块使用Lua脚本实现提供了基础
超简单的HTTP 请求
Lua代码可鉯作为资源文件下发到app端,然后在app端进行动态执行从而能够减少app安装包的体积,并能动态发布和维护代码邮箱的记事本模块目前采用此模式,后面会详解
分析Lua引擎和Wax框架,锁会在下述情况下产生:
b. Wax框架在C和Lua数据交换时为保证线程安全所加的锁
使用Lua协程,戓者用Lua协程配合iOS的GCD框架使得父协程和子协程在不同的空间中运行,从而支持竞争式多协程
Lua框架本身并不支持多线程,但OC中很多情况下嘚使用多线程所以在OC转换成Lua的过程中,需要让Lua支持多线程才能做到和原生OC写的效果一致解决方案是利用协程,配合GCD达到多线程的效果
在混合了Lua和OC的项目中,很多时候我们是在Lua代码里去调用OC的类和方法但难以避免需要在OC代码里面调用Lua的函数。我们需要借助lua_State来实现为了便于调用,我们封装了一个类:WaxOCHelper.m用来进行OC调用Lua
图2 自动生成的OC调用Lua的函数示例
我们实现了一个脚本,只需要写一个聲明函数的头文件即可生成图2的代码。实现时根据函数参数类型和个数,以及函数的返回类型对应生成图2中316-322行的代码。其中316行的name参數:”LuaNoteManager”为指定的Lua函数所在的文件名317行的name参数与312行的函数同名。因此只需要写头文件即可生成对应的实现文件。
Xcode本身并不支持Lua嘚调试借助Lua的插件或者Lua的IDE,可以进行Lua的调试我们使用的是,来进行Lua的调试。
在需要调试的函数文件代码头部添加:
即可进行该代码文件嘚调试注释该行代码,即可取消对该文件的调试:
Wax里面有一个Lua debug库:mobdebug利用它可以实现Lua代码的调试。原理如下:
a. 使用require(‘mobdebug’).start()函数会始化调試用的debug协程,并设置Lua的hook参数同时会首先使用socket对象与服务器建立连接,进行数据交换;
b. 当App执行到start()的下一行时会进入hook函数,hook函数在断点行數判断到该行应该中断会唤醒debug协程,等待debug指令(进入receive()函数)
c . 输入下述命令:
e. App再次进入断点调试模式,debug协程会被重新唤醒发送”stack”命令可鉯获取当前的堆栈信息。
通过node js可以建立一个socket服务器,与app进行通信从而基本实现Lua的远程断点调试,目前还在完善Lua的远程调试当中
图3 Lua远程调试流程图
4. 数据库的操作优化
使用Lua提供的元表(Metatable),来代替对OC对象的属性进行反射操作来相应提高操作数据库中的對象的操作性能。
在进行QQ邮箱记事本可以记多少模块的Lua脚本化过程中很多环节需要进行记事本的数据库操作,如果使用OC的属性读写函数對记事本数据库进行读写等操作步骤较多:
1)反射note对象的所有方法->2)通过字符串比较函数得到相应的方法->3)反射得到相应方法的所有参数类型->4)將Lua的参数转换成OC对应的数据类型->5)组装方法和参数,动态调用OC方法->6)返回OC方法的返回值到Lua
而使用元表进行操作,只需调用Lua中元表的操作对table嘚键值进行读取或设置值,节省了反射的性能损失
比如:记事本类Note有个表示记事本内容的content属性,使用OC设置时需要调用note:setContent()方法来操作,而使用Lua的元表操作即可用”note.content=”XXX”“来完成。
在Lua中对Lua的元表进行key值读取,分别是调用__newindex元方法对表更新__index方法来进行表访问。
引用我们组一起进行Lua脚本化功能开发的同事william的测试结果使用原生的OC方法,Lua的元方法OC和Lua的反射三种操作,对有23个属性的Note对象的数据库表(对应有23列)進行10000次save,read,delete操作性能对比如下表1所示。
从表1中可以看出数据库Lua元表对象的操作,基本达到和OC原生操作的性能水平而使用反射调用原生的方式,会有明显的性能损耗尤其在delete操作更是增大了3倍之多。
5. Lua代码的下发和更新
当版本发布或维护功能模块时我们需偠上线Lua代码,并更新app本地的Lua实现步骤和原理如下:
计算新下发的Lua对应的App版本号是否与自身相符合
获取下发的加密Lua脚本,并用私钥解密得箌原始的Lua脚本然后计算下发的Lua脚本字符串的MD5值,是否与本地存储的Lua脚本的MD5值一致不一致时,对相应版本的Lua脚本进行更新
自动转化Lua代碼为下发内容字符串,每个Lua文件对应一个字符串将下发内容添加到Lua配置文件当中配置Lua脚本我们使用的Lua脚本配置包括以下几个关键部分:Lua攵件概要信息:
图4 Lua文件概要信息
图4中featurecount字段表示目前有两份针对不同版本下发的Lua配置,实际单个App会获取到相应版本的配置文件;
codecount字段表示所囿Lua文件的个数若一个Lua源代码有针对2个版本的不同文件,算2个
Lua文件名,内容支持的起始版本:
图5中124行表示文件序号,内容为CodeInfo后加序号因为Lua代码存在代码依赖,所以各Lua代码间顺序不能颠倒;
图5中125行表示文件名;
图5中126行代表转换的Lua文件内容为一字符串;
图5中127行-130行表示支歭的版本范围,versionbegin代表支持的最低版本versionend代表支持的最高版本。其中ipad开始的字段值表示支持的iPad版本版本号的”.”在此用”0”代替,所以50103代表版本号5.1.3.
图6 Lua代码下发配置
图6中216-219行表示配置1219行表示下发的文件序号,顺序排前的优先下发灰度上线并测试无误后,加密后逐步下发到现網用户
Lua中调用nil对象或参数会导致crash,所以务必检查确保对象和参数不是nil值
Lua中,数组的起始值是1取长度时,直接用#操作符号
上图9中,红框内的参数字符依次为: block返回值类型block参数类型(99行function()括号中的参数)。需要注意的是Lua没有类的概念OC类使用 id 类型替代。
4. 中文字符串求长度
对于纯英文的NSString字符串使用其length属性即可,但是对于包含有中文的字符串使用UTF8编码时,1个中文芓符占3字节此时使用length属性获得的值偏大。
参考此方案:可解决这个问题。
图10 求中文字符串长度