原文:http://luanova.org/ioswithlua/
本文讨论用Lua创建iOS应用的3种方法。包括用Lua创建完整的应用(Corona)一直到用Lua作为app中的脚本元素(通过Wax或diy)。在此之前,我们需要问自己两个问题:
1、为什么要使用Lua?
2、苹果允许使用Lua吗?
这两个问题是紧密相关的。
如果你在此之前对Lua一无所知,我会简单介绍一下Lua。如果你熟悉Lua,则可以跳过这部分内容。
关于Lua
Lua是一个高效、轻量级、嵌入式脚本语言。类似Javascrip、Ruby或Python。有许多和我一样的使用者,都认为Lua是一种简洁优雅的语言。
Lua始于1993年巴西里约热内卢宗铎天主教大学的Roberto Ierusalimschy, Waldemar Celes 和Luiz Henrique de Figueiredo。它应用在Mi Casa Verde, Adobe Lightroom, Celestia, lighttpd,LuaTeX, nmap,Wireshark, 思科的Adaptive Security Appliance, pbLua,以及成千上万的游戏中(包括 Grim Fandango(神通鬼大),魔兽世界,Tap Tap Revenge(劲乐团)等)。Lua采用MIT协议——这意味着Lua基本无碍于商业和非商业目的。
Lua的主要数据构建机制是表——可变数组和哈希表的结合。列表1列出了一个Lua Table,假设我们用于描述汽车和它的每加仑行驶里程数。我们可以用字符串类型的键来储存车辆信息,如license和make。而采用数字下标索引的方式存储一系列的每加仑里程数。
列表 1: 一个 Lua Table类型
car_data = { license = 'XVW1942', make = 'Volvo', model = 'XC70',30, 31.3,32.4, 34.0}
print(car_data[1]) -- 30
print(car_data['license']) -- XVW1942
print(car_data.license) -- XVW1942 (also!)
在Lua中,数组下标从1开始,而不是0。'--'表示注释开始直至行末。让你想不到的是,Lua既不是面向对象的语言,也不是函数编程语言。但是,它也提供了几种机制允许你定制自己的高级特性。Lua中内包含了各种不同的对象系统如:传统OO系统和无类化OO系统(比如Oracle的Self语言和Io语言。译者注* 这两种都是基于范型的语言)。Lua支持“first class 函数”(译者注*其实是匿名函数, Lua可以在运行时随时构造出一个函数,并把它看作一个对象),闭包以及元特性(如元表和元方法)。Lua能很好地满足函数式编程的需要。
关于Lua 的面向对象编程的介绍,请阅读《Lua 编程》(这本书很好地介绍了Lua的方方面面)。你也应当阅读Luawiki上的Lua示例。
列表2列出了链表类的一种实现。变量List实际上是table,用作所有链表对象的元表。它实现了一种向后查找表索引的类似于类的事件处理机制。"List.__index=List"一行允许我们为List对象创建方法。方法是保存在List元表中的函数。当我们调用list对象的这些函数时,将在List元表中查找这些函数的定义并运行。
这段代码显示了Lua的一系列增强特性:多赋值(以及函数返回多个返回值),方法调用语法糖(':' 符号的作用,类似于在函数调用中增加了一个self参数,这在许多语言,从Python到OC都能见到)。
列表 2: Linked List 类
List = {}
List.__index = List
function List:new()
local l = { head = {},tail = {}, size = 0 }
l.head.__next,l.tail.__prev = l.tail, l.head
return setmetatable(l,self)
end
function List:first()
if self.size > 0 then
returnself.head.next
else
return nil
end
end
function List:addFirst(elem)
local node = { prev= self.head, value = elem,
next = self.head.next }
node.next.prev =node
self.head.next =node
self.size =self.size + 1
end
mylist = List:new()
mylist:addFirst(12)
print(mylist:first())
在这里,我忽略了一些重要的和有趣的东西(比如闭包)。但至少,你已经学到了一点Lua的皮毛。在后面我们进入iPhone编码的时候,会看到更多的Lua代码。更多关于Lua的介绍,请阅读这个网站。
iOS支持脚本吗?
正如本文开头列出的两个问题,尤其是第2个问题:“iPhone允许使用Lua(或其他解释型语言)吗?”毕竟,早在苹果的IDP许可协议中就已经阐明“只有苹果官方的API和内置解释器所支持的解释型代码能被下载或用于app中”。
事实上,本文的拟写大纲时,苹果已经改变了原来禁止开发者在app使用除OC和Javascript(Javascript能在web app或者本地 app中使用——通过UIWebView)以外的其他语言的条款(circa2010 四月)。最近(2010 九月),苹果再次改变了这个条款,允许使用脚本语言。
但仍然有几个重要的限制。更主要的是,虽然你可以使用Lua(及其它脚本语言),但你的app不能允许用户从web上下载插件(用过应用程序购买吗?),也不能允许用户编写脚本、下载脚本等。有大量的商店应用在使用Lua这样的语言(比如劲乐团)。
当然,在app中包含Lua这类语言的两个最为主要的作用,就是创建插件系统,让用户自己能够编写脚本。除此之外还有许多。
如何在iOS开发中使用Lua?
尽管你不能为终端用户创建一个插件系统,也不能让用户自己编写脚本,但你仍然能以一种插件的方式开发你的系统!这可以加快原型的开发速度,同时在下个版本中有助于添加新的功能。使用Lua还有另一种好处,它允许你进行“快速原型开发”(我最喜欢抱怨的一句话:不要闭门造车式地编程),缓解甚至不需要内存管理,允许更多的团队成员参与到开发中来(有许多Lua项目根本没有程序员在编写代码),应用程序优化更加轻松,提供更强劲的持久化机制。
简而言之,Lua节省了开发时间,降低了开发门槛。生活变得如此轻松愉快!假设你已经决定使用Lua,那么我们该如何做起呢?
Corona
Ansca Mobile公司的Corona允许你完全用Lua来开发iOS应用,以及Android应用。你可以用同样的源代码编译出iOS和Android程序。这正是Lua(实际上是Corona)为何如此吸引人的原因:跨平台。
列表3 是一个app的全部源代码。
列表3: Swirly Text 应用中的main.lua
local w, h = display.stageWidth, display.stageHeight
local dx, dy, dtheta = 5, 5, 5
local background = display.newRect(0, 0, w, h)
background:setFillColor(255, 255, 255)
local message = display.newText('Hello from Corona', w/2, h/2)
message:setTextColor(0, 0, 200)
local function update(event)
local counter_spin =false
message:translate(dx, dy)
message:rotate(dtheta)
if message.x > wor message.x < 0 then
dx= -1 * dx
counter_spin = true
end
if message.y > hor message.y < 0 then
dy = -1 * dy
counter_spin = true
end
if counter_spin then
dtheta = -1 * dtheta
end
end
Runtime:addEventListener('enterFrame', update)
Corona程序可以用任何文本编辑器开发——我用的是Emacs。所有Lua源代码中用到的资源(图片、声音、数据)必需放在同一个目录,Corona需要main.lua文件来启动app。可以在Corona模拟器中测试代码(支持Intel cpu和Power pc的Mac)。图1显示了我的corona IDE: Emacs(包含Lua文件出口和项目窗口)、Corona终端(可以从诊断中打印调试信息),以及Corona模拟器。
图 1: 我的 Corona 'IDE'
从左至右(反时针方向):Corona模拟器、Emacs的两个窗口(源代码窗口和项目目录窗口)、Corona 终端(输出调试信息)。
想要在物理硬件上运行程序,使用Corona 模拟器的Openfor Build命令。要以iOS编译,你应该提供一个Provisioning Profile(开发或部署)——没错,你并不需要IDP帐号——这两个文件随同app代码和资源一同上传到Ansca公司的服务器上,然后将编译结果返回给你。要以Android编译,你应当有适当的签名证书。然后随着编译过程把你的代码传到Ansca的服务器上。你不必安装AndroidSDK。我没有太深入地研究,但编译后的.apk文件和.app文件已经包含了所有你Lua代码以某种方式处理过的东西。短暂的查看后表明,那不是标准的编译后的Lua字节码,但应该是类似的格式。
Corona事件系统可以处理触摸(包括多点触摸),访问GPS和加速器,处理动画以及自定义事件。它还有一个强大的图形系统,允许你绘制圆、矩形和文本。最近还增加了折线,允许你绘制多边形。你可以显示图片。Corona允许你把这些对象组合在一起然后对他们进行变换。列表4,摘自太阳系模拟器的代码片段,展示了组合多个图形对象的简单例子。其他Corona支持的特性还包括声视频播放,加密算法库,LuaSocket网络库,SQLite存取库LuaSQLite等。还能访问本地组件包括textfield、alert和activityindicator。你还可以用webview做诸如登录屏幕之类的事情,有一个示例程序提供了一个库,可以连接到Facebook。我最近看到有一个游戏(很贵)使用了Box2D物理引擎、角色和一些OpenFeint的功能(类似排行榜)。
列表 4: 太阳系应用代码片段
function new(params)
local color =params.color or planet_colors[random(#planet_colors)]
local radius =params.radius or planetRadius()
local planet =display.newGroup()
planet.theta = 0
local x = params.x -ox
local y = params.y -oy
planet.orbital_radius = sqrt(x*x+y*y)
local body =display.newCircle(x + ox, y + oy, radius, radius)
body:setFillColor(unpack(color))
planet:insert(body,true)
planet.body = body
planet.delta_theta =(40/planet.orbital_radius) * 0.1
return planet
end
把table作为函数参数传递,可以使用命名参数并提供默认值。因此会有 local radius = params.radius orplanetRadius() 这样的写法。
Corona为你做了许多,但同时也有许多不足。最大的问题是对本地控件的访问限制。由于Corona模拟器的限制,它对本地控件的访问是糟糕的。在模拟器中,本地alerts和activityindicators用OSX equivalents实现而不是iOS widgets实现的。textfield、textbox以及web popups在模拟器运行时是不可用的。这在开发时让人痛苦。
最后,除了ANSCA标准以外,无法访问O-CAPI。不仅是大量的标准库,而且第3方库也无法使用,如Three20或 mobileads 这样的APIs。当然,随着Corona Android版本的发布,你可能不想访问OC API因为它限制你的应用程序跨平台的能力(或者增加了复杂性)。最好是通过Lua的CAPI来扩展,就像是许多跨平台的项目一样。
我在ANSCA 小组在他们论坛上的讨论非常有用。随着2.0版本(2010.9)的发布,Corona向每位开发者每年收取249美金。对于游戏版,收费每开发者每年349美金。Ansca公司的网站暗示游戏版的价格只是预览版的。这意味着当正式版发布时价格将会更高。
DIY
很容易把Lua解释器放到iOS app中。打开一个Xcode项目把Lua的源文件(除lua.c和luac.c命令行程序外)加到项目中。编译。你就可以使用标准的LuaC API去创建一个解释起并运行源代码,就像 iLua所做的。你可以在 http://github.com/profburke/ilua下载这个示例代码。iLuaShell是一个简单的view-based application,它提供两个文本框给用户——一个给用户输入Lua代码,另一个是不可编辑的,仅仅是显示Lua代码计算的结果。
这个工作用evaluate方法完成,如列表5所示。在方法中首先获取第1个文本域的值,把它交给Lua解释器解析和执行,然后把Lua输出结果放到第2个文本域中。
列表 5 evaluate 方法
-(void)evaluate {
int err;
[input resignFirstResponder];
lua_settop(L, 0);
err = luaL_loadstring(L, [input.text
cStringUsingEncoding:NSASCIIStringEncoding]);
if (0 != err) {
output.text = [NSString stringWithCString:lua_tostring(L, -1)
encoding:NSASCIIStringEncoding];
lua_pop(L, 1);
return;
}
err = lua_pcall(L, 0, LUA_MULTRET, 0);
if (0 != err) {
output.text = [NSString stringWithCString:lua_tostring(L, -1)
encoding:NSASCIIStringEncoding];
lua_pop(L, 1);
return;
}
int nresults =lua_gettop(L);
if (0 == nresults) {
output.text = @"<no results>";
} else {
NSString *outputNS = [NSString string];
for(int i = nresults; i > 0; i--) {
outputNS = [outputNSstringByAppendingFormat:@"%s ",
lua_tostring(L, -1* i)];
}
lua_pop(L, nresults);
output.text = outputNS;
}
}
注意错误的捕捉和处理被极度简化了。一个好的Luashell应该要做得更多一些,并不是随随便便就可以往苹果商店中放的…
这样做的弊端在哪里?最大的问题是到OC的桥接丢失了。理想状态下我们应该从Lua中调用OC方法,以及相反方向的调用。Lua中要是能实现代码回调和委托方法就太爽了。
iPhone Wax
CoreyJohnson的iOS Wax最吸引人的地方是在Lua和OC间实现了相互调用。通过Wax,你能轻易用Lua继承一个OC类!列表6展示用扩Wax实现对viewController类的扩展。这段代码是DIY小节中提及的app的一部分。
列表 6:RootViewController.lua
waxClass{'RootViewController', UI.ViewController }
function init(self)
self.super:init()
self.input =UI.TextView:initWithFrame(CGRect(20, 20, 280, 114))
self.output =UI.TextView:initWithFrame(CGRect(20, 184, 280, 225))
localevalButton = UI.Button:buttonWithType(UIButtonTypeRoundedRect)
evalButton:setTitle_forState('Evaluate', UIControlStateNormal)
evalButton:setFrame(CGRect(200,142, 100, 32))
evalButton:addTarget_action_forControlEvents(self, 'eval:',
UIControlEventTouchUpInside)
self.evalButton = evalButton
self:view():addSubview(self.input)
self:view():addSubview(self.output)
self:view():addSubview(self.evalButton)
return self
end
function eval(self, sender)
self.input:resignFirstResponder()
local code,errmsg = loadstring(self.input:text())
if not codethen
self.output:setText(errmsg)
return
end
local success,result = pcall(code)
print('resultis ' .. tostring(result))
if not successthen
self.output:setText('Error: ' .. tostring(result))
else
self.output:setText(tostring(result))
end
end
waxClass函数实际定义了一个新的OC类。本例中我们定义了一个名为RootViewController的类派生自UIViewController。(在Wax中,该OC类被放在了UI.ViewController命名空间中,而不是UIViewController)。
这个类的Lua类型是table(其实是userdata,但你可以看成是table),因此self.input是Lua表字段而不是OC属性。访问属性你必需用setter/getter方法,例如self.output:setText()。我就是在这一点上被误导了,直到某天我在这个mail list中问到这个问题才恍然大悟,从此再也不会搞混了(这个maillist上的人们都非常友好)。
Wax类也可以实现协议。例如,在这个示例Stats中,演示了用两个定制的UITableviewController类处理UITableView。这个两个类都实现了UITableViewDelegate和UITableViewDataSource协议。列表7展现了一个类,实现了分组表视图的UITableViewDataSource协议。
列表 7: SortedDataSource.lua
waxClass{'SortedDataSource', NS.Object, protocols ={'UITableViewDataSource'}, }
function init(self, source_table)
self.source_table = source_table
return self
end
function numberOfSectionsInTableView(self, tableView)
return#self.source_table.headers
end
function tableView_numberOfRowsInSection(self, tableView, section)
local index =self.source_table.headers[section+1]
return#self.source_table[index]
end
function tableView_cellForRowAtIndexPath(self, tableView,indexPath)
localidentifier = 'TableViewCell'
local cell = tableView:dequeueReusableCellWithIdentifier(identifier)
cell = cell orUI.TableViewCell:initWithStyle_reuseIdentifier(UITableViewCellStyleDefault,
identifier)
local key =self.source_table.headers[indexPath:section()+1]
localcomponent = self.source_table[key]
local player =component[indexPath:row()+1]
cell:setText(player[1] .. ' ' .. player[2] .. ' ' .. player[3])
return cell
end
function tableView_titleForHeaderInSection(self, tableView, section)
returnself.source_table.headers[section+1]
end
注意在tableView_titleForHeaderInSection函数中,Lua字符串和OC字符串是自动转换的。
Wax使用的统一命名方案允许你很容易猜到在Lua中访问OC方法的函数名称。Wax使用TextMate来操作OC调用。例如,你可以直接从Xcode文档粘贴方法签名并转换为Lua调用(我正在犹豫是写一个Emacs函数来干这件事情,还是打开Textmate,然后自己去喝一罐Kool-aid。)
Wax还有许多好东西比如访问SQLite、简单的HTTP请求、XML和JSON处理、CG动画、渐变。另外最近还增加了用Lua编写应用程序委托(不需要在启动Wax时再用OC来实现),从命令行运行测试。最有趣和给力的消息是,Wax现在提供了一个交互式控制台,你可以telnet到模拟器(或者设备上!)与运行的程序进行交互:调整参数或查看运行状态。
Wax是开源的,也使用MIT协议。现在,这个项目由众多开发者参与和使用。
结束语
应用商店中,用Lua开发的app是被接受的。Ansca论坛罗列的Coronaapp的超过了150个主题(我没有全部看过…它们不全部是新的应用)。看过Wax的邮件列表,你会发现鲜有开发者声明在使用Wax编写app,很可能还有更多的开发者没有附在列表中。还有许多App采用了DIY的方法。
Corona提供一种创建iOS应用的可选方案,但前提是你不需要本地UI元素。可以加入一个本地的TextField,但在模拟器中你看不到它们,这个现实让人无语。但考虑到它的Android兼容,Corona仍然值得一看。我喜欢用Corona并希望它能有更多的改善。
DIY的方法是截然不同的做法:你有完全的控制。但如果你需要在Lua和OC间有大量的交互,需要大量的工作去做。Johnson的wax在Lua和OC间桥接得非常好,在LuaC库上也做得很好——有的地方Corona根本就没有进行处理。
尽管强如人意,iOS开发者协议中的近期改变仍然意味着你能在iOS开发中使用Lua而不用担心app被拒绝。我相信在你的iOS项目中可以使用Lua这样的技术能有助于开发。给Corona和Wax一个积极的环境。此外,如果为了方便你更直接地控制对Lua的使用,我建议你充分体会这门语言的精华。