Luakit的歷史淵源
最近發(fā)布了一個跨平臺的app開發(fā)框架Luakit 。那怎么會想到做這樣一個東西呢疾就?這要先說一下我參與過的一些項目,和在項目中接觸到的一些技術點和對項目開發(fā)體檢了,因為Luakit是集合了幾個重要技術才能做到用Lua腳本來實現(xiàn)跨平臺app開發(fā)的。
我主要參與的項目是QQMail的IOS版祈秕。在2017年下半年,由于機緣巧合雏胃,我參與開發(fā)了企業(yè)微信的一個分支版本请毛,appstore上叫政務微信。QQMail的歷史比較悠久了瞭亮,在QQMail項目里我們使用了兩項技術是比較特殊的获印,其他項目團隊接觸得比較少,一個是Lua腳本化技術,一個是orm技術兼丰。而在政務微信開發(fā)過程中是企業(yè)微信團隊的跨平臺開發(fā)技術給我留下很深印象,下面我首先簡單介紹這幾項技術唆缴。
當時QQMail的Lua腳本化技術我們是基于wax來做的鳍征,只能在IOS上跑,不具備跨平臺的能力面徽。QQMail里面有幾個版本中艳丛,整個記事本模塊從底層邏輯層到界面渲染全部都用Lua來實現(xiàn),腳本化過程中我們也克服了很多技術難點趟紊,例如如何在Lua腳本實現(xiàn)競爭式多線程氮双,如何高效方便地在Lua環(huán)境實現(xiàn)數(shù)據(jù)存儲之類的這些業(yè)界難題,當然了霎匈,腳本化之后我們也第一次吃到腳本化的甜頭戴差,最大的優(yōu)點就是對代碼的掌控能力大大提升,一個是可以隨時上線铛嘱,另外就是可以給不同的用戶下發(fā)不同的代碼暖释。這個對于發(fā)現(xiàn)問題有很大的好處,當有用戶投訴的時候墨吓,給用戶下發(fā)特殊的debug代碼球匕,基本沒有發(fā)現(xiàn)不了的問題。
orm技術我們組內(nèi)同事的研究成果GYDataCenter帖烘,這個orm框架確實簡單易用亮曹,可以大大減少數(shù)據(jù)庫相關的開發(fā)量,當我們后來做政務微信的時候秘症,項目里沒有引入GYDataCenter照卦,我們對直接裸寫sql都非常的不適應,也極大的抵觸历极。
在深入接觸政務微信后窄瘟,我們感到企業(yè)微信客戶端團隊最有價值的技術是跨平臺開發(fā)的技術,企業(yè)微信是基于chromium這套google開源的跨平臺開發(fā)框架實現(xiàn)的業(yè)務跨平臺的趟卸√愦校跨平臺的業(yè)務代碼包括,線程模型锄列,http短連接請求图云,請求調(diào)度,tcp長鏈接邻邮,數(shù)據(jù)庫存儲竣况,數(shù)據(jù)包加解密等等,基本上除了界面筒严,其他都放到了底層c++來實現(xiàn)了丹泉。當我們剛接觸這種c++寫的業(yè)務代碼時情萤,我們十分抵觸,因為用c++開發(fā)會使復雜度大大提高摹恨,內(nèi)存管理問題也是使用其他高級語言開發(fā)所不會碰到的筋岛。但是當項目繼續(xù)下去,我們做了幾個版本的業(yè)務的時候晒哄,慢慢的我們感覺到跨平臺帶來的好處了睁宰,雖然開發(fā)復雜,但是參考其他業(yè)務的代碼寝凌,我們修改一下做新業(yè)務也不是太大的問題柒傻,最大的好處是只要開發(fā)一次,IOS和android就都work了较木,確實很高效红符。業(yè)務代碼只有一份,bug也只有一份劫映,一個平臺修復了违孝,另一個平臺也可以享受到。
深入接觸這幾個框架后泳赋,我發(fā)現(xiàn)Lua跟chromium真是絕配雌桑,chromium提供跨平臺的消息循環(huán)機制可以完美解決lua實現(xiàn)競爭式多線程的問題,在lua環(huán)境實現(xiàn)競爭式多線程(注意祖今,不是單單線程安全)是使用lua開發(fā)的一個普遍性的難題校坑,cocos2d-x的lua-binding也沒解決這個問題,所以基于cocos2d-x lua版開發(fā)的游戲也很難做到全腳本化千诬,因為Lua只能單線程耍目。有了Luakit后,這類問題都有解決方案了徐绑。而lua的內(nèi)存管理機制也可以很好的解決chromium用c++開發(fā)邪驮,內(nèi)存管理和不適合函數(shù)式編程的最大的弊端,兩者解合可以產(chǎn)生很好的效果傲茄。有了lua的多線程模型后毅访,參考GYDataCenter的實現(xiàn)原理,我們可以實現(xiàn)一套lua版的orm框架盘榨,GYDataCenter只能在ios使用喻粹,現(xiàn)在lua版的orm框架可以具有跨平臺的特性。
Luakit的功能簡介
Luakit提供的很多強大的功能草巡,這些功能都是可以跨平臺運行的守呜,Luakit主要包括以下功能接口
- 多線程接口
- orm模型接口
- 文件操作接口
- http請求
- 異步socket接口
- 全局通知機制
- Lua代碼加解密
下面簡單介紹多線程接口,orm接口,http請求查乒,異步socket接口和全局通知接口弥喉。
多線程模型
如何在Lua實現(xiàn)競爭式多線程我會再發(fā)一篇文章專門講講,因為這個問題是Lua領域的普遍存在的問題侣颂,有一定的技術意義档桃。這里我先簡單帶過一下實現(xiàn)思路,一個lua解析器本身是不具備多線程能力憔晒,甚至不是線程安全的,但是在服務器開發(fā)上已經(jīng)有人嘗試起多條線程然后給每條線程配置獨立的Lua解析器蔑舞,然后多條線程通過一定的數(shù)據(jù)通道傳輸數(shù)據(jù)拒担,通過這樣的方式實現(xiàn)真正的多線程,但是這個思路一直沒有延伸到客戶端開發(fā)攻询,主要原因是因為客戶端通常把真正的線程隱藏起來从撼,無論IOS或者android,都不能輕易地接觸真正的線程钧栖,但是由于chromium提供了開源的線程模型低零,通過修改chromium的底層源碼,生成消息循環(huán)時的給每個消息循環(huán)配置獨立的lua解析器拯杠,這樣最大的問題就得到了解決掏婶,下面看一下Luakit 提供的多線程接口。
創(chuàng)建線程 潭陪,demo code
-- Parma1 is the thread type ,there are five types of thread you can create.
-- BusinessThreadUI
-- BusinessThreadDB
-- BusinessThreadLOGIC
-- BusinessThreadFILE
-- BusinessThreadIO
-- Param2 is the thread name
-- Result is new threadId which is the token you should hold to do further action
local newThreadId = lua.thread.createThread(BusinessThreadLOGIC,"newThread")
異步調(diào)用方法雄妥,類似IOS gcd中的 dispatch_async , demo code
-- Parma1 is the threadId for which you want to perform method
-- Parma2 is the modelName
-- Parma3 is the methodName
-- The result is just like you run the below code on a specified thread async
-- require(modelName).methodName("params", 1.1, {1,2,3}, function (p)
-- end)
lua.thread.postToThread(threadId,modelName,methodName,"params", 1.1, {1,2,3}, function (p)
-- do something here
end)
同步調(diào)用方法依溯,類似IOS gcd中的 dispatch_sync 老厌, demo code
-- Parma1 is the threadId for which you want to perform method
-- Parma2 is the modelName
-- Parma3 is the methodName
-- The result is just like you run the below code on a specified thread sync
-- local result = require(modelName).methodName("params", 1.1, {1,2,3}, function (p)
-- end)
local result = lua.thread.postToThreadSync(threadId,modelName,methodName,"params", 1.1, {1,2,3}, function (p)
-- do something here
end)
orm接口
orm 模型的實現(xiàn)方法是參考IOS orm 開源庫GYDataCenter的實現(xiàn)方法,GYDataCenter很依賴IOS gcd 的機制黎炉,Luakit中可以用新的lua多線程接口取代枝秤,可以做到同樣的效果,下面羅列一下 orm demo code
Luakit 提供的orm框架有如下特征
- 面向對象接口
- 自動建表自動更新表結構和索引
- 自帶cache功能
- 定時transaction
- 線程安全慷嗜,可以在任何線程發(fā)起數(shù)據(jù)庫操作
定義數(shù)據(jù)模型淀弹, demo code
-- Add the define table to dbData.lua
-- Luakit provide 7 colum types
-- IntegerField to sqlite integer
-- RealField to sqlite real
-- BlobField to sqlite blob
-- CharField to sqlite varchar
-- TextField to sqlite text
-- BooleandField to sqlite bool
-- DateTimeField to sqlite integer
user = {
__dbname__ = "test.db",
__tablename__ = "user",
username = {"CharField",{max_length = 100, unique = true, primary_key = true}},
password = {"CharField",{max_length = 50, unique = true}},
age = {"IntegerField",{null = true}},
job = {"CharField",{max_length = 50, null = true}},
des = {"TextField",{null = true}},
time_create = {"DateTimeField",{null = true}}
},
-- when you use, you can do just like below
local Table = require('orm.class.table')
local userTable = Table("user")
插入數(shù)據(jù), demo code
local userTable = Table("user")
local user = userTable({
username = "user1",
password = "abc",
time_create = os.time()
})
user:save()
更新數(shù)據(jù) demo code
local userTable = Table("user")
local user = userTable.get:primaryKey({"user1"}):first()
user.password = "efg"
user.time_create = os.time()
user:save()
select 數(shù)據(jù)洪添,demo code
local userTable = Table("user")
local users = userTable.get:all()
print("select all -----------")
local user = userTable.get:first()
print("select first -----------")
users = userTable.get:limit(3):offset(2):all()
print("select limit offset -----------")
users = userTable.get:order_by({desc('age'), asc('username')}):all()
print("select order_by -----------")
users = userTable.get:where({ age__lt = 30,
age__lte = 30,
age__gt = 10,
age__gte = 10,
username__in = {"first", "second", "creator"},
password__notin = {"testpasswd", "new", "hello"},
username__null = false
}):all()
print("select where -----------")
users = userTable.get:where({"scrt_tw",30},"password = ? AND age < ?"):all()
print("select where customs -----------")
users = userTable.get:primaryKey({"first","randomusername"}):all()
print("select primaryKey -----------")
聯(lián)表查詢垦页,demo code
local userTable = Table("user")
local newsTable = Table("news")
local user_group = newsTable.get:join(userTable):all()
print("join foreign_key")
user_group = newsTable.get:join(userTable,"news.create_user_id = user.username AND user.age < ?", {20}):all()
print("join where ")
user_group = newsTable.get:join(userTable,nil,nil,nil,{create_user_id = "username", title = "username"}):all()
print("join matchColumns ")
http請求
Luakit提供了http請求接口,包括了請求隊列調(diào)度控制, 實現(xiàn)代碼, demo code
-- url , the request url
-- isPost, boolean value represent post or get
-- uploadContent, string value represent the post data
-- uploadPath, string value represent the file path to post
-- downloadPath, string value to tell where to save the response
-- headers, tables to tell the http header
-- socketWatcherTimeout, int value represent the socketTimeout
-- onResponse, function value represent the response callback
-- onProgress, function value represent the onProgress callback
lua.http.request({ url = "http://tj.nineton.cn/Heart/index/all?city=CHSH000000",
onResponse = function (response)
end})
異步socket接口
Luakit 提供了非阻塞的socket調(diào)用接口干奢, demo code
local socket = lua.asyncSocket.create("127.0.0.1",4001)
socket.connectCallback = function (rv)
if rv >= 0 then
print("Connected")
socket:read()
end
end
socket.readCallback = function (str)
print(str)
timer = lua.timer.createTimer(0)
timer:start(2000,function ()
socket:write(str)
end)
socket:read()
end
socket.writeCallback = function (rv)
print("write" .. rv)
end
socket:connect()
通知接口
app開發(fā)中經(jīng)常會遇到需要一對多的通知場景痊焊,例如ios有系統(tǒng)提供Notification Center 來提供,為了跨平臺的實現(xiàn)通知,Luakit也提供通知接口
Lua register and post notification, demo code
lua.notification.createListener(function (l)
local listener = l
listener:AddObserver(3,
function (data)
print("lua Observer")
if data then
for k,v in pairs(data) do
print("lua Observer"..k..v)
end
end
end
)
end);
lua.notification.postNotification(3,
{
lua1 = "lua123",
lua2 = "lua234"
})
Android register and post notification, demo code
LuaNotificationListener listener = new LuaNotificationListener();
INotificationObserver observer = new INotificationObserver() {
@Override
public void onObserve(int type, Object info) {
HashMap<String, Integer> map = (HashMap<String, Integer>)info;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
Log.i("business", "android onObserve");
Log.i("business", entry.getKey());
Log.i("business",""+entry.getValue());
}
}
};
listener.addObserver(3, observer);
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("row", new Integer(2));
NotificationHelper.postNotification(3, map);
IOS register and post notification, demo code
_notification_observer.reset(new NotificationProxyObserver(self));
_notification_observer->AddObserver(3);
- (void)onNotification:(int)type data:(id)data
{
NSLog(@"object-c onNotification type = %d data = %@", type , data);
}
post_notification(3, @{@"row":@(2)});
結語
在騰訊我也接觸過不少項目(參與開發(fā)或者了解代碼)薄啥,每個項目都會發(fā)展出一套屬于自己的基礎架構辕羽,基礎架構的選擇通常都會根據(jù)自己原有的知識體系搭建,這個基本無一例外垄惧,習慣用chromium的團隊刁愿,所有由那個團隊建立的項目都會基于chromium做基礎架構,如果沒有特別熟悉的到逊,就會使用原生提供的接口搭建自己的基礎架構铣口,這個無可厚非,但是選擇完基礎架構后觉壶,基本上app的素質就已經(jīng)定下來脑题,能不能跨平臺,數(shù)據(jù)能不能支持orm铜靶,代碼能不能熱更新叔遂,所有這些基本能力都已經(jīng)定下來了,后續(xù)加入團隊的人無論多牛都只是在原有基礎上添磚加瓦争剿,修修補補已艰,大動筋骨通常都有很大代價的。所有我認為對項目的技術負責人來說蚕苇,選擇什么基礎架構這件事是再重要不過了哩掺,項目中后期花無數(shù)個晚上來解決不知從何查起的bug,對投訴無能為力捆蜀,沒有足夠的工具來快速響應疮丛,大量重復代碼,不斷反復的bug辆它,這些問題有可能看似是一個剛入職的工程師的疏忽或者設計不當誊薄,其實大部分的原因從選擇基礎架構的時候已經(jīng)注定了,日后代碼的復雜度锰茉,app具有的能力呢蔫,早就已經(jīng)定下了。
Luakit 是我暫時知道的最高效的基礎架構飒筑,因為它具有以下特點
- 跨平臺(千萬別小看這特性片吊,效率是成倍提升的,企業(yè)微信底層代碼可以跨平臺運行才能如此高效的完成幾個平臺的開發(fā)并迅速推出市場)
- 支持orm存儲
- 腳本化(腳本化的優(yōu)勢在于可以隨時發(fā)布协屡,可以給不同的用戶下發(fā)不一樣的代碼俏脊,這點對定位問題有很大好處)
最后,希望大家可以多了解肤晓,試用Luakit 爷贫,有問題可以發(fā)郵件到williamwen1986@gmail.com