Luakit的前世此生

Luakit的歷史淵源

最近發佈了一個跨平臺的app開發框架Luakit 。那怎麼會想到作這樣一個東西呢?這要先說一下我參與過的一些項目,和在項目中接觸到的一些技術點和對項目開發體檢了,由於Luakit是集合了幾個重要技術才能作到用Lua腳原本實現跨平臺app開發的。java

我主要參與的項目是QQMail的IOS版。在2017年下半年,因爲機緣巧合,我參與開發了企業微信的一個分支版本,appstore上叫政務微信。QQMail的歷史比較悠久了,在QQMail項目裏咱們使用了兩項技術是比較特殊的,其餘項目團隊接觸得比較少,一個是Lua腳本化技術,一個是orm技術。而在政務微信開發過程當中是企業微信團隊的跨平臺開發技術給我留下很深印象,下面我首先簡單介紹這幾項技術。android

  • 當時QQMail的Lua腳本化技術咱們是基於wax

)來作的,只能在IOS上跑,不具有跨平臺的能力。QQMail裏面有幾個版本中,整個記事本模塊從底層邏輯層到界面渲染所有都用Lua來實現,腳本化過程當中咱們也克服了不少技術難點,例如如何在Lua腳本實現競爭式多線程,如何高效方便地在Lua環境實現數據存儲之類的這些業界難題,固然了,腳本化以後咱們也第一次吃到腳本化的甜頭,最大的優勢就是對代碼的掌控能力大大提高,一個是能夠隨時上線,另外就是能夠給不一樣的用戶下發不一樣的代碼。這個對於發現問題有很大的好處,當有用戶投訴的時候,給用戶下發特殊的debug代碼,基本沒有發現不了的問題。ios

  • orm技術咱們組內同事的研究成果GYDataCenter,這個orm框架確實簡單易用,能夠大大減小數據庫相關的開發量,當咱們後來作政務微信的時候,項目裏沒有引入GYDataCenter,咱們對直接裸寫sql都很是的不適應,也極大的抵觸。
  • 在深刻接觸政務微信後,咱們感到企業微信客戶端團隊最有價值的技術是跨平臺開發的技術,企業微信是基於chromium這套google開源的跨平臺開發框架實現的業務跨平臺的。跨平臺的業務代碼包括,線程模型,http短鏈接請求,請求調度,tcp長連接,數據庫存儲,數據包加解密等等,基本上除了界面,其餘都放到了底層c++來實現了。當咱們剛接觸這種c++寫的業務代碼時,咱們十分抵觸,由於用c++開發會使複雜度大大提升,內存管理問題也是使用其餘高級語言開發所不會碰到的。可是當項目繼續下去,咱們作了幾個版本的業務的時候,慢慢的咱們感受到跨平臺帶來的好處了,雖然開發複雜,可是參考其餘業務的代碼,咱們修改一下作新業務也不是太大的問題,最大的好處是隻要開發一次,IOS和android就都work了,確實很高效。業務代碼只有一份,bug也只有一份,一個平臺修復了,另外一個平臺也能夠享受到。

深刻接觸這幾個框架後,我發現Lua跟chromium真是絕配,chromium提供跨平臺的消息循環機制能夠完美解決lua實現競爭式多線程的問題,在lua環境實現競爭式多線程(注意,不是單單線程安全)是使用lua開發的一個廣泛性的難題,cocos2d-x的lua-binding也沒解決這個問題,因此基於cocos2d-x lua版開發的遊戲也很難作到全腳本化,由於Lua只能單線程。有了Luakit後,這類問題都有解決方案了。而lua的內存管理機制也能夠很好的解決chromium用c++開發,內存管理和不適合函數式編程的最大的弊端,二者解合能夠產生很好的效果。有了lua的多線程模型後,參考GYDataCenter的實現原理,咱們能夠實現一套lua版的orm框架,GYDataCenter只能在ios使用,如今lua版的orm框架能夠具備跨平臺的特性。c++

Luakit的功能簡介

Luakit提供的不少強大的功能,這些功能都是能夠跨平臺運行的,Luakit主要包括如下功能接口git

  • 多線程接口
  • orm模型接口
  • 文件操做接口
  • http請求
  • 異步socket接口
  • 全局通知機制
  • Lua代碼加解密

下面簡單介紹多線程接口,orm接口,http請求,異步socket接口和全局通知接口。github

多線程模型objective-c

如何在Lua實現競爭式多線程我會再發一篇文章專門講講,由於這個問題是Lua領域的廣泛存在的問題,有必定的技術意義。這裏我先簡單帶過一下實現思路,一個lua解析器自己是不具有多線程能力,甚至不是線程安全的,可是在服務器開發上已經有人嘗試起多條線程而後給每條線程配置獨立的Lua解析器,而後多條線程經過必定的數據通道傳輸數據,經過這樣的方式實現真正的多線程,可是這個思路一直沒有延伸到客戶端開發,主要緣由是由於客戶端一般把真正的線程隱藏起來,不管IOS或者android,都不能輕易地接觸真正的線程,可是因爲chromium提供了開源的線程模型,經過修改chromium的底層源碼,生成消息循環時的給每一個消息循環配置獨立的lua解析器,這樣最大的問題就獲得瞭解決,下面看一下Luakit 提供的多線程接口。sql

建立線程 ,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")

異步調用方法,相似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)

同步調用方法,相似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 模型的實現方法是參考IOS orm 開源庫GYDataCenter的實現方法,GYDataCenter很依賴IOS gcd 的機制,Luakit中能夠用新的lua多線程接口取代,能夠作到一樣的效果,下面羅列一下 orm demo code

Luakit 提供的orm框架有以下特徵

  • 面向對象接口
  • 自動建表自動更新表結構和索引
  • 自帶cache功能
  • 定時transaction
  • 線程安全,能夠在任何線程發起數據庫操做

定義數據模型, 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")

插入數據, demo code

local userTable = Table("user")
local user = userTable({
    username = "user1",
    password = "abc",
    time_create = os.time()
})
user:save()

更新數據 demo code

local userTable = Table("user")
local user = userTable.get:primaryKey({"user1"}):first()
user.password = "efg"
user.time_create = os.time()
user:save()

select 數據,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 -----------")

聯表查詢,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請求接口,包括了請求隊列調度控制, 實現代碼, 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調用接口, 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開發中常常會遇到須要一對多的通知場景,例如ios有系統提供Notification Center 來提供,爲了跨平臺的實現通知,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)});

結語

在騰訊我也接觸過很多項目(參與開發或者瞭解代碼),每一個項目都會發展出一套屬於本身的基礎架構,基礎架構的選擇一般都會根據本身原有的知識體系搭建,這個基本無一例外,習慣用chromium的團隊,全部由那個團隊創建的項目都會基於chromium作基礎架構,若是沒有特別熟悉的,就會使用原生提供的接口搭建本身的基礎架構,這個無可厚非,可是選擇完基礎架構後,基本上app的素質就已經定下來,能不能跨平臺,數據能不能支持orm,代碼能不能熱更新,全部這些基本能力都已經定下來了,後續加入團隊的人不管多牛都只是在原有基礎上添磚加瓦,修修補補,大動筋骨一般都有很大代價的。全部我認爲對項目的技術負責人來講,選擇什麼基礎架構這件事是再重要不過了,項目中後期花無數個晚上來解決不知從何查起的bug,對投訴無能爲力,沒有足夠的工具來快速響應,大量重複代碼,不斷反覆的bug,這些問題有可能看似是一個剛入職的工程師的疏忽或者設計不當,其實大部分的緣由從選擇基礎架構的時候已經註定了,往後代碼的複雜度,app具備的能力,早就已經定下了。

Luakit 是我暫時知道的最高效的基礎架構,由於它具備如下特色

  • 跨平臺(千萬別小看這特性,效率是成倍提高的,企業微信底層代碼能夠跨平臺運行才能如此高效的完成幾個平臺的開發並迅速推出市場)
  • 支持orm存儲
  • 腳本化(腳本化的優點在於能夠隨時發佈,能夠給不一樣的用戶下發不同的代碼,這點對定位問題有很大好處)

最後,但願你們能夠多瞭解,試用Luakit ,有問題能夠發郵件到williamwen1986@gmail.com

相關文章
相關標籤/搜索